/**
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.livingstories.client.contentmanager;
import com.google.gwt.ajaxloader.client.AjaxLoader;
import com.google.gwt.ajaxloader.client.AjaxLoader.AjaxLoaderOptions;
import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.ChangeEvent;
import com.google.gwt.event.dom.client.ChangeHandler;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.FocusEvent;
import com.google.gwt.event.dom.client.FocusHandler;
import com.google.gwt.event.dom.client.KeyUpEvent;
import com.google.gwt.event.dom.client.KeyUpHandler;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.maps.client.MapWidget;
import com.google.gwt.maps.client.control.SmallMapControl;
import com.google.gwt.maps.client.event.MapRightClickHandler;
import com.google.gwt.maps.client.geocode.Geocoder;
import com.google.gwt.maps.client.geocode.LatLngCallback;
import com.google.gwt.maps.client.geom.LatLng;
import com.google.gwt.maps.client.overlay.Marker;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.CheckBox;
import com.google.gwt.user.client.ui.DeckPanel;
import com.google.gwt.user.client.ui.DisclosurePanel;
import com.google.gwt.user.client.ui.DockPanel;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.Grid;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.InlineLabel;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.ListBox;
import com.google.gwt.user.client.ui.PopupPanel;
import com.google.gwt.user.client.ui.RadioButton;
import com.google.gwt.user.client.ui.SimplePanel;
import com.google.gwt.user.client.ui.TextArea;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwt.user.datepicker.client.DateBox;
import com.google.gwt.user.datepicker.client.DatePicker;
import com.google.livingstories.client.AssetContentItem;
import com.google.livingstories.client.AssetType;
import com.google.livingstories.client.ContentItemType;
import com.google.livingstories.client.BackgroundContentItem;
import com.google.livingstories.client.BaseContentItem;
import com.google.livingstories.client.ContentRpcService;
import com.google.livingstories.client.ContentRpcServiceAsync;
import com.google.livingstories.client.DataContentItem;
import com.google.livingstories.client.DefaultContentItem;
import com.google.livingstories.client.EventContentItem;
import com.google.livingstories.client.Importance;
import com.google.livingstories.client.LivingStoryRpcService;
import com.google.livingstories.client.LivingStoryRpcServiceAsync;
import com.google.livingstories.client.Location;
import com.google.livingstories.client.NarrativeContentItem;
import com.google.livingstories.client.NarrativeType;
import com.google.livingstories.client.PlayerContentItem;
import com.google.livingstories.client.PlayerType;
import com.google.livingstories.client.PublishState;
import com.google.livingstories.client.QuoteContentItem;
import com.google.livingstories.client.ReactionContentItem;
import com.google.livingstories.client.StoryPlayerContentItem;
import com.google.livingstories.client.Theme;
import com.google.livingstories.client.lsp.views.contentitems.BasePlayerPreview;
import com.google.livingstories.client.lsp.views.contentitems.StreamViewFactory;
import com.google.livingstories.client.ui.ContentItemListBox;
import com.google.livingstories.client.ui.CoordinatedLivingStorySelector;
import com.google.livingstories.client.ui.EnumDropdown;
import com.google.livingstories.client.ui.ItemList;
import com.google.livingstories.client.ui.RichTextEditor;
import com.google.livingstories.client.ui.SingleContentItemSelectionPanel;
import com.google.livingstories.client.ui.SuggestionAwareContentItemListBox;
import com.google.livingstories.client.util.Constants;
import com.google.livingstories.client.util.DateUtil;
import com.google.livingstories.client.util.GlobalUtil;
import com.google.livingstories.client.util.LivingStoryData;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Page to enter content items.
* TODO: convert to using UiBinder.
*/
public class ContentItemManager extends ManagerPane {
private static final int MAP_HEIGHT = 256;
private static final int MAP_WIDTH = 256;
private static final int MAP_ZOOM = 10;
@SuppressWarnings("deprecation")
private static final String DEFAULT_TIME_STRING = DateUtil.formatTime(new Date(0, 0, 1, 12, 0));
private static final int LONG_TEXTBOX_VISIBLE_LENGTH = 60;
private static final ContentManagerMessages msgs = GWT.create(ContentManagerMessages.class);
/**
* Create a remote service proxy to talk to the server-side content persisting service.
*/
private final ContentRpcServiceAsync contentRpcService = GWT.create(ContentRpcService.class);
/**
* Create a remote service proxy to talk to the server-side living story persisting service.
*/
private final LivingStoryRpcServiceAsync livingStoryService
= GWT.create(LivingStoryRpcService.class);
private DeckPanel contentPanel;
private EnumDropdown<ContentItemType> contentItemTypeSelector;
private DeckPanel specialAttributesPanel;
private Label contentTitle;
private RichTextEditor contentEditor;
private Label contentItemIdLabel;
private Label timestamp;
private CoordinatedLivingStorySelector livingStorySelector;
private ChangeHandler livingStorySelectionHandler;
private EnumDropdown<Importance> importanceSelector;
private ContentItemListBox contentItemListBox;
// Contributor management stuff
private HTML contributorListHtml;
private Label clearContributorsControl;
private PlayerSuggestAndAddPanel contributorSuggestPanel;
private Map<Long, String> currentContributorIdsToNamesMap;
private Map<Long, PlayerContentItem> unassignedPlayersIdToContentItemMap;
private ItemList<Theme> themeListBox;
// Location related stuff
private boolean mapsKeyExists;
private String mapsKey;
private TextBox latitudeTextBox;
private TextBox longitudeTextBox;
private TextArea locationDescriptionTextArea;
private RadioButton useDisplayedLocation;
private RadioButton useAlternateLocation;
private TextBox alternateTextBox;
private RadioButton useManualLatLong;
private Button geocodeButton;
private Label geocoderStatus;
private MapWidget map;
private Marker mapMarker;
// Source related stuff
private TextBox sourceDescriptionBox;
private SingleContentItemSelectionPanel sourceContentItemSelector;
private DockPanel pickerPanel;
private SuggestionAwareContentItemListBox linkedContentItemSelector;
private ListBox selectedLinkedContentItems;
private Label advisoryLabel;
private Label publishStateLabel;
private SaveControlsWidgetGroup topSaveControls = new SaveControlsWidgetGroup();
private SaveControlsWidgetGroup bottomSaveControls = new SaveControlsWidgetGroup();
private SimplePanel previewPanel;
/*** Event specific attributes ***/
private TextBox dateTrigger;
private PopupPanel datePopup;
private DatePicker startDatePicker;
private TextBox startTime;
private CheckBox hasSeparateEndDate;
private DatePicker endDatePicker;
private TextBox endTime;
private TextBox updateEditor;
private RichTextEditor summaryEditor;
/*** Player specific attributes ***/
private TextBox nameTextBox;
private TextBox aliasesTextBox;
private EnumDropdown<PlayerType> playerTypeSelector;
private SingleContentItemSelectionPanel photoSelector;
/*** Story Player specific attributes ***/
private FlowPanel parentPlayerDisplayPanel;
private Label changeParentLink;
private Label parentSelectionInstructions;
private PlayerSuggestAndAddPanel generalPlayerSuggestPanel;
private PlayerContentItem parentPlayer;
private DeckPanel playerAttributesPanel;
/*** Asset specific attributes ***/
private EnumDropdown<AssetType> assetTypeSelector;
private Label previewUrlLabel;
private TextBox previewUrlTextBox;
private Label captionLabel;
private TextArea captionTextArea;
private Label imageUrlLabel;
private TextBox imageUrlTextBox;
/*** Narrative specific attributes ***/
private TextBox headlineTextBox;
private EnumDropdown<NarrativeType> narrativeTypeSelector;
private DateBox narrativeDateBox;
private RichTextEditor narrativeSummaryTextArea;
/*** Background specific attributes ***/
private TextBox conceptNameTextBox;
private Map<ContentItemType, Integer> contentItemTypeToEditorPanelMap =
new HashMap<ContentItemType, Integer>();
public ContentItemManager() {
mapsKey = LivingStoryData.getMapsKey();
mapsKeyExists = mapsKey != null && !mapsKey.isEmpty();
HorizontalPanel container = new HorizontalPanel();
container.add(createControlsPanel());
container.add(createContentPanel());
// Event handlers
createLivingStorySelectionHandler();
createContentItemTypeSelectionHandler();
createContentItemSelectionHandler();
createSaveDeleteHandlers(topSaveControls);
createSaveDeleteHandlers(bottomSaveControls);
initWidget(container);
}
private Widget createControlsPanel() {
VerticalPanel controlsPanel = new VerticalPanel();
controlsPanel.add(createLivingStorySelector());
controlsPanel.add(createNewContentItemButton());
controlsPanel.add(createContentItemListBox());
return controlsPanel;
}
private Widget createLivingStorySelector() {
// Our livingStorySelector extends the superclass slightly, in that when the list
// of living stories is successfully loaded up, this triggers the list boxes
// to load the content items and retrieve the themes for the now-selected living story.
livingStorySelector = new CoordinatedLivingStorySelector(livingStoryService, true) {
@Override
public void onSuccessNextStep() {
super.onSuccessNextStep();
if (hasSelection()) {
LivingStoryData.setLivingStoryId(getSelectedLivingStoryId());
contentItemListBox.loadItemsForLivingStory(getSelectedLivingStoryId());
linkedContentItemSelector.loadItemsForLivingStory(getSelectedLivingStoryId());
themeListBox.refresh();
}
}
};
return livingStorySelector;
}
private Widget createNewContentItemButton() {
Button newContentItemButton = new Button("New Content Entity");
newContentItemButton.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
createOrChangeContentItem(new DefaultContentItem(
null, livingStorySelector.getSelectedLivingStoryId()), false, null);
}
});
return newContentItemButton;
}
/**
* Create a list box for displaying all the content items for the selected living story so that
* the user can select one to edit.
*/
private Widget createContentItemListBox() {
contentItemListBox = new ContentItemListBox(false);
contentItemListBox.setVisibleItemCount(15);
contentItemListBox.addFilterChangeHandler(new ChangeHandler() {
@Override
public void onChange(ChangeEvent event) {
contentPanel.showWidget(0);
}
});
return contentItemListBox;
}
private Widget createContentPanel() {
contentPanel = new DeckPanel();
Label chooseContentItemLabel = new Label("Choose something to edit, or create new content.");
chooseContentItemLabel.setStylePrimaryName("title");
DOM.setStyleAttribute(chooseContentItemLabel.getElement(), "marginTop", "5em");
contentPanel.add(chooseContentItemLabel);
VerticalPanel editorContentPanel = new VerticalPanel();
editorContentPanel.add(createSaveDeletePanel(topSaveControls));
editorContentPanel.add(createEditorPanel());
editorContentPanel.add(createSaveDeletePanel(bottomSaveControls));
contentPanel.add(editorContentPanel);
Label previewTitle = new Label("Preview");
previewTitle.setStylePrimaryName("header");
previewPanel = new SimplePanel();
previewPanel.setStylePrimaryName("previewPanel");
editorContentPanel.add(previewTitle);
editorContentPanel.add(previewPanel);
contentPanel.showWidget(0);
return contentPanel;
}
/**
* Create a panel for showing the text area to enter the HTML for a content piece; selectors
* to enter its priority and type; and the timestamp.
*/
private Widget createEditorPanel() {
contentItemIdLabel = new Label();
HorizontalPanel contentItemIdPanel = new HorizontalPanel();
contentItemIdPanel.setSpacing(2);
contentItemIdPanel.add(new Label("Id:"));
contentItemIdPanel.add(contentItemIdLabel);
contentEditor = new RichTextEditor();
timestamp = new Label();
HorizontalPanel timestampPanel = new HorizontalPanel();
timestampPanel.setSpacing(2);
timestampPanel.add(new Label("Publish time:"));
timestampPanel.add(timestamp);
publishStateLabel = new Label();
contentTitle = new Label("Content");
contentTitle.setStylePrimaryName("header");
VerticalPanel editorPanel = new VerticalPanel();
editorPanel.add(contentItemIdPanel);
editorPanel.add(createContentItemTypeSelectorPanel());
editorPanel.add(createSpecialAttributesPanel());
editorPanel.add(contentTitle);
editorPanel.add(contentEditor);
editorPanel.add(createImportanceSelectorPanel());
editorPanel.add(createContributorSelector());
editorPanel.add(createAdditionalPropertiesPanel());
editorPanel.add(createLinkedContentItemsPicker());
editorPanel.add(publishStateLabel);
editorPanel.add(timestampPanel);
return editorPanel;
}
/**
* Create a panel for the content priority/importance selector.
*/
private Widget createImportanceSelectorPanel() {
Label enterImportanceLabel = new Label("Select priority:");
importanceSelector = EnumDropdown.newInstance(Importance.class);
importanceSelector.selectConstant(Importance.MEDIUM);
HorizontalPanel importanceSelectorPanel = new HorizontalPanel();
importanceSelectorPanel.add(enterImportanceLabel);
importanceSelectorPanel.add(importanceSelector);
return importanceSelectorPanel;
}
/**
* Create a selector for the content type.
*/
private Widget createContentItemTypeSelectorPanel() {
Label enterTypeLabel = new Label("Select content type:");
contentItemTypeSelector = EnumDropdown.newInstance(ContentItemType.class);
HorizontalPanel typeSelectorPanel = new HorizontalPanel();
DOM.setStyleAttribute(typeSelectorPanel.getElement(), "paddingBottom", "10px");
typeSelectorPanel.add(enterTypeLabel);
typeSelectorPanel.add(contentItemTypeSelector);
return typeSelectorPanel;
}
private Widget createAdditionalPropertiesPanel() {
VerticalPanel additionalPanel = new VerticalPanel();
additionalPanel.setWidth("100%");
Label title = new Label("Additional Properties");
title.setStylePrimaryName("header");
additionalPanel.add(title);
additionalPanel.add(createThemeListBox());
if (mapsKeyExists) {
additionalPanel.add(createLocationPanel());
}
additionalPanel.add(createSourceInformationPanel());
return additionalPanel;
}
/**
* Create a multiselect list box for displaying all the themes that a content item is a part of.
*/
private Widget createThemeListBox() {
themeListBox = new ItemList<Theme>(true, false) {
@Override
public void loadItems() {
try {
Long livingStoryId = livingStorySelector.getSelectedLivingStoryId();
if (livingStoryId != null) {
livingStoryService.getThemesForLivingStory(
livingStoryId, getCallback(new ThemeListAdaptor()));
}
} catch (UnsupportedOperationException ignored) {
}
}
};
themeListBox.setVisibleItemCount(5);
DisclosurePanel themesPanel = new DisclosurePanel("Themes");
themesPanel.add(themeListBox);
return themesPanel;
}
private class ThemeListAdaptor extends ItemList.ListItemAdapter<Theme> {
@Override
public String getItemText(Theme theme) {
return theme.getName();
}
@Override
public String getItemValue(Theme theme) {
return Long.toString(theme.getId());
}
}
private Widget createLocationPanel() {
final VerticalPanel locationPanel = new VerticalPanel();
// show a map based on geocoded or manually-inputted lat-long combination
HorizontalPanel descriptionPanel = new HorizontalPanel();
descriptionPanel.add(new HTML("Location name (displayed to readers):"));
locationDescriptionTextArea = new TextArea();
locationDescriptionTextArea.setCharacterWidth(50);
locationDescriptionTextArea.setHeight("60px");
descriptionPanel.add(locationDescriptionTextArea);
Label geocodingOptions = new Label("Geocode based on:");
useDisplayedLocation = new RadioButton("geoGroup", "The displayed location name");
useDisplayedLocation.setValue(true);
useAlternateLocation = new RadioButton("geoGroup",
"An alternate location that geocodes better: ");
alternateTextBox = new TextBox();
alternateTextBox.setEnabled(false);
HorizontalPanel alternatePanel = new HorizontalPanel();
alternatePanel.add(useAlternateLocation);
alternatePanel.add(alternateTextBox);
useManualLatLong = new RadioButton("geoGroup",
"Manually entered latitude and longitude numbers (enter these below)");
HorizontalPanel latLongPanel = new HorizontalPanel();
latLongPanel.add(new HTML("Latitude: "));
latitudeTextBox = new TextBox();
latitudeTextBox.setEnabled(false);
latLongPanel.add(latitudeTextBox);
latLongPanel.add(new HTML(" Longitude: "));
longitudeTextBox = new TextBox();
longitudeTextBox.setEnabled(false);
latLongPanel.add(longitudeTextBox);
HorizontalPanel buttonPanel = new HorizontalPanel();
geocodeButton = new Button("Geocode location");
geocodeButton.setEnabled(false);
buttonPanel.add(geocodeButton);
geocoderStatus = new Label("");
buttonPanel.add(geocoderStatus);
AjaxLoaderOptions options = AjaxLoaderOptions.newInstance();
options.setOtherParms(mapsKey + "&sensor=false");
AjaxLoader.loadApi("maps", "2", new Runnable() {
@Override
public void run() {
map = new MapWidget();
map.setSize(MAP_WIDTH + "px", MAP_HEIGHT + "px");
map.addControl(new SmallMapControl());
map.setDoubleClickZoom(true);
map.setDraggable(true);
map.setScrollWheelZoomEnabled(true);
map.setZoomLevel(MAP_ZOOM);
map.setVisible(false);
locationPanel.add(map);
createLocationHandlers();
}
}, options);
locationPanel.add(descriptionPanel);
locationPanel.add(geocodingOptions);
locationPanel.add(useDisplayedLocation);
locationPanel.add(alternatePanel);
locationPanel.add(useManualLatLong);
locationPanel.add(latLongPanel);
locationPanel.add(buttonPanel);
locationPanel.add(
new Label("Tip: once the map is visible, right-click a point on the map to indicate that"
+ " this is the precise location you want"));
DisclosurePanel locationZippy = new DisclosurePanel("Location");
locationZippy.add(locationPanel);
return locationZippy;
}
private Widget createSourceInformationPanel() {
VerticalPanel panel = new VerticalPanel();
panel.add(new Label("You can enter either one or both of these fields."));
sourceContentItemSelector = new SingleContentItemSelectionPanel();
sourceDescriptionBox = new TextBox();
sourceDescriptionBox.setVisibleLength(LONG_TEXTBOX_VISIBLE_LENGTH);
Grid grid = new Grid(2, 2);
grid.setWidget(0, 0, new Label("Content item that contains source information:"));
grid.setWidget(0, 1, sourceContentItemSelector);
grid.setWidget(1, 0, new Label("Description of source material:"));
grid.setWidget(1, 1, sourceDescriptionBox);
panel.add(grid);
DisclosurePanel sourcePanel = new DisclosurePanel("Source Information");
sourcePanel.add(panel);
return sourcePanel;
}
private Widget createContributorSelector() {
currentContributorIdsToNamesMap = new HashMap<Long, String>();
Label title = new Label("Contributors");
title.setStylePrimaryName("header");
contributorListHtml = new HTML();
clearContributorsControl = new Label("Clear current contributors");
clearContributorsControl.setStylePrimaryName("primaryLink");
clearContributorsControl.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
currentContributorIdsToNamesMap.clear();
formatCurrentContributorList();
}
});
Label instructions = new Label("Type a contributor name into the box below to add an existing"
+ " contributor to the story, or to create and add a new contributor on-the-fly:");
contributorSuggestPanel = new PlayerSuggestAndAddPanel(contentRpcService, true,
new AsyncCallback<BaseContentItem>() {
@Override
public void onFailure(Throwable caught) {}
@Override
public void onSuccess(BaseContentItem result) {
PlayerContentItem contributor = (PlayerContentItem)result;
addUnassignedPlayer(contributor);
currentContributorIdsToNamesMap.put(result.getId(), contributor.getName());
formatCurrentContributorList();
}
});
if (unassignedPlayersIdToContentItemMap == null) {
// Asynchronously query for the contributor names:
contentRpcService.getUnassignedPlayers(new AsyncCallback<List<PlayerContentItem>>() {
@Override
public void onFailure(Throwable caught) {
contributorListHtml.setText("Could not retrieve list of available contributors");
}
@Override
public void onSuccess(List<PlayerContentItem> results) {
unassignedPlayersIdToContentItemMap = new HashMap<Long, PlayerContentItem>();
populateUnassignedPlayersMap(results);
populatePlayerSuggestPanel(contributorSuggestPanel);
}
});
} else {
populatePlayerSuggestPanel(contributorSuggestPanel);
}
FlowPanel contributorsPanel = new FlowPanel();
contributorsPanel.add(title);
contributorsPanel.add(contributorListHtml);
contributorsPanel.add(clearContributorsControl);
contributorsPanel.add(instructions);
contributorsPanel.add(contributorSuggestPanel);
return contributorsPanel;
}
private void formatCurrentContributorList() {
boolean hasContributors = !currentContributorIdsToNamesMap.isEmpty();
if (!hasContributors) {
contributorListHtml.setHTML("<em>No contributors yet added</em>");
} else {
StringBuilder contributorBuilder = new StringBuilder("<ul>");
for (String contributorName : currentContributorIdsToNamesMap.values()) {
contributorBuilder.append("<li>").append(contributorName).append("</li>");
}
contributorBuilder.append("</ul>");
contributorListHtml.setHTML(contributorBuilder.toString());
}
clearContributorsControl.setVisible(hasContributors);
}
private Widget createLinkedContentItemsPicker() {
pickerPanel = new DockPanel();
pickerPanel.setVerticalAlignment(HorizontalPanel.ALIGN_MIDDLE);
Label title = new Label("Linked Items");
title.setStylePrimaryName("header");
Label instructions = new Label("Select items from the left list and click on the arrow in the" +
" middle to move them to the right list. The items in the right list will be linked to" +
" the current item.");
instructions.setWidth("500px");
pickerPanel.add(title, DockPanel.NORTH);
pickerPanel.add(instructions, DockPanel.NORTH);
linkedContentItemSelector = new SuggestionAwareContentItemListBox(true);
linkedContentItemSelector.setVisibleItemCount(10);
advisoryLabel = new Label("The system has identified one or more players"
+ " that we suggest adding to the list of linked entities. These suggestions are now shown"
+ " in the area above. You may change the filter settings to revisit other linkable"
+ " entities, and may later return to these suggestions, so long as you continue to"
+ " edit only this content entity without switching to another.");
advisoryLabel.setStylePrimaryName("serverResponseLabelSuccess");
advisoryLabel.setWidth("475px");
hideSuggestions();
pickerPanel.add(advisoryLabel, DockPanel.SOUTH);
FlowPanel linkedPanel = new FlowPanel();
linkedPanel.add(linkedContentItemSelector);
pickerPanel.add(linkedPanel, DockPanel.WEST);
Button addButton = new Button("»");
addButton.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
List<String> selectedItems = linkedContentItemSelector.getSelectedItems();
List<String> selectedValues = linkedContentItemSelector.getSelectedValues();
for (int i = 0; i < selectedItems.size(); i++) {
selectedLinkedContentItems.addItem(selectedItems.get(i), selectedValues.get(i));
}
}
});
Button removeButton = new Button("«");
removeButton.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
for (int i = selectedLinkedContentItems.getItemCount() - 1; i >= 0; i--) {
if (selectedLinkedContentItems.isItemSelected(i)) {
selectedLinkedContentItems.removeItem(i);
}
}
}
});
VerticalPanel buttonPanel = new VerticalPanel();
buttonPanel.add(addButton);
buttonPanel.add(removeButton);
pickerPanel.add(buttonPanel, DockPanel.CENTER);
selectedLinkedContentItems = new ListBox(true);
selectedLinkedContentItems.setVisibleItemCount(10);
VerticalPanel selectedPanel = new VerticalPanel();
selectedPanel.add(new Label("Selected for linking:"));
selectedPanel.add(selectedLinkedContentItems);
pickerPanel.add(selectedPanel, DockPanel.EAST);
return pickerPanel;
}
private Widget createSpecialAttributesPanel() {
specialAttributesPanel = new DeckPanel();
specialAttributesPanel.add(createEventAttributesPanel());
contentItemTypeToEditorPanelMap.put(ContentItemType.EVENT, 0);
specialAttributesPanel.add(createAssetAttributesPanel());
contentItemTypeToEditorPanelMap.put(ContentItemType.ASSET, 1);
playerAttributesPanel = new DeckPanel();
playerAttributesPanel.add(createStoryPlayerAttributesPanel());
playerAttributesPanel.add(createPlayerAttributesPanel());
playerAttributesPanel.showWidget(0);
specialAttributesPanel.add(playerAttributesPanel);
contentItemTypeToEditorPanelMap.put(ContentItemType.PLAYER, 2);
specialAttributesPanel.add(createNarrativeAttributesPanel());
contentItemTypeToEditorPanelMap.put(ContentItemType.NARRATIVE, 3);
specialAttributesPanel.add(createBackgroundAttributesPanel());
contentItemTypeToEditorPanelMap.put(ContentItemType.BACKGROUND, 4);
specialAttributesPanel.showWidget(0);
return specialAttributesPanel;
}
private Widget createEventAttributesPanel() {
dateTrigger = new TextBox();
dateTrigger.setVisibleLength(LONG_TEXTBOX_VISIBLE_LENGTH);
dateTrigger.setReadOnly(true);
dateTrigger.setStylePrimaryName("dateTriggerBox");
createDatePickerPanel();
updateEditor = new TextBox();
updateEditor.setVisibleLength(LONG_TEXTBOX_VISIBLE_LENGTH);
summaryEditor = new RichTextEditor();
Label updateTitle = new Label("Update");
updateTitle.setStylePrimaryName("header");
Label summaryTitle = new Label("Summary");
summaryTitle.setStylePrimaryName("header");
VerticalPanel eventPanel = new VerticalPanel();
eventPanel.add(new Label("Event Date:"));
eventPanel.add(dateTrigger);
eventPanel.add(updateTitle);
eventPanel.add(updateEditor);
eventPanel.add(summaryTitle);
eventPanel.add(summaryEditor);
dateTrigger.addFocusHandler(new FocusHandler() {
@Override
public void onFocus(FocusEvent event) {
datePopup.showRelativeTo(dateTrigger);
}
});
dateTrigger.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
datePopup.showRelativeTo(dateTrigger);
}
});
return eventPanel;
}
/** creates a panel of datePickers in a popup, including setting up appropriate event handling.*/
private Widget createDatePickerPanel() {
datePopup = new PopupPanel(false /* doesn't close if you click away */, true /* modal */);
// TODO: Make it possible for the user to cancel, either by clicking away from
// the popup panel or by hitting an explicit cancel button.
startDatePicker = new DatePicker();
startTime = new TextBox();
hasSeparateEndDate = new CheckBox("Event has a separate end date & time");
endDatePicker = new DatePicker();
endTime = new TextBox();
endTime.setEnabled(false);
Grid table = new Grid(5, 2);
table.setWidget(1, 0, new Label("Start date:"));
table.setWidget(2, 0, startDatePicker);
table.setWidget(3, 0, new Label("Start time:"));
table.setWidget(4, 0, startTime);
table.setWidget(0, 1, hasSeparateEndDate);
table.setWidget(1, 1, new Label("End date:"));
table.setWidget(2, 1, endDatePicker);
table.setWidget(3, 1, new Label("End time:"));
table.setWidget(4, 1, endTime);
Button okButton = new Button("OK");
final InlineLabel problemLabel = new InlineLabel("");
problemLabel.setStylePrimaryName("serverResponseLabelError");
FlowPanel panel = new FlowPanel();
panel.add(table);
panel.add(new Label("Event time may be blank, or should be entered as, e.g., 3:00 PM."));
panel.add(okButton);
panel.add(problemLabel);
hasSeparateEndDate.addValueChangeHandler(new ValueChangeHandler<Boolean> () {
@Override
public void onValueChange(ValueChangeEvent<Boolean> event) {
boolean isChecked = event.getValue().booleanValue();
// endDatePicker.setEnabled(isChecked); -- if only the API supported it. :-(
endTime.setEnabled(isChecked);
}
});
okButton.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
Date endDateTime = getEndDateTime();
if (endDateTime != null && endDateTime.before(getStartDateTime())) {
problemLabel.setText("End date/time cannot be before start date/time");
} else {
problemLabel.setText("");
datePopup.hide();
setDateTriggerText();
}
}
});
datePopup.setWidget(panel);
return datePopup;
}
private void setDateTriggerText() {
String dateString = DateUtil.formatDateTime(getStartDateTime());
Date endDateTime = getEndDateTime();
if (endDateTime != null) {
dateString = msgs.dateRange(dateString, DateUtil.formatDateTime(endDateTime));
}
dateTrigger.setText(dateString);
}
private Date getStartDateTime() {
return getDateTimeImpl(startDatePicker, startTime);
}
private Date getEndDateTime() {
return hasSeparateEndDate.getValue() ? getDateTimeImpl(endDatePicker, endTime) : null;
}
private Date getDateTimeImpl(DatePicker picker, TextBox textBox) {
Date ret = picker.getValue();
String text = textBox.getText();
if (text.isEmpty()) {
text = DEFAULT_TIME_STRING;
}
DateUtil.parseTime(text, ret);
return ret;
}
private Widget createPlayerAttributesPanel() {
nameTextBox = new TextBox();
aliasesTextBox = new TextBox();
playerTypeSelector = EnumDropdown.newInstance(PlayerType.class);
photoSelector = new SingleContentItemSelectionPanel();
Grid generalPlayerAttributesPanel = new Grid(4, 2);
generalPlayerAttributesPanel.setWidget(0, 0, new Label("Player name:"));
generalPlayerAttributesPanel.setWidget(0, 1, nameTextBox);
generalPlayerAttributesPanel.setWidget(1, 0, new Label("Aliases:"));
generalPlayerAttributesPanel.setWidget(1, 1, aliasesTextBox);
generalPlayerAttributesPanel.setWidget(2, 0, new Label("Player type:"));
generalPlayerAttributesPanel.setWidget(2, 1, playerTypeSelector);
generalPlayerAttributesPanel.setWidget(3, 0, new Label("Photo:"));
generalPlayerAttributesPanel.setWidget(3, 1, photoSelector);
return generalPlayerAttributesPanel;
}
private Widget createStoryPlayerAttributesPanel() {
Label title = new Label("Parent player entity");
title.setStylePrimaryName("header");
parentPlayerDisplayPanel = new FlowPanel();
parentPlayerDisplayPanel.setWidth("450px");
changeParentLink = new Label("Change parent");
changeParentLink.setStylePrimaryName("primaryLink");
HorizontalPanel parentPlayerDisplayAndChangeLinkPanel = new HorizontalPanel();
parentPlayerDisplayAndChangeLinkPanel.add(parentPlayerDisplayPanel);
parentPlayerDisplayAndChangeLinkPanel.add(changeParentLink);
parentSelectionInstructions = new Label("Type a player name into the box below. Select from the"
+ " list to add an existing player to the story. To add a new player to the story that"
+ " doesn't exist yet, type their name into the box and click on the button.");
parentSelectionInstructions.setWidth("450px");
generalPlayerSuggestPanel = new PlayerSuggestAndAddPanel(contentRpcService, false,
new AsyncCallback<BaseContentItem>() {
@Override
public void onFailure(Throwable caught) {
parentPlayer = null;
formatParentPlayerDisplay();
}
@Override
public void onSuccess(BaseContentItem result) {
parentPlayer = (PlayerContentItem)result;
addUnassignedPlayer(parentPlayer);
formatParentPlayerDisplay();
}
});
changeParentLink.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
parentPlayer = null;
formatParentPlayerDisplay();
}
});
if (unassignedPlayersIdToContentItemMap == null) {
// Asynchronously query for the contributor names:
contentRpcService.getUnassignedPlayers(new AsyncCallback<List<PlayerContentItem>>() {
@Override
public void onFailure(Throwable caught) {}
@Override
public void onSuccess(List<PlayerContentItem> results) {
unassignedPlayersIdToContentItemMap = new HashMap<Long, PlayerContentItem>();
populateUnassignedPlayersMap(results);
populatePlayerSuggestPanel(generalPlayerSuggestPanel);
}
});
} else {
populatePlayerSuggestPanel(generalPlayerSuggestPanel);
}
Label contentInstructions = new Label("Please enter the role of the selected player in this " +
"particular story in the 'Content' box below.");
DOM.setStyleAttribute(contentInstructions.getElement(), "marginTop", "3em");
formatParentPlayerDisplay();
FlowPanel storyPlayerAttributesPanel = new FlowPanel();
storyPlayerAttributesPanel.add(title);
storyPlayerAttributesPanel.add(parentPlayerDisplayAndChangeLinkPanel);
storyPlayerAttributesPanel.add(parentSelectionInstructions);
storyPlayerAttributesPanel.add(generalPlayerSuggestPanel);
storyPlayerAttributesPanel.add(contentInstructions);
return storyPlayerAttributesPanel;
}
private void formatParentPlayerDisplay() {
boolean isParentNull = parentPlayer == null;
parentPlayerDisplayPanel.clear();
if (!isParentNull) {
parentPlayerDisplayPanel.add(new BasePlayerPreview(parentPlayer));
}
parentPlayerDisplayPanel.setVisible(!isParentNull);
changeParentLink.setVisible(!isParentNull);
parentSelectionInstructions.setVisible(isParentNull);
generalPlayerSuggestPanel.setVisible(isParentNull);
}
private Widget createAssetAttributesPanel() {
assetTypeSelector = EnumDropdown.newInstance(AssetType.class);
previewUrlLabel = new Label("Asset preview url:");
previewUrlTextBox = new TextBox();
imageUrlLabel = new Label("Image url (images only):");
imageUrlTextBox = new TextBox();
captionLabel = new Label("Asset caption:");
captionTextArea = new TextArea();
assetTypeSelector.addChangeHandler(new ChangeHandler() {
@Override
public void onChange(ChangeEvent event) {
setAssetControlVisibility();
}
});
assetTypeSelector.selectConstant(AssetType.IMAGE);
Grid assetPanel = new Grid(4, 2);
assetPanel.setWidget(0, 0, new Label("Asset type:"));
assetPanel.setWidget(0, 1, assetTypeSelector);
assetPanel.setWidget(1, 0, previewUrlLabel);
assetPanel.setWidget(1, 1, previewUrlTextBox);
assetPanel.setWidget(2, 0, imageUrlLabel);
assetPanel.setWidget(2, 1, imageUrlTextBox);
assetPanel.setWidget(3, 0, captionLabel);
assetPanel.setWidget(3, 1, captionTextArea);
return assetPanel;
}
/**
* Hides and shows labels, textareas, and other controls as appropriate for the asset
* type a user is editing. Also may hide the main content rich text editor, if appropriate.
*/
private void setAssetControlVisibility() {
boolean visibleContent = true;
boolean visiblePreviewUrl = true;
boolean visibleImageUrl = false;
boolean visibleCaption = true;
if (contentItemTypeSelector.getSelectedConstant() == ContentItemType.ASSET) {
switch (assetTypeSelector.getSelectedConstant()) {
case AUDIO:
case DOCUMENT:
visiblePreviewUrl = false;
break;
case IMAGE:
visibleImageUrl = true;
visibleContent = false;
break;
case LINK:
visiblePreviewUrl = false;
visibleCaption = false;
break;
default:
// nothing
break;
}
}
contentTitle.setVisible(visibleContent);
contentEditor.setVisible(visibleContent);
previewUrlLabel.setVisible(visiblePreviewUrl);
previewUrlTextBox.setVisible(visiblePreviewUrl);
imageUrlLabel.setVisible(visibleImageUrl);
imageUrlTextBox.setVisible(visibleImageUrl);
captionLabel.setVisible(visibleCaption);
captionTextArea.setVisible(visibleCaption);
}
private Widget createNarrativeAttributesPanel() {
headlineTextBox = new TextBox();
headlineTextBox.setVisibleLength(LONG_TEXTBOX_VISIBLE_LENGTH);
narrativeTypeSelector = EnumDropdown.newInstance(NarrativeType.class);
narrativeDateBox = new DateBox();
narrativeSummaryTextArea = new RichTextEditor();
narrativeSummaryTextArea.setSize("400px", "100px");
Grid narrativePanel = new Grid(4, 2);
narrativePanel.setWidget(0, 0, new Label("Headline:"));
narrativePanel.setWidget(0, 1, headlineTextBox);
narrativePanel.setWidget(1, 0, new Label("Narrative type:"));
narrativePanel.setWidget(1, 1, narrativeTypeSelector);
narrativePanel.setWidget(2, 0, new Label("Date (optional):"));
narrativePanel.setWidget(2, 1, narrativeDateBox);
narrativePanel.setWidget(3, 0, new Label("Summary (optional):"));
narrativePanel.setWidget(3, 1, narrativeSummaryTextArea);
return narrativePanel;
}
private Widget createBackgroundAttributesPanel() {
conceptNameTextBox = new TextBox();
Grid backgroundPanel = new Grid(1, 2);
backgroundPanel.setWidget(0, 0, new Label("Concept name:"));
backgroundPanel.setWidget(0, 1, conceptNameTextBox);
return backgroundPanel;
}
/**
* Create buttons to save and delete content pieces. And a label for error messages if the
* updates don't work.
*/
private Widget createSaveDeletePanel(SaveControlsWidgetGroup widgets) {
widgets.saveDraftButton = new Button("Save as draft");
widgets.publishButton = new Button("Publish");
widgets.republishButton = new Button("Republish, without updating time");
widgets.republishButton.setEnabled(false);
widgets.deleteButton = new Button("Delete");
widgets.deleteButton.setEnabled(false);
widgets.statusLabel = new Label();
HorizontalPanel buttonPanel = new HorizontalPanel();
buttonPanel.add(widgets.saveDraftButton);
buttonPanel.add(widgets.publishButton);
buttonPanel.add(widgets.republishButton);
buttonPanel.add(widgets.deleteButton);
buttonPanel.add(widgets.statusLabel);
return buttonPanel;
}
/**
* Create a handler to handle selection in the living story list box.
*/
private void createLivingStorySelectionHandler() {
livingStorySelectionHandler = new ChangeHandler() {
@Override
public void onChange(ChangeEvent event) {
contentPanel.showWidget(0);
if (livingStorySelector.hasSelection()) {
contentItemListBox.loadItemsForLivingStory(
livingStorySelector.getSelectedLivingStoryId());
linkedContentItemSelector.loadItemsForLivingStory(
livingStorySelector.getSelectedLivingStoryId());
themeListBox.refresh();
LivingStoryData.setLivingStoryId(livingStorySelector.getSelectedLivingStoryId());
}
}
};
livingStorySelector.addChangeHandler(livingStorySelectionHandler);
}
/**
* Create a handler to handle selection in the 'Type' list box. If the "Event" option is
* selected, this requires help from remote services to populate the document list box.
*/
private void createContentItemTypeSelectionHandler() {
ChangeHandler typeSelectionHandler = new ChangeHandler() {
@Override
public void onChange(ChangeEvent event) {
ContentItemType type = contentItemTypeSelector.getSelectedConstant();
showSpecialAttributesPanel(type);
setAssetControlVisibility();
}
};
contentItemTypeSelector.addChangeHandler(typeSelectionHandler);
}
/**
* Create a handler to handle selection of a content piece from the content list box.
*/
private void createContentItemSelectionHandler() {
ChangeHandler contentSelectionHandler = new ChangeHandler() {
@Override
public void onChange(ChangeEvent event) {
contentPanel.showWidget(1);
topSaveControls.statusLabel.setText("");
bottomSaveControls.statusLabel.setText("");
BaseContentItem selectedContentItem = contentItemListBox.getSelectedContentItem();
contentItemIdLabel.setText(String.valueOf(selectedContentItem.getId()));
contentEditor.setContent(selectedContentItem.getContent());
timestamp.setText(DateUtil.formatDateTime(selectedContentItem.getTimestamp()));
importanceSelector.selectConstant(selectedContentItem.getImportance());
contentItemTypeSelector.selectConstant(selectedContentItem.getContentItemType());
setAssetControlVisibility();
showSpecialAttributesPanel(selectedContentItem.getContentItemType());
// First clear or set these fields to default values.
// Otherwise, if the user changes the content item type, they may
// see data from some other content item in the form fields.
startDatePicker.setValue(DateUtil.getDateMidnight());
startTime.setText("");
endDatePicker.setValue(DateUtil.getDateMidnight());
endTime.setText("");
setDateTriggerText();
updateEditor.setText("");
summaryEditor.setContent("");
nameTextBox.setText("");
aliasesTextBox.setText("");
playerTypeSelector.selectConstant(PlayerType.PERSON);
photoSelector.setSelection(null);
assetTypeSelector.selectConstant(AssetType.IMAGE);
captionTextArea.setText("");
previewUrlTextBox.setText("");
imageUrlTextBox.setText("");
headlineTextBox.setText("");
narrativeTypeSelector.selectConstant(NarrativeType.FEATURE);
narrativeDateBox.setValue(null);
narrativeSummaryTextArea.setContent("");
parentPlayer = null;
formatParentPlayerDisplay();
switch (selectedContentItem.getContentItemType()) {
case EVENT:
EventContentItem eventContentItem = (EventContentItem) selectedContentItem;
Date startDate = eventContentItem.getEventStartDate();
Date endDate = eventContentItem.getEventEndDate();
if (startDate == null) {
startDate = new Date();
}
startDatePicker.setValue(startDate);
startDatePicker.setCurrentMonth(startDatePicker.getValue());
startTime.setValue(DateUtil.formatTime(startDate));
hasSeparateEndDate.setValue(endDate != null, true);
endDatePicker.setValue(endDate == null ? startDatePicker.getValue() : endDate);
endDatePicker.setCurrentMonth(endDatePicker.getValue());
endTime.setText(endDate == null ? startTime.getText() : DateUtil.formatTime(endDate));
setDateTriggerText();
updateEditor.setText(eventContentItem.getEventUpdate());
summaryEditor.setContent(eventContentItem.getEventSummary());
break;
case PLAYER:
if (selectedContentItem.getLivingStoryId() == null) {
PlayerContentItem playerContentItem = (PlayerContentItem) selectedContentItem;
nameTextBox.setText(playerContentItem.getName());
aliasesTextBox.setText(GlobalUtil.join(",", playerContentItem.getAliases()));
playerTypeSelector.selectConstant(playerContentItem.getPlayerType());
photoSelector.setSelection(playerContentItem.getPhotoContentItem());
} else {
parentPlayer =
((StoryPlayerContentItem) selectedContentItem).getParentPlayerContentItem();
formatParentPlayerDisplay();
}
break;
case ASSET:
AssetContentItem assetContentItem = (AssetContentItem) selectedContentItem;
AssetType assetType = assetContentItem.getAssetType();
assetTypeSelector.selectConstant(assetType);
setAssetControlVisibility();
captionTextArea.setText(assetContentItem.getCaption());
previewUrlTextBox.setText(assetContentItem.getPreviewUrl());
if (assetType == AssetType.IMAGE) {
contentEditor.setContent("");
imageUrlTextBox.setText(selectedContentItem.getContent());
}
break;
case NARRATIVE:
NarrativeContentItem narrativeContentItem = (NarrativeContentItem) selectedContentItem;
headlineTextBox.setText(narrativeContentItem.getHeadline());
narrativeTypeSelector.selectConstant(narrativeContentItem.getNarrativeType());
narrativeDateBox.setValue(narrativeContentItem.getNarrativeDate());
narrativeSummaryTextArea.setContent(narrativeContentItem.getNarrativeSummary());
break;
case BACKGROUND:
BackgroundContentItem backgroundContentItem =
(BackgroundContentItem) selectedContentItem;
if (backgroundContentItem.isConcept()) {
conceptNameTextBox.setText(backgroundContentItem.getConceptName());
}
break;
}
int themeCount = themeListBox.getItemCount();
Set<Long> themesInContentItem = selectedContentItem.getThemeIds();
for (int i = 0; i < themeCount; i++) {
themeListBox.setItemSelected(i, themesInContentItem.contains(
Long.parseLong(themeListBox.getValue(i))));
}
currentContributorIdsToNamesMap.clear();
for (Long contributorId : selectedContentItem.getContributorIds()) {
currentContributorIdsToNamesMap.put(contributorId,
unassignedPlayersIdToContentItemMap.get(contributorId).getName());
}
formatCurrentContributorList();
contributorSuggestPanel.clear();
if (mapsKeyExists) {
Location location = selectedContentItem.getLocation();
if (location != null) {
Double latitude = location.getLatitude();
latitudeTextBox.setText(latitude == null ? "" : latitude.toString());
Double longitude = location.getLongitude();
longitudeTextBox.setText(longitude == null ? "" : longitude.toString());
if (latitude != null && longitude != null) {
recenterMap();
}
String description = location.getDescription();
locationDescriptionTextArea.setText(description == null ? "" : description);
}
// Ensure that the state of the location controls are accurate for the content item data.
adjustLocationControls();
controlGeocodeButton();
}
// Set the source information related fields
String sourceDescription = selectedContentItem.getSourceDescription();
sourceDescriptionBox.setText(sourceDescription == null ? "" : sourceDescription);
sourceContentItemSelector.setSelection(selectedContentItem.getSourceContentItem());
updateSelectedLinkedContentItems(selectedContentItem);
updateDisplayedPublishStatus(selectedContentItem);
topSaveControls.deleteButton.setEnabled(true);
bottomSaveControls.deleteButton.setEnabled(true);
hideSuggestions();
updatePreview();
}
};
contentItemListBox.addSelectionChangeHandler(contentSelectionHandler);
}
private void updateDisplayedPublishStatus(BaseContentItem contentItem) {
PublishState publishState = contentItem.getPublishState();
boolean isPublished = publishState == PublishState.PUBLISHED;
publishStateLabel.setText("Publish Status : " + publishState);
topSaveControls.republishButton.setEnabled(isPublished);
bottomSaveControls.republishButton.setEnabled(isPublished);
}
private void updateSelectedLinkedContentItems(BaseContentItem contentItem) {
selectedLinkedContentItems.clear();
// Initially set the linked content items list to just the ids. Then issue
// a request to get the actual content.
Set<Long> linkedContentItemIds = contentItem.getLinkedContentItemIds();
for (Long contentItemId : linkedContentItemIds) {
selectedLinkedContentItems.addItem(String.valueOf(contentItemId));
}
if (!linkedContentItemIds.isEmpty()) {
contentRpcService.getContentItems(linkedContentItemIds,
new AsyncCallback<List<BaseContentItem>>() {
@Override
public void onFailure(Throwable caught) {}
@Override
public void onSuccess(List<BaseContentItem> result) {
selectedLinkedContentItems.clear();
for (BaseContentItem contentItem : result) {
String content = contentItem.getDisplayString();
if (content.length() > Constants.CONTENT_SNIPPET_LENGTH) {
content = content.substring(0, Constants.CONTENT_SNIPPET_LENGTH).concat("...");
}
selectedLinkedContentItems.addItem(content, String.valueOf(contentItem.getId()));
}
}
});
}
}
private void createSaveDeleteHandlers(final SaveControlsWidgetGroup widgets) {
widgets.saveDraftButton.addClickHandler(new SaveHandler(false, false, widgets.statusLabel));
widgets.publishButton.addClickHandler(new SaveHandler(true, false, widgets.statusLabel));
widgets.republishButton.addClickHandler(new SaveHandler(true, true, widgets.statusLabel));
ClickHandler deleteHandler = new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
deleteContentItem(contentItemListBox.getSelectedContentItem(), widgets.statusLabel);
}
};
widgets.deleteButton.addClickHandler(deleteHandler);
}
/**
* Handler for the 'Save as draft' and 'Publish' buttons.
*/
private class SaveHandler implements ClickHandler {
private boolean publish;
private boolean republish;
private Label statusLabel;
BaseContentItem selectedContentItem;
public SaveHandler(boolean publish, boolean republish, Label statusLabel) {
this.publish = publish;
this.republish = republish;
this.statusLabel = statusLabel;
}
@Override
public void onClick(ClickEvent event) {
doClickWork((Widget) event.getSource());
}
public void doClickWork(Widget showPromptRelativeTo) {
selectedContentItem = contentItemListBox.getSelectedContentItem();
long contentItemId = selectedContentItem.getId();
Date creationDate = republish ? selectedContentItem.getTimestamp() : new Date();
ContentItemType contentItemType = contentItemTypeSelector.getSelectedConstant();
AssetType assetType =
contentItemType == ContentItemType.ASSET ? assetTypeSelector.getSelectedConstant() : null;
String content =
contentEditor.isVisible() ? contentEditor.getContent() : imageUrlTextBox.getText();
boolean isImage = contentItemType == ContentItemType.ASSET
&& assetTypeSelector.getSelectedConstant() == AssetType.IMAGE;
if (content.isEmpty() && contentItemType != ContentItemType.EVENT && !isImage) {
showInputError("Content cannot be empty.");
return;
}
if (showPromptRelativeTo != null
&& (assetType == AssetType.AUDIO || assetType == AssetType.VIDEO
|| assetType == AssetType.INTERACTIVE)) {
ObjectElementProofreader proofreader = new ObjectElementProofreader();
String sanifiedContent = proofreader.proofread(content);
if (sanifiedContent != null) {
new SaveHandlerPrompt(sanifiedContent).showRelativeTo(showPromptRelativeTo);
return;
}
}
Importance importance = importanceSelector.getSelectedConstant();
Long livingStoryId = livingStorySelector.getSelectedLivingStoryId();
Set<Long> themeIds = new HashSet<Long>();
for (String id : themeListBox.getSelectedItemValues()) {
themeIds.add(Long.valueOf(id));
}
if (publish && contentItemType == ContentItemType.EVENT
&& currentContributorIdsToNamesMap.isEmpty()) {
showInputError("Must select at least one contributor for publishing.");
return;
}
Location location = new Location(null, null, "");
// Initialize the location if it was entered
if (mapsKeyExists) {
Double latitude = null;
Double longitude = null;
String latitudeString = latitudeTextBox.getText();
if (!latitudeString.isEmpty()) {
String longitudeString = longitudeTextBox.getText();
if (longitudeString.isEmpty()) {
showInputError("Both latitude and longitude have to be entered.");
return;
}
try {
latitude = Double.valueOf(latitudeString);
longitude = Double.valueOf(longitudeString);
if (latitude > 90.0 || latitude < -90.0) {
showInputError("Latitude should be between -90 and +90");
return;
}
if (longitude > 180 || longitude < -180) {
showInputError("Longitude should be between -180 and +180");
return;
}
} catch (NumberFormatException e) {
showInputError("Latitude and Longitude should be decimal numbers.");
return;
}
}
location = new Location(latitude, longitude, locationDescriptionTextArea.getText());
}
Set<Long> currentContributorIds = new HashSet<Long>(currentContributorIdsToNamesMap.keySet());
BaseContentItem contentItem;
switch (contentItemType) {
case EVENT:
Date startDate = getStartDateTime();
Date endDate = getEndDateTime();
if (startDate.equals(endDate)) {
// actually, a null end-date is what we want
endDate = null;
}
String update = updateEditor.getText().trim();
if (update.isEmpty()) {
showInputError("Event update cannot be empty.");
return;
}
contentItem = new EventContentItem(contentItemId, creationDate, currentContributorIds,
importance, livingStoryId, startDate, endDate, update, summaryEditor.getContent(),
content);
break;
case PLAYER:
if (livingStoryId == null) {
String nameString = nameTextBox.getText();
if (nameString.isEmpty()) {
showInputError("Player name cannot be empty.");
return;
}
List<String> aliasList = new ArrayList<String>();
for (String alias : aliasesTextBox.getText().split(",")) {
String trimmed = alias.trim();
if (!trimmed.isEmpty()) {
aliasList.add(trimmed);
}
}
BaseContentItem photoContentItem = photoSelector.getSelection();
if (photoContentItem != null
&& (photoContentItem.getContentItemType() != ContentItemType.ASSET
|| ((AssetContentItem) photoContentItem).getAssetType() != AssetType.IMAGE)) {
showInputError("Player photo must be an image");
return;
}
contentItem = new PlayerContentItem(contentItemId, creationDate, currentContributorIds,
content, importance, nameString, aliasList,
playerTypeSelector.getSelectedConstant(), (AssetContentItem) photoContentItem);
} else {
if (parentPlayer == null) {
showInputError("Parent player must be chosen or created");
return;
}
contentItem = new StoryPlayerContentItem(contentItemId, creationDate,
currentContributorIds, content, importance, livingStoryId, parentPlayer);
}
break;
case QUOTE:
contentItem = new QuoteContentItem(contentItemId, creationDate, currentContributorIds,
content, importance, livingStoryId);
break;
case BACKGROUND:
contentItem = new BackgroundContentItem(contentItemId, creationDate,
currentContributorIds, content, importance, livingStoryId,
conceptNameTextBox.getText());
break;
case DATA:
contentItem = new DataContentItem(contentItemId, creationDate, currentContributorIds,
content, importance, livingStoryId);
break;
case ASSET:
contentItem = new AssetContentItem(contentItemId, creationDate, currentContributorIds,
content, importance, livingStoryId, assetType, captionTextArea.getText(),
previewUrlTextBox.getText());
break;
case NARRATIVE:
// There are 2 types possible for a content item that is now being saved as a narrative.
// Either it has just been created and so selectedContentItem is a "DefaultContentItem"
// with a type of 'Event'. In this case, the standalone value should be set to true
// because this is the first time the item is being saved as a narrative, and hasn't
// been linked from anything yet. The second case is when an existing narrative
// is being resaved. In this case, we want to preserve the old value of the
// 'isStandalone' field.
contentItem = new NarrativeContentItem(contentItemId, creationDate, currentContributorIds,
content, importance, livingStoryId, headlineTextBox.getText().trim(),
narrativeTypeSelector.getSelectedConstant(),
selectedContentItem.getContentItemType() == ContentItemType.NARRATIVE ?
((NarrativeContentItem)selectedContentItem).isStandalone() : true,
narrativeDateBox.getValue(), narrativeSummaryTextArea.getContent());
break;
case REACTION:
contentItem = new ReactionContentItem(contentItemId, creationDate, currentContributorIds,
content, importance, livingStoryId);
break;
default:
throw new IllegalStateException("Unknown Content Item Type");
}
contentItem.setPublishState(publish ? PublishState.PUBLISHED : PublishState.DRAFT);
contentItem.setThemeIds(themeIds);
contentItem.setLocation(location);
contentItem.setSourceDescription(sourceDescriptionBox.getText());
contentItem.setSourceContentItem(sourceContentItemSelector.getSelection());
Set<Long> linkedContentItemIds = new HashSet<Long>();
for (int i = 0; i < selectedLinkedContentItems.getItemCount(); i++) {
linkedContentItemIds.add(Long.valueOf(selectedLinkedContentItems.getValue(i)));
}
contentItem.setLinkedContentItemIds(linkedContentItemIds);
createOrChangeContentItem(contentItem, publish, statusLabel);
}
private void showInputError(String errorMsg) {
statusLabel.setText(errorMsg);
statusLabel.setStyleName("serverResponseLabelError");
}
private class SaveHandlerPrompt extends PopupPanel {
public SaveHandlerPrompt(final String sanifiedContent) {
super(false /* auto-hide*/, true /* modal */);
Label explanation = new Label("The content you have entered uses <object> or <embed> tags"
+ " that may cause problems in one or more browsers. We suggest you use the following"
+ " markup instead. (Your original markup is preserved as a comment below the new"
+ " suggested code.)");
explanation.setWidth("475px");
TextArea area = new TextArea();
area.setCharacterWidth(60);
area.setVisibleLines(15);
area.setText(sanifiedContent);
area.setReadOnly(true);
Button useSuggested = new Button("Use suggested content");
useSuggested.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
contentEditor.setContent(sanifiedContent);
hide();
doClickWork(null);
}
});
Button useOriginal = new Button("Ignore suggestion; use original content");
useOriginal.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
hide();
doClickWork(null);
}
});
FlowPanel panel = new FlowPanel();
panel.add(explanation);
FlowPanel plainDiv = new FlowPanel();
plainDiv.add(area);
panel.add(plainDiv);
panel.add(useSuggested);
panel.add(useOriginal);
setWidget(panel);
}
}
}
/**
* Creates event handlers for the Locations UI.
*/
private void createLocationHandlers() {
// first, set up interactions between the widgets:
final ClickHandler radioHandler = new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
adjustLocationControls();
controlGeocodeButton();
}
};
final KeyUpHandler textHandler = new KeyUpHandler() {
@Override
public void onKeyUp(KeyUpEvent event) {
controlGeocodeButton();
}
};
useDisplayedLocation.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
radioHandler.onClick(event);
locationDescriptionTextArea.setFocus(true);
}
});
useAlternateLocation.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
radioHandler.onClick(event);
alternateTextBox.setFocus(true);
}
});
useManualLatLong.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
radioHandler.onClick(event);
latitudeTextBox.setFocus(true);
}
});
locationDescriptionTextArea.addKeyUpHandler(textHandler);
alternateTextBox.addKeyUpHandler(textHandler);
latitudeTextBox.addKeyUpHandler(textHandler);
longitudeTextBox.addKeyUpHandler(textHandler);
// Actually handle the geocode button:
geocodeButton.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
if (useManualLatLong.getValue()) {
// the latitude and longitude textboxes already have the right values in them
recenterMap();
} else {
String address = (useDisplayedLocation.getValue() ? locationDescriptionTextArea
: alternateTextBox).getText();
geocoderStatus.setText("");
new Geocoder().getLatLng(address, new LatLngCallback() {
@Override
public void onFailure() {
geocoderStatus.setText("geocoding failed!");
}
@Override
public void onSuccess(LatLng point) {
geocoderStatus.setText("success");
latitudeTextBox.setText(String.valueOf(point.getLatitude()));
longitudeTextBox.setText(String.valueOf(point.getLongitude()));
recenterMap();
}
});
}
}
});
map.addMapRightClickHandler(new MapRightClickHandler() {
@Override
public void onRightClick(MapRightClickEvent event) {
LatLng clickedLatLng = map.convertContainerPixelToLatLng(event.getPoint());
latitudeTextBox.setText(String.valueOf(clickedLatLng.getLatitude()));
longitudeTextBox.setText(String.valueOf(clickedLatLng.getLongitude()));
useManualLatLong.setValue(true);
useManualLatLong.fireEvent(new ClickEvent() {});
recenterMap();
}
});
}
void adjustLocationControls() {
alternateTextBox.setEnabled(useAlternateLocation.getValue());
boolean manualLatLong = useManualLatLong.getValue();
latitudeTextBox.setEnabled(manualLatLong);
longitudeTextBox.setEnabled(manualLatLong);
}
void controlGeocodeButton() {
if (useDisplayedLocation.getValue()) {
geocodeButton.setEnabled(!locationDescriptionTextArea.getText().isEmpty());
geocodeButton.setText("Geocode location");
} else if (useAlternateLocation.getValue()) {
geocodeButton.setEnabled(!alternateTextBox.getText().isEmpty());
geocodeButton.setText("Geocode location");
} else {
geocodeButton.setEnabled(!latitudeTextBox.getText().isEmpty()
&& !longitudeTextBox.getText().isEmpty());
geocodeButton.setText("Map location");
}
}
void recenterMap() {
try {
LatLng target = LatLng.newInstance(
Double.parseDouble(latitudeTextBox.getText()),
Double.parseDouble(longitudeTextBox.getText()));
if (map.isVisible()) {
map.panTo(target);
} else {
map.setVisible(true);
map.setCenter(target);
map.checkResizeAndCenter();
// checkResizeAndCenter() call added per comments in
// http://code.google.com/p/gwt-google-apis/issues/detail?id=223
}
if (mapMarker == null) {
mapMarker = new Marker(target);
map.addOverlay(mapMarker);
} else {
mapMarker.setLatLng(target);
}
} catch (NumberFormatException e) {
geocoderStatus.setText("invalid latitude or longitude");
map.setVisible(false);
}
// Make the copyright text smaller so it fits in the map.
// This doesn't seem to work if it's set right when the map is created, so do it here.
map.getElement().getFirstChildElement().getNextSiblingElement()
.getStyle().setProperty("fontSize", "xx-small");
}
/**
* Make an RPC call to the server to persist a new content entity or a change to an existing
* content entity to the datastore. The timestamp is updated once the change is done if no
* value had been entered for it before.
*/
private void createOrChangeContentItem(
final BaseContentItem sentContentItem, final boolean publish, final Label statusLabel) {
AsyncCallback<BaseContentItem> callback = new AsyncCallback<BaseContentItem>() {
public void onFailure(Throwable caught) {
if (statusLabel != null) {
statusLabel.setText("Save not successful. Try again.");
statusLabel.setStyleName("serverResponseLabelError");
}
}
public void onSuccess(BaseContentItem returnedContentItem) {
if (statusLabel != null) {
statusLabel.setText(publish ? "Published!" : "Saved as draft");
statusLabel.setStyleName("serverResponseLabelSuccess");
}
timestamp.setText(DateUtil.formatDateTime(returnedContentItem.getTimestamp()));
updateDisplayedPublishStatus(returnedContentItem);
if (returnedContentItem.getContentItemType() == ContentItemType.PLAYER
&& returnedContentItem.getLivingStoryId() == null) {
addUnassignedPlayer((PlayerContentItem) returnedContentItem);
}
// Set the content editor items to what was returned from the server. This is needed
// because some changes are made to the content as entered by the user, such as adding
// target="_blank" in links and adding player tags.
contentEditor.setContent(returnedContentItem.getContent());
if (returnedContentItem.getContentItemType() == ContentItemType.EVENT) {
summaryEditor.setContent(((EventContentItem)returnedContentItem).getEventSummary());
}
if (returnedContentItem.getContentItemType() == ContentItemType.NARRATIVE) {
narrativeSummaryTextArea.setContent(
((NarrativeContentItem) returnedContentItem).getNarrativeSummary());
}
// remember which linked content items were suggested, but fix up the returned content
// item so that nothing incorrect gets cached locally.
Set<Long> suggestionIds = GlobalUtil.copySet(returnedContentItem.getLinkedContentItemIds());
suggestionIds.removeAll(sentContentItem.getLinkedContentItemIds());
returnedContentItem.setLinkedContentItemIds(sentContentItem.getLinkedContentItemIds());
contentItemListBox.addOrUpdateContentItem(returnedContentItem);
linkedContentItemSelector.addOrUpdateContentItem(returnedContentItem);
updatePreview();
if (!suggestionIds.isEmpty()) {
linkedContentItemSelector.setSuggestedContentItemIds(suggestionIds);
linkedContentItemSelector.selectSuggested();
advisoryLabel.setVisible(true);
// scroll the picker panel into view. This should assure that both the
// linkedContentItemSelector and advisoryLabel are fully visible (if indeed both can fit
// onscreen at once).
pickerPanel.getElement().scrollIntoView();
// and, in case it doesn't fit all onscreen, prioritize display of the
// advisory label:
advisoryLabel.getElement().scrollIntoView();
} else {
hideSuggestions();
}
}
};
contentRpcService.createOrChangeContentItem(sentContentItem, callback);
}
private void hideSuggestions() {
advisoryLabel.setVisible(false);
linkedContentItemSelector.setSuggestedContentItemIds(Collections.<Long>emptySet());
}
/**
* Make an RPC call to the server to delete an existing content entity. After it's done, remove
* it from the content item Listbox and clear the edit area.
*/
private void deleteContentItem(final BaseContentItem contentItem, final Label statusLabel) {
final Long id = contentItem.getId();
AsyncCallback<Void> callback = new AsyncCallback<Void>() {
public void onFailure(Throwable caught) {
statusLabel.setText("Delete not successful. Try again.");
statusLabel.setStyleName("serverResponseLabelError");
}
public void onSuccess(Void result) {
statusLabel.setText("Saved!");
statusLabel.setStyleName("serverResponseLabelSuccess");
contentPanel.showWidget(0);
contentItemListBox.removeContentItem(id);
if (contentItem.getLivingStoryId() == null
&& contentItem.getContentItemType() == ContentItemType.PLAYER) {
removeUnassignedPlayer(id, ((PlayerContentItem)contentItem).getName());
}
}
};
contentRpcService.deleteContentItem(contentItem.getId(), callback);
}
private void showSpecialAttributesPanel(ContentItemType contentItemType) {
Integer panelIndex = contentItemTypeToEditorPanelMap.get(contentItemType);
if (panelIndex == null) {
specialAttributesPanel.setVisible(false);
} else {
specialAttributesPanel.showWidget(panelIndex);
specialAttributesPanel.setVisible(true);
if (contentItemType == ContentItemType.PLAYER) {
if (livingStorySelector.getSelectedLivingStoryId() == null) {
// Display fields for name, aliases, player type and a photo selector
playerAttributesPanel.showWidget(1);
} else {
// Display a parent player content item selector
playerAttributesPanel.showWidget(0);
formatParentPlayerDisplay();
}
}
}
}
private void updatePreview() {
BaseContentItem contentItem = contentItemListBox.getSelectedContentItem();
if (contentItem.getDisplayString().equals("New Content Item")) {
previewPanel.clear();
} else {
previewPanel.setWidget(StreamViewFactory.createView(contentItem,
contentItemListBox.getLoadedContentItemsMap()));
}
}
@Override
public void onLivingStoriesChanged() {
livingStorySelector.refresh();
contentItemListBox.clear();
}
@Override
public void onShow() {
livingStorySelector.selectCoordinatedLivingStory();
livingStorySelectionHandler.onChange(null);
if (livingStorySelector.hasSelection()) {
LivingStoryData.setLivingStoryId(livingStorySelector.getSelectedLivingStoryId());
}
}
private void populateUnassignedPlayersMap(List<PlayerContentItem> players) {
for (PlayerContentItem player : players) {
unassignedPlayersIdToContentItemMap.put(player.getId(), player);
}
}
private void populatePlayerSuggestPanel(PlayerSuggestAndAddPanel playerSuggestPanel) {
for (PlayerContentItem player : unassignedPlayersIdToContentItemMap.values()) {
playerSuggestPanel.addPlayer(player);
}
}
private void addUnassignedPlayer(PlayerContentItem player) {
if (!unassignedPlayersIdToContentItemMap.containsKey(player.getId())) {
unassignedPlayersIdToContentItemMap.put(player.getId(), player);
contributorSuggestPanel.addPlayer(player);
generalPlayerSuggestPanel.addPlayer(player);
}
}
private void removeUnassignedPlayer(Long id, String name) {
if (unassignedPlayersIdToContentItemMap.containsKey(id)) {
unassignedPlayersIdToContentItemMap.remove(id);
contributorSuggestPanel.removePlayer(name);
generalPlayerSuggestPanel.removePlayer(name);
}
}
/**
* Utility class for grouping and handling widgets related to saving & deleting content items
*
*/
private class SaveControlsWidgetGroup {
Button saveDraftButton;
Button publishButton;
Button republishButton;
Button deleteButton;
Label statusLabel;
}
}