org.gwtportlets.portlet.client.edit.PageEditor.java Source code

Java tutorial

Introduction

Here is the source code for org.gwtportlets.portlet.client.edit.PageEditor.java

Source

/*
 * GWT Portlets Framework (http://code.google.com/p/gwtportlets/)
 * Copyright 2009 Business Systems Group (Africa)
 *
 * This file is part of GWT Portlets.
 *
 * GWT Portlets is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * GWT Portlets is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with GWT Portlets.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.gwtportlets.portlet.client.edit;

import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.MouseMoveEvent;
import com.google.gwt.event.dom.client.MouseMoveHandler;
import com.google.gwt.event.logical.shared.CloseEvent;
import com.google.gwt.event.logical.shared.CloseHandler;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.user.client.*;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.*;
import org.gwtportlets.portlet.client.HasWidgetFactoryEnabled;
import org.gwtportlets.portlet.client.WidgetFactory;
import org.gwtportlets.portlet.client.WidgetFactoryProvider;
import org.gwtportlets.portlet.client.WidgetRefreshHook;
import org.gwtportlets.portlet.client.edit.row.RowLayoutEditor;
import org.gwtportlets.portlet.client.layout.*;
import org.gwtportlets.portlet.client.ui.*;
import org.gwtportlets.portlet.client.ui.LayoutPanel;
import org.gwtportlets.portlet.client.util.Rectangle;
import org.gwtportlets.portlet.client.util.SyncToClientArea;

import java.util.ArrayList;
import java.util.List;

/**
 * Manages editing the layouts for a tree of containers. Extend this and
 * implement savePage. 
 */
public abstract class PageEditor implements Event.NativePreviewHandler, LayoutHandler {

    private final LayoutPanel overlay = new LayoutPanel();
    private final SyncToClientArea sync = new SyncToClientArea(overlay);
    private final HTML dropIndicator = new HTML();
    private final HTML heldIndicator = new HTML();
    private final EditorManagerControlDialog status = new EditorManagerControlDialog(this);

    private Container root;
    private Container previousRoot;
    private String pageName;
    private int editDepth;
    private int maxEditDepth;

    private List<LayoutEditor> editorList = new ArrayList<LayoutEditor>();
    private List<WidgetResizer> resizerList = new ArrayList<WidgetResizer>();
    private List<ContainerIndicator> emptyList = new ArrayList<ContainerIndicator>();

    private UndoStack undoStack = new UndoStack();
    private WidgetEditor defaultWidgetEditor = createDefaultWidgetEditor();
    private TitlePanelEditor titlePanelEditor = createCaptionPanelEditor();
    private LayoutPanelEditor layoutPanelEditor = createLayoutPanelEditor();
    private WidgetReplacer widgetReplacer = createWidgetReplacer();
    private Button save = new CssButton("Save");

    private boolean updatePending;

    private PopupPanel menuPopup;
    private Widget menuTarget;

    private Widget heldWidget;
    private Container heldWidgetContainer;
    private LayoutConstraints heldConstraints;
    private String overflow;
    private boolean drillDownOnDrop;
    private String description;
    private boolean refreshOnDrop;

    private boolean pickupBusy;
    private int lastDropIndicatorClientX;
    private int lastDropIndicatorClientY;

    private static final String STYLE_RAISED = "raised";

    private final CloseHandler editDialogClosed = new CloseHandler() {
        public void onClose(CloseEvent closeEvent) {
            Object sender = closeEvent.getSource();
            if (sender instanceof PageEditorDialog && !((PageEditorDialog) sender).isDirty()) {
                discardUndo();
            }
            hideIndicator(heldIndicator);
            lowerOverlay();
            showAllResizers();
        }
    };

    private final ClickHandler discardHeldWidget = new ClickHandler() {
        public void onClick(ClickEvent event) {
            discardHeldWidget();
        }
    };

    /** Creates a LayoutPanel with row layout. */
    protected WidgetOption createRow = new WidgetOption() {
        public Widget createWidget(Widget replacing) {
            return new LayoutPanel(new RowLayout(false));
        }

        public String getName() {
            return "Row";
        }

        public boolean isContainerOption() {
            return true;
        }
    };

    /** Creates a LayoutPanel with column layout. */
    protected WidgetOption createColumn = new WidgetOption() {
        public Widget createWidget(Widget replacing) {
            return new LayoutPanel(new RowLayout(true));
        }

        public String getName() {
            return "Column";
        }

        public boolean isContainerOption() {
            return true;
        }
    };

    /** Creates a TitlePanel. */
    protected WidgetOption createTitle = new WidgetOption() {
        public Widget createWidget(Widget replacing) {
            return new TitlePanel();
        }

        public String getName() {
            return "Title";
        }

        public boolean isContainerOption() {
            return true;
        }

        public boolean isAddPlaceholderOnContain() {
            return false;
        }
    };

    /** Creates a placeholder.  */
    protected WidgetOption createPlaceholder = new WidgetOption() {
        public String getName() {
            return "Placeholder";
        }

        public Widget createWidget(Widget replacing) {
            return new PlaceholderPortlet();
        }
    };

    /** Creates a spacer.  */
    protected WidgetOption createSpacer = new WidgetOption() {
        public String getName() {
            return "Spacer";
        }

        public Widget createWidget(Widget replacing) {
            return new SpacerPortlet();
        }
    };

    /** Popup a dialog to choose a portlet. */
    protected WidgetOption createPortlet = new WidgetOption() {
        public String getName() {
            return "Portlet...";
        }

        public void createWidget(Widget replacing, AsyncCallback cb) {
            PageEditor.this.createWidget(replacing, cb);
        }
    };

