com.google.livingstories.client.contentmanager.ContentItemManager.java Source code

Java tutorial

Introduction

Here is the source code for com.google.livingstories.client.contentmanager.ContentItemManager.java

Source

/**
 * 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:&nbsp;"));
        latitudeTextBox = new TextBox();
        latitudeTextBox.setEnabled(false);
        latLongPanel.add(latitudeTextBox);
        latLongPanel.add(new HTML("&nbsp;Longitude:&nbsp;"));
        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("&raquo;");
        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("&laquo;");
        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;
    }
}