    /**
     * Add this to widgets to make mouse move events look they they come
     * from the overlay panel.
     */
    protected MouseMoveHandler overlayMouseListener = new MouseMoveHandler() {
        public void onMouseMove(MouseMoveEvent event) {
            NativeEvent ev = event.getNativeEvent();
            onOverlayMouseMove(ev.getClientX(), ev.getClientY());
        }
    };

    /**
     * Add this to widgets to make click events look they they come
     * from the overlay panel.
     */
    protected ClickHandler overlayClickListener = new ClickHandler() {
        public void onClick(ClickEvent event) {
            NativeEvent ev = event.getNativeEvent();
            onOverlayClicked(ev.getClientX(), ev.getClientY());
        }
    };

    public PageEditor() {
        dropIndicator.setStyleName("portlet-ed-drop-indicator");
        heldIndicator.setStyleName("portlet-ed-held-indicator");
        overlay.setStyleName("portlet-ed-overlay");
        overlay.setTitle("Click for menu");

        overlay.addClickHandler(overlayClickListener);
        overlay.addMouseMoveHandler(overlayMouseListener);
        heldIndicator.addMouseMoveHandler(overlayMouseListener);

        dropIndicator.addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent event) {
                NativeEvent ev = event.getNativeEvent();
                onDropIndicatorClicked(ev.getClientX(), ev.getClientY());
            }
        });

        getControl().getLeftPanel().add(save);

        save.addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent event) {
                WidgetFactory wf = getRoot().createWidgetFactory();
                LayoutUtil.clearDoNotSendToServerFields(wf);
                savePage(wf, new AsyncCallback() {
                    public void onFailure(Throwable caught) {
                    }

                    public void onSuccess(Object result) {
                    }
                });
            }
        });
    }

    /**
     * Save changes to the page and invoke callback when done or on error.
     */
    protected abstract void savePage(WidgetFactory wf, AsyncCallback callback);

    private void onDropIndicatorClicked(int clientX, int clientY) {
        if (pickupBusy) {
            dropWidget(clientX, clientY);
        }
    }

    /**
     * Mouse move from our overlay panel or the held indicator.
     */
    private void onOverlayMouseMove(int clientX, int clientY) {
        if (pickupBusy) {
            moveDropIndicator(clientX, clientY);
        }
    }

    /**
     * Our overlay panel has been clicked.
     */
    private void onOverlayClicked(int clientX, int clientY) {
        if (menuPopup != null) {
            hideMenu();
        } else if (pickupBusy) {
            dropWidget(clientX, clientY);
        } else {
            toggleMenuForWidget(null);
        }
    }

    /**
     * Start editing root.
     */
    public void startEditing(String pageName, Container root) {
        stopEditing();
        this.root = root;
        if (previousRoot != root) {
            undoStack.clear();
        }
        previousRoot = null;
        RootPanel.get().add(overlay);
        sync.resizeWidget();
        sync.startListening();
        setEditDepth(editDepth);
        this.pageName = pageName;
        save.setTitle(pageName == null ? "Save" : "Save '" + pageName + "'");
    }

    /**
     * Stop editing our root container. NOP if not editing.
     */
    public void stopEditing() {
        if (root != null) {
            hideMenu();
            hideIndicator(dropIndicator);
            disposeEditorsEtc();
            if (overlay.isAttached()) {
                sync.stopListening();
                RootPanel.get().remove(overlay);
            }
            status.hide();
            previousRoot = root;
            root = null;
        }
    }

    /**
     * Clear the undo history. Normally the editor will keep the history
     * between start and stop editing calls so long as the root Container
     * being edited remains the same.
     */
    public void clearUndoStack() {
        undoStack.clear();
        previousRoot = null;
    }

    public Container getRoot() {
        return root;
    }

    /**
     * Get the name of the page being edited.
     */
    public String getPageName() {
        return pageName;
    }

    /**
     * Get rid of all editors, resizers and empty indicators.
     */
    private void disposeEditorsEtc() {
        for (int i = editorList.size() - 1; i >= 0; i--) {
            editorList.get(i).dispose();
        }
        editorList.clear();
        for (int i = resizerList.size() - 1; i >= 0; i--) {
            resizerList.get(i).dispose();
        }
        resizerList.clear();
        for (int i = emptyList.size() - 1; i >= 0; i--) {
            emptyList.get(i).dispose();
        }
        emptyList.clear();
    }

    /**
     * Get all the widget options we support. These are used by
     * {@link #buildAddMenu()},
     * {@link #buildChangeContainerToMenu(org.gwtportlets.portlet.client.layout.Container)}
     * and {@link #buildReplaceWithMenu(com.google.gwt.user.client.ui.Widget)}
     * to create menu items. Subclasses should override this and return
     * extra application specific options.<p>
     *
     * Subclasses can also override the methods that create the menus and
     * menu items themselves (e.g. to group options into submenus).
     */
    protected WidgetOption[] getWidgetOptionList() {
        return new WidgetOption[] { createRow, createColumn, createTitle, createPlaceholder, createSpacer,
                createPortlet };
    }

    /**
     * Change the editing depth. If there are no containers at the specified
     * depth then try one level up and so on.
     */
    public void setEditDepth(int editDepth) {
        hideMenu();
        hideIndicator(dropIndicator);
        if (editDepth < 0) {
            editDepth = 0;
        }
        ArrayList found = new ArrayList();
        for (;; --editDepth) {
            maxEditDepth = findContainersAtDepth(root, 0, editDepth, found);
            if (!found.isEmpty()) {
                break;
            }
        }
        if (this.editDepth != editDepth) {
            disposeEditorsEtc();
        }
        this.editDepth = editDepth;

        // make sure we have an editor for each container, a resizer widget
        // for each widget in each container and an empty indicator for each
        // empty container
        ArrayList<LayoutEditor> newEditorList = new ArrayList<LayoutEditor>();
        ArrayList newResizerList = new ArrayList();
        ArrayList newEmptyList = new ArrayList();
        for (int i = 0; i < found.size(); i++) {
            Container container = (Container) found.get(i);
            LayoutEditor ed = getEditorFor(container);
            if (ed == null) {
                ed = createEditorFor(container);
                if (ed == null) {
                    continue;
                }
                ed.init(this, container);
            }
            newEditorList.add(ed);
            int wc = container.getWidgetCount();
            if (wc == 0) {
                ContainerIndicator e = getEmptyIndicatorFor(ed);
                if (e == null) {
                    e = new ContainerIndicator(this, ed);
                }
                newEmptyList.add(e);
                e.update();
            } else {
                for (int j = 0; j < wc; j++) {
                    Widget w = container.getWidget(j);
                    WidgetResizer r = getResizerFor(w);
                    if (r == null || r.getEditor() != ed) {
                        r = new WidgetResizer(this, ed, w);
                    }
                    newResizerList.add(r);
                    r.update();
                }
            }
        }

        // dispose editors for containers we no longer have
        editorList.removeAll(newEditorList);
        for (int i = editorList.size() - 1; i >= 0; i--) {
            editorList.get(i).dispose();
        }
        editorList = newEditorList;

        // dispose resizers for widgets we no longer have
        resizerList.removeAll(newResizerList);
        for (int i = resizerList.size() - 1; i >= 0; i--) {
            resizerList.get(i).dispose();
        }
        resizerList = newResizerList;

        // dispose of empty indicators that are no longer needed
        emptyList.removeAll(newEmptyList);
        for (int i = emptyList.size() - 1; i >= 0; i--) {
            emptyList.get(i).dispose();
        }
        emptyList = newEmptyList;

        if (pickupBusy) {
            moveDropIndicator(lastDropIndicatorClientX, lastDropIndicatorClientY);
        }
        if (heldWidget != null) {
            moveIndicator(heldIndicator, heldWidget);
        }

        status.update();
    }

    public int getEditDepth() {
        return editDepth;
    }

    public int getMaxEditDepth() {
        return maxEditDepth;
    }

    public void update() {
        setEditDepth(editDepth);
    }

    /**
     * Put all containers present at depth into list and return the maximum
     * depth of the container tree.
     */
    private int findContainersAtDepth(Container container, int depth, int targetDepth, List list) {
        if (depth == targetDepth) {
            list.add(container);
        }
        int ans = depth;
        for (int i = container.getWidgetCount() - 1; i >= 0; i--) {
            Widget w = container.getWidget(i);
            if (w instanceof Container) {
                int max = findContainersAtDepth((Container) w, depth + 1, targetDepth, list);
                if (max > ans) {
                    ans = max;
                }
            }
        }
        return ans;
    }

    /**
     * Get the resizer for widget (null if none).
     */
    private WidgetResizer getResizerFor(Widget widget) {
        for (int i = resizerList.size() - 1; i >= 0; i--) {
            WidgetResizer r = resizerList.get(i);
            if (r.getTarget() == widget) {
                return r;
            }
        }
        return null;
    }

    /**
     * Get the editor for container (null if none).
     */
    private LayoutEditor getEditorFor(Container container) {
        for (int i = editorList.size() - 1; i >= 0; i--) {
            LayoutEditor ed = editorList.get(i);
            if (ed.getContainer() == container) {
                return ed;
            }
        }
        return null;
    }

    /**
     * Get the empty indicator for ed (null if none).
     */
    private ContainerIndicator getEmptyIndicatorFor(LayoutEditor ed) {
        for (int i = emptyList.size() - 1; i >= 0; i--) {
            ContainerIndicator e = emptyList.get(i);
            if (e.getEditor() == ed) {
                return e;
            }
        }
        return null;
    }

    /**
     * Create an editor for container or return null if none is available
     * (i.e. unsupported layout).
     */
    protected LayoutEditor createEditorFor(Container c) {
        Layout layout = c.getLayout();
        if (layout instanceof RowLayout) {
            return new RowLayoutEditor();
        }
        return null;
    }

    /**
     * Get or create an editor for w or return null if none.
     */
    protected WidgetEditor getWidgetEditorFor(Widget w) {
        if (w instanceof WidgetEditorProvider) {
            // can provide its own editor
            return ((WidgetEditorProvider) w).getWidgetEditorFor(w);
        }
        if (w instanceof TitlePanel) {
            return titlePanelEditor;
        }
        if (w instanceof LayoutPanel) {
            return layoutPanelEditor;
        }
        if (w instanceof WidgetFactoryProvider) {
            return defaultWidgetEditor;
        }
        // No point in editing if widget does not preserve its state
        return null;
    }

    /**
     * Use preview for our global actions so they work even when the mouse is
     * over our resizers and other widgets.
     */
    public void onPreviewNativeEvent(Event.NativePreviewEvent event) {
        NativeEvent ev = event.getNativeEvent();
        switch (Event.getTypeInt(ev.getType())) {

        case Event.ONMOUSEWHEEL:
            onMouseWheel(ev.getMouseWheelVelocityY());
            event.cancel();
            break;

        case Event.ONKEYPRESS:
            int key = ev.getKeyCode();
            if (key == 27) {
                onEscPressed();
            } else if (ev.getCtrlKey()) {
                if (key == 'z') {
                    undoStack.undo();
                } else if (key == 'Z' && ev.getShiftKey()) {
                    undoStack.redo();
                }
            }
            event.cancel();
            break;
        }
    }

    /**
     * The mouse wheel was moved.
     */
    private void onMouseWheel(int deltaY) {
        if (status.isLevelVisible()) {
            setEditDepth(deltaY < 0 ? editDepth - 1 : editDepth + 1);
        }
    }

    /**
     * The escape key was pressed.
     */
    protected void onEscPressed() {
        if (pickupBusy) {
            discardHeldWidget();
        } else {
            stopEditing();
        }
    }

    /**
     * Find the editor containing the coordinates given.
     */
    private LayoutEditor getEditorFor(int clientX, int clientY) {
        for (int i = editorList.size() - 1; i >= 0; i--) {
            LayoutEditor ed = editorList.get(i);
            Widget container = (Widget) ed.getContainer();
            if (LDOM.contains(container.getElement(), clientX, clientY)) {
                return ed;
            }
        }
        return null;
    }

    /**
     * Start dragging the widget.
     */
    public void dragWidget(LayoutEditor src, Widget widget, int clientX, int clientY) {
        description = "Move " + getWidgetDescription(widget);
        startWidgetDrag(src, widget, clientX, clientY);
    }

    /**
     * Pickup the widget and enter 'drop it somewhere' mode.
     */
    private void pickupWidget(LayoutEditor src, Widget widget) {
        pickupOrAddWidget(src, widget, "Drop " + getWidgetDescription(widget));
    }

    /**
     * Get a short description for w. This is used to describe operations
     * for undo/redo etc. Subclasses can override this to provide more
     * information about their widgets.
     */
    public String getWidgetDescription(Widget w) {
        StringBuffer s = new StringBuffer();
        if (w instanceof Portlet) {
            s.append(((Portlet) w).getWidgetName());
        } else if (w instanceof Container) {
            s.append(((Container) w).getDescription());
        } else {
            s.append("Widget");
        }
        return s.toString();
    }

    /**
     * Hide all resizers and indicators.
     */
    private void hideAllResizers() {
        hideOtherResizers((Container) null);
    }

    /**
     * Hide all resizers and indicators that have nothing to do with container.
     * If container is null then all are hidden.
     */
    private void hideOtherResizers(Container container) {
        for (int i = resizerList.size() - 1; i >= 0; i--) {
            WidgetResizer r = resizerList.get(i);
            r.setVisible(r.getEditor().getContainer() == container);
        }
        for (int i = emptyList.size() - 1; i >= 0; i--) {
            ContainerIndicator ci = emptyList.get(i);
            ci.setVisible(ci.getEditor().getContainer() == container);
        }
    }

    /**
     * Hide all resizers and indicators that have nothing to do with widget.
     * If widget is null then all are hidden.
     */
    private void hideOtherResizers(Widget widget) {
        for (int i = resizerList.size() - 1; i >= 0; i--) {
            WidgetResizer r = resizerList.get(i);
            r.setVisible(r.getTarget() == widget);
        }
        for (int i = emptyList.size() - 1; i >= 0; i--) {
            ContainerIndicator ci = emptyList.get(i);
            ci.setVisible(ci.getEditor().getContainer() == widget);
        }
    }

    /**
     * Show all resizers and indicators.
     */
    private void showAllResizers() {
        for (int i = resizerList.size() - 1; i >= 0; i--) {
            resizerList.get(i).setVisible(true);
        }
        for (int i = emptyList.size() - 1; i >= 0; i--) {
            emptyList.get(i).setVisible(true);
        }
        update();
        // have to do an update as sometimes the indicators
        // dont display properly after setVisible(true)        
    }

    private void deleteWidget(LayoutEditor src, Widget widget) {
        raiseOverlay();
        hideAllResizers();
        moveIndicator(heldIndicator, widget);
        if (Window.confirm("Are you sure you want to delete this widget?")) {
            beginUndo("Delete " + getWidgetDescription(widget));
            widget.removeFromParent();
            src.getContainer().layout();
            onWidgetDeleted(widget);
        }
        showAllResizers();
        discardHeldWidget();
    }

    /**
     * Raise the overlay panel above the resizers.
     */
    private void raiseOverlay() {
        overlay.addStyleDependentName(STYLE_RAISED);
    }

    /**
     * Lower the overlay panel below the resizers.
     */
    private void lowerOverlay() {
        overlay.removeStyleDependentName(STYLE_RAISED);
    }

    /**
     * The widget has been deleted from the layout. Override this to do
     * extra cleanup.
     */
    protected void onWidgetDeleted(Widget widget) {
    }

    private void pickupOrAddWidget(LayoutEditor src, Widget widget, String description) {
        Event ev = DOM.eventGetCurrentEvent();
        startWidgetDrag(src, widget, ev == null ? -1 : DOM.eventGetClientX(ev),
                ev == null ? -1 : DOM.eventGetClientY(ev));
        raiseOverlay();
        pickupBusy = true;

        boolean pickup = widget.isAttached();
        if (pickup) {
            moveIndicator(heldIndicator, widget);
        }

        this.description = description;
        status.beginOperation(description, "Choose position for widget and click", false, discardHeldWidget);
    }

    /**
     * Move the indicator to cover 'over' and show it.
     */
    private void moveIndicator(Widget indicator, Widget over) {
        Rectangle r = LDOM.getBounds(over);
        if (!indicator.isAttached()) {
            RootPanel.get().add(indicator);
        }
        LDOM.setBounds(indicator, r);
    }

    /**
     * Hide the indicator if it is showing.
     */
    private void hideIndicator(Widget indicator) {
        if (indicator.isAttached()) {
            RootPanel.get().remove(indicator);
        }
    }

    private void startWidgetDrag(LayoutEditor src, Widget widget, int left, int top) {
        hideMenu();
        heldWidget = widget;
        heldWidgetContainer = src == null ? null : src.getContainer();
        heldConstraints = heldWidgetContainer == null ? null : heldWidgetContainer.getLayoutConstraints(widget);
        moveDropIndicator(left, top);
    }

    /**
     * Drop the currently held widget at clientX, clientY.
     */
    public void dropWidget(int clientX, int clientY) {
        if (heldWidget == null) {
            return;
        }
        LayoutEditor dest = getEditorFor(clientX, clientY);
        if (dest != null) {
            if (description == null) {
                throw new IllegalStateException("description == null");
            }
            beginUndo(description);
            if (dest.dropWidget(heldWidget, heldConstraints, overflow, clientX, clientY)) {
                if (heldWidgetContainer != dest.getContainer()) {
                    dest.getContainer().layout();
                }
                if (refreshOnDrop) {
                    refresh(heldWidget);
                }
            } else {
                discardUndo();
            }
        }
        if (heldWidgetContainer != null) {
            heldWidgetContainer.layout();
        }
        int newDepth = drillDownOnDrop ? editDepth + 1 : editDepth;
        discardHeldWidget();
        setEditDepth(newDepth);
        cancelPendingUpdate();
    }

    private void discardHeldWidget() {
        hideIndicator(dropIndicator);
        hideIndicator(heldIndicator);
        pickupBusy = false;
        heldWidget = null;
        heldWidgetContainer = null;
        heldConstraints = null;
        drillDownOnDrop = false;
        overflow = null;
        description = null;
        refreshOnDrop = false;
        lowerOverlay();
        status.endOperation();
    }

    /**
     * Move the drop indicator over the widget at left and top (if any).
     */
    private void moveDropIndicator(int clientX, int clientY) {
        lastDropIndicatorClientX = clientX;
        lastDropIndicatorClientY = clientY;
        Rectangle r = null;
        if (heldWidget != null) {
            LayoutEditor dest = getEditorFor(clientX, clientY);
            if (dest != null) {
                r = dest.getDropArea(heldWidget, heldConstraints, clientX, clientY);
            }
        }
        if (r == null) {
            RootPanel.get().remove(dropIndicator);
        } else {
            if (!dropIndicator.isAttached()) {
                RootPanel.get().add(dropIndicator);
            }
            LDOM.setBounds(dropIndicator, r);
        }
    }

    /**
     * Display an editor menu suitable for resizer. If resizer is null then
     * display a general layout editing menu. If the menu to be displayed
     * is already visible then hide it instead.
     */
    public void toggleMenuForWidget(WidgetResizer resizer) {
        if (resizer == menuTarget && menuPopup != null) {
            hideMenu();
            return;
        }
        menuTarget = resizer;
        MenuBar bar = createMenuBar(true);
        if (resizer != null) {
            buildEditorMenuItems(resizer.getEditor(), resizer.getTarget(), bar);
        }
        buildManagerMenuItems(bar);
        displayMenu(bar, -1, -1);
    }

    /**
     * Create a menu bar for use in the page editor.
     */
    public MenuBar createMenuBar(boolean vertical) {
        return new PageEditorMenuBar(this, vertical);
    }

    /**
     * Display an editor menu suitable for indicator. If the menu to be
     * displayed is already visible then hide it instead.
     */
    public void toggleMenuForContainer(ContainerIndicator indicator) {
        if (indicator == menuTarget && menuPopup != null) {
            hideMenu();
            return;
        }
        menuTarget = indicator;
        MenuBar bar = createMenuBar(true);
        buildEditorMenuItems(indicator.getEditor(), null, bar);
        buildManagerMenuItems(bar);
        displayMenu(bar, -1, -1);
    }

    /**
     * Add menu items appropriate to editor and widget to bar. Note that
     * widget may be null but editor will never be null.
     */
    private void buildEditorMenuItems(final LayoutEditor editor, final Widget widget, MenuBar bar) {

        if (widget != null && editor.isEditConstraints(widget)) {
            bar.addItem("Edit Constraints...", new Command() {
                public void execute() {
                    hideOtherResizers(widget);
                    beginUndo("Edit " + getWidgetDescription(widget) + " Constraints");
                    editor.editConstraints(widget, new ValueChangeHandler() {
                        public void onValueChange(ValueChangeEvent event) {
                            onConstraintsChanged(widget);
                        }
                    }, editDialogClosed);
                }
            }).setTitle("Edit widget layout constraints");
        }

        editor.createMenuItems(widget, bar);

        if (widget != null) {
            bar.addItem("Pickup", new Command() {
                public void execute() {
                    pickupWidget(editor, widget);
                }
            }).setTitle("Pickup the widget (and click to drop it somewhere)");

            bar.addItem("Delete...", new Command() {
                public void execute() {
                    deleteWidget(editor, widget);
                }
            }).setTitle("Remove the widget");

            bar.addItem("Replace With", buildReplaceWithMenu(widget));

            WidgetEditor we = getWidgetEditorFor(widget);
            if (we != null) {
                we.createMenuItems(this, bar, widget);
            }
        }

        bar.addItem("Parent Container", buildParentMenu(editor));
    }

    /**
     * The widgets constraints have been updated.
     */
    protected void onConstraintsChanged(Widget widget) {
    }

    /**
     * Build a menu to replace widget with another widget or container.
     */
    private MenuBar buildReplaceWithMenu(final Widget widget) {
        WidgetOption[] a = getWidgetOptionList();
        MenuBar bar = createMenuBar(true);
        for (int i = 0; i < a.length; i++) {
            bar.addItem(createReplaceWithMenuItem(widget, a[i]));
        }
        return bar;
    }

    /**
     * Create a menu item to replace a widget with another widget or container.
     */
    private MenuItem createReplaceWithMenuItem(final Widget widget, final WidgetOption op) {
        MenuItem mi = new MenuItem(op.getName(), new Command() {
            public void execute() {
                op.createWidget(widget, new WidgetCallback() {
                    public void onSuccess(Widget w) {
                        replaceWidget(widget, w, op.isContainerOption(), op.isAddPlaceholderOnContain(),
                                op.isRefreshNeeded(w));
                    }
                });
            }
        });
        mi.setTitle((op.isContainerOption() ? "Put widget into new " : "Replace with ") + op.getDescription());
        return mi;
    }

    /**
     * Replace old with nw. Prompts for confirmation, creates unto record
     * etc. If addOldToNew is true is a Container then old is added to it
     * i.e. it takes the place of old but contains old.
     */
    private void replaceWidget(Widget old, Widget nw, boolean addOldToNew, boolean addPlaceholder,
            boolean refresh) {
        if (!addOldToNew) {
            hideAllResizers();
            moveIndicator(heldIndicator, old);
            boolean ok = Window
                    .confirm("Are you sure you want to " + "replace this widget with " + getWidgetDescription(nw));
            showAllResizers();
            discardHeldWidget();
            if (!ok) {
                return;
            }
        }
        beginUndo("Replace " + getWidgetDescription(old) + " with " + getWidgetDescription(nw));
        replaceWidgetImpl(old, nw, addOldToNew, addOldToNew && addPlaceholder ? createPlaceholder() : null);
        if (refresh) {
            refresh(nw);
        }
        if (addOldToNew) {
            setEditDepth(editDepth + 1);
        }
    }

    private Widget createPlaceholder() {
        return new PlaceholderPortlet();
    }

    /**
     * Replace old with nw. If addOldToNew is true then nw must be a Container
     * and old is added to it.
     */
    private void replaceWidgetImpl(Widget old, Widget nw, boolean addOldToNew, Widget placeholder) {
        Container con = (Container) old.getParent();
        int index = con.getWidgetIndex(old);
        LayoutConstraints constraints = con.getLayoutConstraints(old);
        con.remove(old);
        if (addOldToNew) {
            Container newCon = (Container) nw;
            newCon.add(old);
            if (placeholder != null) {
                newCon.add(placeholder);
            }
        } else {
            onWidgetDeleted(old);
        }
        con.insert(nw, index, constraints);
        con.layout();
        // con may not be at current edit depth so we may not get a
        // layoutUpdated notification so schedule an update
        scheduleUpdate();
    }

    /**
     * Build a menu of container specific menu items for the container
     * belonging to editor for the 'Parent >' menu.
     *
     * @see org.gwtportlets.portlet.client.edit.LayoutEditor#getContainer()
     */
    private MenuBar buildParentMenu(final LayoutEditor editor) {
        MenuBar bar = createMenuBar(true);
        final Container con = editor.getContainer();

        if (editor.isEditLayout()) {
            bar.addItem("Edit Layout...", new Command() {
                public void execute() {
                    hideOtherResizers(con);
                    raiseOverlay();
                    moveIndicator(heldIndicator, (Widget) con);
                    beginUndo("Edit " + getWidgetDescription((Widget) con) + " Layout");
                    editor.editLayout(editDialogClosed);
                }
            }).setTitle("Edit container layout settings");
        }

        MenuBar changeTo = buildChangeContainerToMenu(con);
        if (changeTo != null) {
            bar.addItem("Change To", changeTo).setTitle("Convert this container into a different container");
        }

        WidgetEditor we = getWidgetEditorFor((Widget) con);
        if (we != null) {
            we.createMenuItems(this, bar, (Widget) con);
        }

        return bar;
    }

    /**
     * Build a menu to replace con with a different container and move all of
     * its children to the new container. Returns null if menu would be empty.
     */
    private MenuBar buildChangeContainerToMenu(final Container con) {
        MenuBar bar = createMenuBar(true);
        WidgetOption[] a = getWidgetOptionList();
        int c = 0;
        for (int i = 0; i < a.length; i++) {
            WidgetOption op = a[i];
            if (op.isContainerOption()) {
                bar.addItem(createChangeContainerToMenuItem(con, op));
                ++c;
            }
        }
        return c == 0 ? null : bar;
    }

    /**
     * Create a menu item to replace a container with a different container
     * and move all of its children to the new container. The new container
     * widget is provided by op.
     */
    private MenuItem createChangeContainerToMenuItem(final Container con, final WidgetOption op) {
        MenuItem mi = new MenuItem(op.getName(), new Command() {
            public void execute() {
                op.createWidget(null, new WidgetCallback() {
                    public void onSuccess(Widget w) {
                        changeContainerTo(con, w);
                    }
                });
            }
        });
        mi.setTitle("Change to " + op.getDescription());
        return mi;
    }

    /**
     * Replace old with nw (must be a Container) and move all of olds
     * children to nw. Undo support is included.
     */
    private void changeContainerTo(Container old, Widget nw) {
        beginUndo("Change To " + getWidgetDescription(nw));
        moveChildren(old, (Container) nw);
        replaceWidgetImpl((Widget) old, nw, false, null);
    }

    /**
     * Add menu options for this layout editor manager to bar (e.g. for
     * undo, redo and add widget).
     */
    protected void buildManagerMenuItems(MenuBar bar) {
        UndoStack.Operation op = undoStack.getUndo();
        if (op != null) {
            String s = "Undo " + op.getDescription();
            bar.addItem(s, new Command() {
                public void execute() {
                    undoStack.undo();
                }
            }).setTitle(s + " (Ctrl-Z)");
        }

        op = undoStack.getRedo();
        if (op != null) {
            String s = "Redo " + op.getDescription();
            bar.addItem(s, new Command() {
                public void execute() {
                    undoStack.redo();
                }
            }).setTitle(s + " (Ctrl-Shift-Z)");
        }

        bar.addItem("Add", buildAddMenu());
    }

    /**
     * Build a menu to add new widgets to the layout.
     */
    private MenuBar buildAddMenu() {
        MenuBar bar = createMenuBar(true);
        WidgetOption[] a = getWidgetOptionList();
        for (int i = 0; i < a.length; i++) {
            bar.addItem(createAddMenuItem(a[i]));
        }
        return bar;
    }

    /**
     * Create a menu item to add a widget from an option.
     */
    private MenuItem createAddMenuItem(final WidgetOption op) {
        MenuItem mi = new MenuItem(op.getName(), new Command() {
            public void execute() {
                try {
                    op.createWidget(null, new WidgetCallback() {
                        public void onSuccess(Widget w) {
                            addWidget(w, op.isRefreshNeeded(w), op.getOverflow());
                        }
                    });
                } catch (Exception e) {
                    handleCreateWidgetFailure(op, e);
                }
            }
        });
        mi.setTitle("Add " + op.getDescription());
        return mi;
    }

    protected void handleCreateWidgetFailure(WidgetOption op, Exception e) {
        GWT.log(e.toString(), e);
        Window.alert("Error creating " + op.getName() + ":\n" + e);
    }

    /**
     * Add the widget to the layout with support for choosing where to
     * put it, undo and so on. If w is a container then the edit depth
     * is changed to its level when it is dropped.
     */
    protected void addWidget(Widget w, boolean refreshOnDrop, String overflow) {
        pickupOrAddWidget(null, w, "Add " + getWidgetDescription(w));
        drillDownOnDrop = w instanceof Container;
        this.refreshOnDrop = refreshOnDrop;
        this.overflow = overflow;
    }

    /**
     * Display the menu, hiding any other menu. If clientX and clientY are
     * less than zero then the current event is used to decide where to
     * display the menu.
     */
    protected void displayMenu(MenuBar bar, int clientX, int clientY) {
        hideMenu();
        new WidgetZIndexer().start();
        menuPopup = new PopupPanel(false);
        menuPopup.addCloseHandler(new CloseHandler<PopupPanel>() {
            public void onClose(CloseEvent<PopupPanel> event) {
                menuPopup = null;
            }
        });
        menuPopup.add(bar);
        if (clientX < 0 && clientY < 0) {
            Event ev = DOM.eventGetCurrentEvent();
            if (ev == null) {
                menuPopup.center();
                return;
            }
            clientX = DOM.eventGetClientX(ev) - 4;
            clientY = DOM.eventGetClientY(ev);
        }
        menuPopup.setPopupPosition(clientX, clientY);
        menuPopup.show();
    }

    /**
     * Hide our current menu.
     *
     * @see #displayMenu(com.google.gwt.user.client.ui.MenuBar, int, int) 
     */
    public void hideMenu() {
        if (menuPopup != null) {
            menuPopup.hide();
            menuTarget = null;
        }
    }

    public void onLayoutUpdated(LayoutEvent event) {
        // we may get a update notification from every one of our containers
        // in a single event cycle so schedule just one update for when
        // event processing is complete
        scheduleUpdate();
    }

    /**
     * Schedule an update for when event processing is complete if an update
     * is not already pending.
     */
    protected void scheduleUpdate() {
        if (!updatePending) {
            updatePending = true;
            DeferredCommand.addCommand(new Command() {
                public void execute() {
                    if (updatePending) {
                        updatePending = false;
                        update();
                    }
                }
            });
        }
    }

    /**
     * Cancel any pending update.
     */
    protected void cancelPendingUpdate() {
        updatePending = false;
    }

    /**
     * Move all the children of src to dest, preserving layout constraints if
     * possible and child ordering.
     */
    private void moveChildren(Container src, Container dest) {
        int wc = src.getWidgetCount();
        if (wc == 0) {
            return;
        }
        Widget[] wa = new Widget[wc];
        LayoutConstraints[] ca = new LayoutConstraints[wc];
        Layout layout = dest.getLayout();
        for (int i = wc - 1; i >= 0; i--) {
            Widget w = src.getWidget(i);
            wa[i] = w;
            ca[i] = layout.convertConstraints(src.getLayoutConstraints(w));
            src.remove(w);
        }
        for (int i = 0; i < wc; i++) {
            Widget w = wa[i];
            dest.add(w);
            dest.setLayoutConstraints(w, ca[i]);
        }
    }

    /**
     * Get our control dialog.
     */
    public EditorManagerControlDialog getControl() {
        return status;
    }

    /**
     * Override this to provide a custom WidgetReplacer.
     */
    protected WidgetReplacer createWidgetReplacer() {
        return new WidgetReplacer();
    }

    public WidgetReplacer getWidgetReplacer() {
        return widgetReplacer;
    }

    /**
     * Create the default widget editor used to edit widgets when no
     * other editor available.
     */
    protected DefaultWidgetEditor createDefaultWidgetEditor() {
        return new DefaultWidgetEditor();
    }

    /**
     * Create the editor used for CaptionPanels.
     */
    protected TitlePanelEditor createCaptionPanelEditor() {
        return new TitlePanelEditor();
    }

    /**
     * Create the editor used for LayoutPanels.
     */
    protected LayoutPanelEditor createLayoutPanelEditor() {
        return new LayoutPanelEditor();
    }

    /**
     * Get style names appropriate to widget. Include a null in the array
     * to allow the user to enter a style name.
     *
     * @see org.gwtportlets.portlet.client.edit.DefaultWidgetEditor
     */
    public String[] getStyleNamesFor(Widget widget) {
        return new String[] { null, "portlet-untitled", "portlet-borderless", };
    }

    /**
     * Display a dialog to edit widget. Highlights the widget and provides undo
     * support.
     */
    public void displayEditWidgetDialog(Widget w, PageEditorDialog dlg) {
        raiseOverlay();
        hideAllResizers();
        moveIndicator(heldIndicator, w);
        beginUndo("Edit " + getWidgetDescription(w));
        dlg.addCloseHandler(editDialogClosed);
        dlg.display();
    }

    private void refresh(final Widget w) {
        if (!(w instanceof WidgetFactoryProvider)) {
            return;
        }
        WidgetFactory wf = ((WidgetFactoryProvider) w).createWidgetFactory();
        LayoutUtil.clearDoNotSendToServerFields(wf);
        WidgetRefreshHook.App.get().refresh(w, wf, new AsyncCallback<WidgetFactory>() {
            public void onFailure(Throwable caught) {
                // what to do here?
            }

            public void onSuccess(WidgetFactory wf) {
                WidgetRefreshHook rh = WidgetRefreshHook.App.get();
                if (rh == null || !(rh instanceof HasWidgetFactoryEnabled)
                        || ((HasWidgetFactoryEnabled) rh).isWidgetFactoryEnabled(wf)) {
                    wf.refresh(w);
                }
            }
        });
    }

    /**
     * Display a dialog prompting the user to select a widget to create.
     * Invoke cb with the new widget.
     */
    protected void createWidget(Widget replacing, final AsyncCallback cb) {
        final SelectWidgetFactoryDialog dlg = new SelectWidgetFactoryDialog(LayoutUtil.getWidgetFactoryList());
        dlg.addCloseHandler(new CloseHandler<PopupPanel>() {
            public void onClose(CloseEvent<PopupPanel> event) {
                if (dlg.isOkPressed()) {
                    Widget w = dlg.getSelectedWidgetFactory().createWidgetFactory().createWidget();
                    refresh(w);
                    cb.onSuccess(w);
                }
            }
        });
        dlg.center();
    }

    /**
     * Replace our root widget with a new one.
     */
    protected void setRoot(Container root) {
        discardHeldWidget();
        widgetReplacer.replace((Widget) this.root, (Widget) root);
        this.root = root;
        update();
    }

    /**
     * Preserve the current state of the layout so an operation about to
     * be performed can be undone.
     *
     * @see #discardUndo() 
     */
    public void beginUndo(String description) {
        undoStack.push(new Op(description));
    }

    /**
     * Discard the most recently added undo information. Call this if the
     * operation fails to proceed for some reason and no changes are actually
     * made to the layout.
     *
     * @see #beginUndo(String)
     */
    public void discardUndo() {
        undoStack.discard();
    }

    /**
     * An undoable change to the layout.
     */
    public class Op extends UndoStack.Operation {

        private WidgetFactory state;

        /**
         * Create the operation, preserving the state of the layout for undo.
         * Do this before performing the undoable operation.
         */
        public Op(String description) {
            super(description);
            state = root.createWidgetFactory();
        }

        /**
         * Add this operation to the undo stack. Call this when the undoable
         * operation has been completed.
         */
        public void complete() {
            undoStack.push(this);
        }

        protected void undo() {
            swapState();
        }

        protected void redo() {
            swapState();
        }

        private void swapState() {
            Widget newRoot = state.createWidget();
            state.refresh(newRoot);
            state = root.createWidgetFactory();
            setRoot((Container) newRoot);
        }

    }

    /**
     * Used to create MenuItem's that create widgets for some purpose.
     * Override one of the createWidget methods.
     */
    public static abstract class WidgetOption {

        /**
         * Get a short user friendly name for this option (e.g. to use as text
         * for a menu item).
         */
        public abstract String getName();

        /**
         * Get a short user friendly description for this option (e.g. for use
         * as a tooltip).
         */
        public String getDescription() {
            return getName();
        }

        /**
         * Create a new widget and pass it to cb.onSuccess. If replacing is
         * not null then this is the widget being replaced. Pass null to
         * not create anything. It is ok to open dialogs and so on to select
         * the widget to create.
         */
        public void createWidget(Widget replacing, AsyncCallback cb) {
            cb.onSuccess(createWidget(replacing));
        }

        /**
         * Create a new widget. If replacing is not null then this is the widget
         * being replaced. Return null to not create anything.
         */
        public Widget createWidget(Widget replacing) {
            return null;
        }

        /**
         * Does this option create containers?
         */
        public boolean isContainerOption() {
            return false;
        }

        /**
         * Should an extra placeholder widget be added to the Container we
         * create when replacing a widget (i.e. containing it)?
         */
        public boolean isAddPlaceholderOnContain() {
            return isContainerOption();
        }

        /**
         * Does w (newly created) need to be refreshed (e.g. to get data
         * from the server) after being added to the layout?
         */
        public boolean isRefreshNeeded(Widget w) {
            return false;
        }

        /**
         * Get the preferred constraint overflow setting for widgets created
         * by this option.
         */
        public String getOverflow() {
            return RowLayout.Constraints.HIDDEN;
        }

    }

    private abstract static class WidgetCallback implements AsyncCallback {

        public final void onSuccess(Object result) {
            if (result instanceof Widget) {
                onSuccess((Widget) result);
            }
        }

        public abstract void onSuccess(Widget w);

        public void onFailure(Throwable caught) {
        }
    }

}