You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@isis.apache.org by da...@apache.org on 2012/12/08 15:56:02 UTC

[27/53] [partial] ISIS-188: making structure of component viewers consistent

http://git-wip-us.apache.org/repos/asf/isis/blob/eb613703/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/menu/PopupMenu.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/menu/PopupMenu.java b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/menu/PopupMenu.java
new file mode 100644
index 0000000..b8d9a47
--- /dev/null
+++ b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/menu/PopupMenu.java
@@ -0,0 +1,606 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.isis.viewer.dnd.view.menu;
+
+import java.awt.event.KeyEvent;
+import java.util.Vector;
+
+import org.apache.log4j.Logger;
+
+import org.apache.isis.core.commons.debug.DebugBuilder;
+import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
+import org.apache.isis.core.metamodel.consent.Consent;
+import org.apache.isis.core.metamodel.consent.Veto;
+import org.apache.isis.core.metamodel.spec.ActionType;
+import org.apache.isis.core.metamodel.spec.ObjectSpecification;
+import org.apache.isis.core.metamodel.spec.feature.ObjectAssociation;
+import org.apache.isis.viewer.dnd.drawing.Canvas;
+import org.apache.isis.viewer.dnd.drawing.Color;
+import org.apache.isis.viewer.dnd.drawing.ColorsAndFonts;
+import org.apache.isis.viewer.dnd.drawing.Image;
+import org.apache.isis.viewer.dnd.drawing.Location;
+import org.apache.isis.viewer.dnd.drawing.Padding;
+import org.apache.isis.viewer.dnd.drawing.Shape;
+import org.apache.isis.viewer.dnd.drawing.Size;
+import org.apache.isis.viewer.dnd.drawing.Text;
+import org.apache.isis.viewer.dnd.view.Axes;
+import org.apache.isis.viewer.dnd.view.Click;
+import org.apache.isis.viewer.dnd.view.Content;
+import org.apache.isis.viewer.dnd.view.FocusManager;
+import org.apache.isis.viewer.dnd.view.KeyboardAction;
+import org.apache.isis.viewer.dnd.view.Toolkit;
+import org.apache.isis.viewer.dnd.view.UserAction;
+import org.apache.isis.viewer.dnd.view.UserActionSet;
+import org.apache.isis.viewer.dnd.view.View;
+import org.apache.isis.viewer.dnd.view.ViewConstants;
+import org.apache.isis.viewer.dnd.view.ViewRequirement;
+import org.apache.isis.viewer.dnd.view.ViewSpecification;
+import org.apache.isis.viewer.dnd.view.Workspace;
+import org.apache.isis.viewer.dnd.view.base.AbstractView;
+import org.apache.isis.viewer.dnd.view.content.AbstractContent;
+import org.apache.isis.viewer.dnd.view.content.NullContent;
+import org.apache.isis.viewer.dnd.view.window.SubviewFocusManager;
+
+public class PopupMenu extends AbstractView {
+
+    private static class Item {
+        public static Item createDivider() {
+            final Item item = new Item();
+            item.isBlank = true;
+            return item;
+        }
+
+        public static Item createNoOption() {
+            final Item item = new Item();
+            item.name = "no options";
+            return item;
+        }
+
+        public static Item createOption(final UserAction action, final Object object, final View view, final Location location) {
+            final Item item = new Item();
+            if (action == null) {
+                item.isBlank = true;
+            } else {
+                item.isBlank = false;
+                item.action = action;
+                item.view = view;
+                item.name = action.getName(view);
+                item.description = action.getDescription(view);
+                final Consent consent = action.disabled(view);
+                item.isDisabled = consent.isVetoed();
+                item.reasonDisabled = consent.getReason();
+            }
+            return item;
+        }
+
+        UserAction action;
+        String description;
+        boolean isBlank;
+        boolean isDisabled;
+        String name;
+        String reasonDisabled;
+        View view;
+
+        private Item() {
+        }
+
+        public String getHelp() {
+            return action.getHelp(view);
+        }
+
+        @Override
+        public String toString() {
+            return isBlank ? "NONE" : (name + " " + (isDisabled ? "DISABLED " : " " + action));
+        }
+    }
+
+    private class PopupContent extends AbstractContent {
+
+        public PopupContent() {
+        }
+
+        @Override
+        public Consent canDrop(final Content sourceContent) {
+            return Veto.DEFAULT;
+        }
+
+        @Override
+        public void debugDetails(final DebugBuilder debug) {
+        }
+
+        @Override
+        public ObjectAdapter drop(final Content sourceContent) {
+            return null;
+        }
+
+        @Override
+        public String getDescription() {
+            final int optionNo = getOption();
+            return items[optionNo].description;
+        }
+
+        @Override
+        public String getHelp() {
+            final int optionNo = getOption();
+            return items[optionNo].getHelp();
+        }
+
+        @Override
+        public String getIconName() {
+            return null;
+        }
+
+        @Override
+        public Image getIconPicture(final int iconHeight) {
+            return null;
+        }
+
+        @Override
+        public String getId() {
+            return null;
+        }
+
+        @Override
+        public ObjectAdapter getAdapter() {
+            return null;
+        }
+
+        @Override
+        public boolean isOptionEnabled() {
+            return false;
+        }
+
+        @Override
+        public ObjectSpecification getSpecification() {
+            return null;
+        }
+
+        @Override
+        public boolean isTransient() {
+            return false;
+        }
+
+        public void parseTextEntry(final String entryText) {
+        }
+
+        @Override
+        public String title() {
+            final int optionNo = getOption();
+            return items[optionNo].name;
+        }
+
+        @Override
+        public ObjectAdapter[] getOptions() {
+            return null;
+        }
+    }
+
+    private static class PopupSpecification implements ViewSpecification {
+        @Override
+        public boolean canDisplay(final ViewRequirement requirement) {
+            return false;
+        }
+
+        @Override
+        public View createView(final Content content, final Axes axes, final int sequence) {
+            return null;
+        }
+
+        @Override
+        public String getName() {
+            return "Popup Menu";
+        }
+
+        @Override
+        public boolean isAligned() {
+            return false;
+        }
+
+        @Override
+        public boolean isOpen() {
+            return true;
+        }
+
+        @Override
+        public boolean isReplaceable() {
+            return false;
+        }
+
+        @Override
+        public boolean isResizeable() {
+            return false;
+        }
+
+        @Override
+        public boolean isSubView() {
+            return false;
+        }
+    }
+
+    private static final Logger LOG = Logger.getLogger(PopupMenu.class);
+    private Color backgroundColor;
+    private View forView;
+    private Item[] items = new Item[0];
+    private int optionIdentified;
+    private final FocusManager simpleFocusManager;
+
+    public PopupMenu(final PopupMenuContainer parent) {
+        super(new NullContent(), new PopupSpecification());
+        // REVIEW should this content be used as param 1 above?
+        setContent(new PopupContent());
+        setParent(parent);
+        simpleFocusManager = new SubviewFocusManager(this);
+    }
+
+    private void addItems(final View target, final UserAction[] options, final int len, final Vector<Item> list, final ActionType type) {
+        final int initialSize = list.size();
+        for (int i = 0; i < len; i++) {
+            if (options[i].getType() == type) {
+                if (initialSize > 0 && list.size() == initialSize) {
+                    list.addElement(Item.createDivider());
+                }
+                list.addElement(Item.createOption(options[i], null, target, getLocation()));
+            }
+        }
+    }
+
+    protected Color backgroundColor() {
+        return backgroundColor;
+    }
+
+    @Override
+    public Consent canChangeValue() {
+        return Veto.DEFAULT;
+    }
+
+    @Override
+    public boolean canFocus() {
+        return true;
+    }
+
+    protected Color disabledColor() {
+        return Toolkit.getColor(ColorsAndFonts.COLOR_MENU_DISABLED);
+    }
+
+    /**
+     * Draws the popup menu
+     * 
+     * @see java.awt.Component#paint(java.awt.Graphics)
+     */
+    @Override
+    public void draw(final Canvas canvas) {
+        final Size coreSize = getSize();
+        final int width = coreSize.getWidth();
+        final int height = coreSize.getHeight();
+        canvas.drawSolidRectangle(0, 0, width, height, backgroundColor);
+        canvas.draw3DRectangle(0, 0, width, height, backgroundColor, true);
+
+        final int itemHeight = style().getLineHeight() + ViewConstants.VPADDING;
+        // int baseLine = itemHeight / 2 + style().getAscent() / 2 +
+        // getPadding().getTop();
+        int baseLine = style().getAscent() + getPadding().getTop() + 1;
+        final int left = getPadding().getLeft();
+        for (int i = 0; i < items.length; i++) {
+            if (items[i].isBlank) {
+                final int y = baseLine - (style().getAscent() / 2);
+                canvas.drawLine(1, y, width - 2, y, backgroundColor.brighter());
+                canvas.drawLine(1, y - 1, width - 2, y - 1, backgroundColor.darker());
+            } else {
+                Color color;
+                if (items[i].isDisabled || items[i].action == null) {
+                    color = disabledColor();
+                } else if (getOption() == i) {
+                    final int top = getPadding().getTop() + i * itemHeight;
+                    final int depth = style().getLineHeight() + 2;
+                    canvas.drawSolidRectangle(2, top, width - 4, depth, backgroundColor.darker());
+                    canvas.draw3DRectangle(2, top, width - 4, depth + 1, backgroundColor.brighter(), false);
+                    // canvas.drawText(items[i].name, left, baseLine,
+                    // normalColor(), style());
+
+                    color = reversedColor();
+                } else {
+                    color = normalColor();
+                }
+                canvas.drawText(items[i].name, left, baseLine, color, style());
+                if (items[i].action instanceof UserActionSet) {
+                    Shape arrow;
+                    arrow = new Shape(0, 0);
+                    arrow.addVector(4, 4);
+                    arrow.addVector(-4, 4);
+                    canvas.drawSolidShape(arrow, width - 10, baseLine - 8, color);
+                }
+            }
+
+            baseLine += itemHeight;
+        }
+
+        // canvas.drawRectangleAround(this,
+        // Toolkit.getColor(ColorsAndFonts.COLOR_DEBUG_BOUNDS_VIEW));
+    }
+
+    @Override
+    public void firstClick(final Click click) {
+        if (click.button1() || click.button2()) {
+            mouseMoved(click.getLocation());
+            invoke();
+        }
+    }
+
+    @Override
+    public void focusLost() {
+    }
+
+    @Override
+    public void focusReceived() {
+    }
+
+    @Override
+    public FocusManager getFocusManager() {
+        return simpleFocusManager;
+    }
+
+    @Override
+    public Size getRequiredSize(final Size availableSpace) {
+        final Size size = new Size();
+
+        for (final Item item : items) {
+            final int itemWidth = item.isBlank ? 0 : style().stringWidth(item.name);
+            size.ensureWidth(itemWidth);
+            size.extendHeight(style().getLineHeight() + ViewConstants.VPADDING);
+        }
+
+        size.extend(getPadding());
+        size.extendWidth(ViewConstants.HPADDING * 2);
+        return size;
+    }
+
+    public int getOption() {
+        return optionIdentified;
+    }
+
+    public int getOptionPostion() {
+        final int itemHeight = style().getLineHeight() + ViewConstants.VPADDING;
+        return itemHeight * getOption();
+    }
+
+    public int getOptionCount() {
+        return items.length;
+    }
+
+    @Override
+    public Padding getPadding() {
+        final Padding in = super.getPadding();
+        in.extendTop(ViewConstants.VPADDING);
+        in.extendBottom(ViewConstants.VPADDING);
+        in.extendLeft(ViewConstants.HPADDING + 5);
+        in.extendRight(ViewConstants.HPADDING + 5);
+
+        return in;
+    }
+
+    @Override
+    public Workspace getWorkspace() {
+        return forView.getWorkspace();
+    }
+
+    @Override
+    public boolean hasFocus() {
+        return false;
+    }
+
+    private void invoke() {
+        final int option = getOption();
+        final Item item = items[option];
+        if (item.isBlank || item.action == null || item.action.disabled(forView).isVetoed()) {
+            return;
+
+        } else if (item.action instanceof UserActionSet) {
+            final UserAction[] menuOptions = ((UserActionSet) item.action).getUserActions();
+            ((PopupMenuContainer) getParent()).openSubmenu(menuOptions);
+        } else {
+            final Workspace workspace = getWorkspace();
+
+            final Location location = new Location(getAbsoluteLocation());
+            location.subtract(workspace.getView().getAbsoluteLocation());
+            final Padding padding = workspace.getView().getPadding();
+            location.move(-padding.getLeft(), -padding.getTop());
+
+            final int itemHeight = style().getLineHeight() + ViewConstants.VPADDING;
+            final int baseLine = itemHeight * option;
+            location.add(0, baseLine);
+
+            getParent().dispose();
+            LOG.debug("execute " + item.name + " on " + forView + " in " + workspace);
+            item.action.execute(workspace, forView, location);
+        }
+    }
+
+    @Override
+    public void keyPressed(final KeyboardAction key) {
+        final int keyCode = key.getKeyCode();
+
+        if (keyCode == KeyEvent.VK_ESCAPE) {
+            if (getParent() == null) {
+                dispose();
+            }
+
+            key.consume();
+
+        } else if (keyCode == KeyEvent.VK_ENTER) {
+            key.consume();
+            invoke();
+
+        } else if (keyCode == KeyEvent.VK_RIGHT && items[getOption()].action instanceof UserActionSet) {
+            key.consume();
+            invoke();
+
+        } else if (keyCode == KeyEvent.VK_UP) {
+            key.consume();
+            if (optionIdentified == 0) {
+                optionIdentified = items.length;
+            }
+
+            for (int i = optionIdentified - 1; i >= 0; i--) {
+                if (items[i].isBlank) {
+                    continue;
+                }
+                if (items[i].isDisabled) {
+                    continue;
+                }
+                setOption(i);
+                break;
+            }
+
+        } else if (keyCode == KeyEvent.VK_DOWN) {
+            key.consume();
+            if (optionIdentified == items.length - 1) {
+                optionIdentified = -1;
+            }
+
+            for (int i = optionIdentified + 1; i < items.length; i++) {
+                if (items[i].isBlank) {
+                    continue;
+                }
+                if (items[i].isDisabled) {
+                    continue;
+                }
+                setOption(i);
+                break;
+            }
+        }
+
+    }
+
+    @Override
+    public void keyReleased(final KeyboardAction action) {
+    }
+
+    @Override
+    public void keyTyped(final KeyboardAction action) {
+    }
+
+    /*
+     * @Override public void layout(final Size maximumSize) { coreSize = new
+     * Bounds(getCoreRequiredSize());
+     * 
+     * final int option = getOption(); final int itemHeight =
+     * style().getLineHeight() + VPADDING; int menuWidth = coreSize.getWidth();
+     * // Location menuLocation = new Location(menuWidth - 4, itemHeight *
+     * option); Location menuLocation = new Location(0, itemHeight * option);
+     * 
+     * if (submenu != null) { submenu.layout(maximumSize);
+     * submenu.setLocation(menuLocation);
+     * 
+     * //coreSize.setX(submenu.getSize().getWidth() - 4); //getLocation() }
+     * setSize(getMaximumSize()); }
+     */
+    public View makeView(final ObjectAdapter object, final ObjectAssociation field) throws CloneNotSupportedException {
+        throw new RuntimeException();
+    }
+
+    @Override
+    public void markDamaged() {
+        if (getParent() == null) {
+            super.markDamaged();
+        } else {
+            getParent().markDamaged();
+        }
+        // markDamaged(new Bounds(getAbsoluteLocation(), getSize()));
+        // ///getView().getBounds());
+    }
+
+    @Override
+    public void mouseMoved(final Location at) {
+        int option = (at.getY() - getPadding().getTop()) / (style().getLineHeight() + ViewConstants.VPADDING);
+        option = Math.max(option, 0);
+        option = Math.min(option, items.length - 1);
+        if (option >= 0 && optionIdentified != option) {
+            // LOG.debug("mouse over option " + option + " " + this);
+            setOption(option);
+            markDamaged();
+        }
+    }
+
+    protected Color normalColor() {
+        return Toolkit.getColor(ColorsAndFonts.COLOR_MENU);
+    }
+
+    protected Color reversedColor() {
+        return Toolkit.getColor(ColorsAndFonts.COLOR_MENU_REVERSED);
+    }
+
+    public void setOption(final int option) {
+        if (option != optionIdentified) {
+            optionIdentified = option;
+            markDamaged();
+            updateFeedback();
+        }
+    }
+
+    private void updateFeedback() {
+        final Item item = items[optionIdentified];
+        if (item.isBlank) {
+            getFeedbackManager().clearAction();
+        } else if (isEmpty(item.reasonDisabled)) {
+            getFeedbackManager().setAction(item.description == null ? "" : item.description);
+        } else {
+            getFeedbackManager().setAction(item.reasonDisabled);
+        }
+    }
+
+    private boolean isEmpty(final String str) {
+        return str == null || str.length() == 0;
+    }
+
+    void show(final View target, final UserAction[] options, final Color color) {
+        this.forView = target;
+
+        optionIdentified = 0;
+        backgroundColor = color;
+
+        final int len = options.length;
+        if (len == 0) {
+            items = new Item[] { Item.createNoOption() };
+        } else {
+            final Vector list = new Vector();
+            addItems(target, options, len, list, ActionType.USER);
+            addItems(target, options, len, list, ActionType.EXPLORATION);
+            addItems(target, options, len, list, ActionType.PROTOTYPE);
+            addItems(target, options, len, list, ActionType.DEBUG);
+            items = new Item[list.size()];
+            list.copyInto(items);
+        }
+
+        updateFeedback();
+    }
+
+    protected Text style() {
+        return Toolkit.getText(ColorsAndFonts.TEXT_MENU);
+    }
+
+    @Override
+    public String toString() {
+        return "PopupMenu [location=" + getLocation() + ",item=" + optionIdentified + ",itemCount=" + (items == null ? 0 : items.length) + "]";
+    }
+
+    protected boolean transparentBackground() {
+        return false;
+    }
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/eb613703/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/menu/PopupMenuContainer.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/menu/PopupMenuContainer.java b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/menu/PopupMenuContainer.java
new file mode 100644
index 0000000..1e2f29c
--- /dev/null
+++ b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/menu/PopupMenuContainer.java
@@ -0,0 +1,304 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.isis.viewer.dnd.view.menu;
+
+import java.awt.event.KeyEvent;
+import java.util.Enumeration;
+import java.util.Vector;
+
+import org.apache.isis.core.commons.debug.DebugBuilder;
+import org.apache.isis.core.metamodel.spec.ActionType;
+import org.apache.isis.viewer.dnd.drawing.Canvas;
+import org.apache.isis.viewer.dnd.drawing.Color;
+import org.apache.isis.viewer.dnd.drawing.ColorsAndFonts;
+import org.apache.isis.viewer.dnd.drawing.Location;
+import org.apache.isis.viewer.dnd.drawing.Size;
+import org.apache.isis.viewer.dnd.view.Click;
+import org.apache.isis.viewer.dnd.view.Content;
+import org.apache.isis.viewer.dnd.view.KeyboardAction;
+import org.apache.isis.viewer.dnd.view.MenuOptions;
+import org.apache.isis.viewer.dnd.view.Toolkit;
+import org.apache.isis.viewer.dnd.view.UserAction;
+import org.apache.isis.viewer.dnd.view.UserActionSet;
+import org.apache.isis.viewer.dnd.view.View;
+import org.apache.isis.viewer.dnd.view.base.AbstractView;
+import org.apache.isis.viewer.dnd.view.content.NullContent;
+import org.apache.isis.viewer.dnd.view.debug.DebugOption;
+
+public class PopupMenuContainer extends AbstractView {
+    private static final int MENU_OVERLAP = 4;
+    private static final UserAction DEBUG_OPTION = new DebugOption();
+    private PopupMenu menu;
+    private PopupMenu submenu;
+    private Color backgroundColor;
+    private final View target;
+    private final Vector options = new Vector();
+    private final Location at;
+    private boolean isLayoutInvalid;
+
+    public PopupMenuContainer(final View target, final Location at) {
+        super(new NullContent());
+        this.target = target;
+        this.at = at;
+        setLocation(at);
+        isLayoutInvalid = true;
+    }
+
+    @Override
+    public void debug(final DebugBuilder debug) {
+        super.debug(debug);
+        debug.appendTitle("Submenu");
+        debug.append(submenu);
+        debug.append("\n");
+    }
+
+    @Override
+    public void dispose() {
+        if (getParent() == null) {
+            super.dispose();
+            getViewManager().clearOverlayView(this);
+        } else {
+            getParent().dispose();
+        }
+    }
+
+    @Override
+    public Size getRequiredSize(final Size availableSpace) {
+        final Size size = menu.getRequiredSize(Size.createMax());
+        if (submenu != null) {
+            final Size subviewSize = submenu.getRequiredSize(Size.createMax());
+            size.extendWidth(subviewSize.getWidth() - MENU_OVERLAP);
+            size.ensureHeight(submenuOffset() + subviewSize.getHeight());
+        }
+        return size;
+    }
+
+    @Override
+    public void layout() {
+        if (isLayoutInvalid) {
+            menu.layout();
+            final Size menuSize = menu.getRequiredSize(Size.createMax());
+            menu.setSize(menuSize);
+            menu.setLocation(new Location(0, 0));
+
+            final Location containerLocation = new Location(at);
+            final Size bounds = getViewManager().getOverlaySize();
+            if (containerLocation.getX() < 0) {
+                containerLocation.setX(0);
+            } else if (containerLocation.getX() + menuSize.getWidth() > bounds.getWidth()) {
+                containerLocation.setX(bounds.getWidth() - menuSize.getWidth());
+            }
+
+            if (containerLocation.getY() < 0) {
+                containerLocation.setY(0);
+            } else if (containerLocation.getY() + menuSize.getHeight() > bounds.getHeight()) {
+                containerLocation.setY(bounds.getHeight() - menuSize.getHeight());
+            }
+
+            if (submenu != null) {
+                submenu.layout();
+                final Size submenuSize = submenu.getRequiredSize(Size.createMax());
+                submenu.setSize(submenuSize);
+
+                int submenuOffset = submenuOffset();
+                final Location menuLocation = new Location();
+
+                final int containerBottom = containerLocation.getY() + submenuOffset + submenuSize.getHeight();
+                if (containerBottom > bounds.getHeight()) {
+                    final int overstretch = containerBottom - bounds.getHeight();
+                    submenuOffset -= overstretch;
+                }
+                final Location submenuLocation = new Location(0, submenuOffset);
+
+                final boolean placeToLeft = at.getX() + menuSize.getWidth() + submenuSize.getWidth() < getViewManager().getOverlaySize().getWidth();
+                if (placeToLeft) {
+                    submenuLocation.setX(menuSize.getWidth() - MENU_OVERLAP);
+                } else {
+                    menuLocation.setX(submenuSize.getWidth() - MENU_OVERLAP);
+                    containerLocation.move(-submenu.getSize().getWidth() + MENU_OVERLAP, 0);
+                }
+
+                if (containerLocation.getY() + menuSize.getHeight() > bounds.getHeight()) {
+                    containerLocation.setY(bounds.getHeight() - menuSize.getHeight());
+                }
+
+                submenu.setLocation(submenuLocation); // // !
+                menu.setLocation(menuLocation); // / !
+
+            }
+
+            setLocation(containerLocation);
+
+        }
+    }
+
+    private int submenuOffset() {
+        return menu.getOptionPostion();
+    }
+
+    @Override
+    public void mouseMoved(final Location at) {
+        if (menu.getBounds().contains(at)) {
+            at.subtract(menu.getLocation());
+            menu.mouseMoved(at);
+        } else if (submenu != null && submenu.getBounds().contains(at)) {
+            at.subtract(submenu.getLocation());
+            submenu.mouseMoved(at);
+        }
+    }
+
+    public void show(final boolean forView, final boolean includeDebug, final boolean includeExploration, final boolean includePrototype) {
+        final boolean withExploration = getViewManager().isRunningAsExploration() && includeExploration;
+        final boolean withPrototype = getViewManager().isRunningAsPrototype() && includePrototype;
+
+        final UserActionSet optionSet = new UserActionSetImpl(withExploration, withPrototype, includeDebug, ActionType.USER);
+        if (forView) {
+            target.viewMenuOptions(optionSet);
+        } else {
+            target.contentMenuOptions(optionSet);
+        }
+        optionSet.add(DEBUG_OPTION);
+        final Enumeration e = options.elements();
+        while (e.hasMoreElements()) {
+            final MenuOptions element = (MenuOptions) e.nextElement();
+            element.menuOptions(optionSet);
+        }
+
+        menu = new PopupMenu(this);
+
+        backgroundColor = optionSet.getColor();
+        menu.show(target, optionSet.getUserActions(), backgroundColor);
+        getViewManager().setOverlayView(this);
+
+        if (target != null) {
+            final String status = changeStatus(target, forView, withExploration, includeDebug);
+            getFeedbackManager().setViewDetail(status);
+        }
+    }
+
+    private String changeStatus(final View over, final boolean forView, final boolean includeExploration, final boolean includeDebug) {
+        final StringBuffer status = new StringBuffer("Menu for ");
+        if (forView) {
+            status.append("view ");
+            status.append(over.getSpecification().getName());
+        } else {
+            status.append("object");
+            final Content content = over.getContent();
+            if (content != null) {
+                status.append(" '");
+                status.append(content.title());
+                status.append("'");
+            }
+
+        }
+        if (includeDebug || includeExploration) {
+            status.append(" (includes ");
+            if (includeExploration) {
+                status.append("exploration");
+            }
+            if (includeDebug) {
+                if (includeExploration) {
+                    status.append(" & ");
+                }
+                status.append("debug");
+            }
+            status.append(" options)");
+        }
+        return status.toString();
+    }
+
+    public void addMenuOptions(final MenuOptions options) {
+        this.options.addElement(options);
+    }
+
+    void openSubmenu(final UserAction[] options) {
+        markDamaged();
+
+        submenu = new PopupMenu(this);
+        submenu.setParent(this);
+        submenu.show(target, options, backgroundColor);
+        invalidateLayout();
+        final Size size = getRequiredSize(Size.createMax());
+        setSize(size);
+        layout();
+
+        isLayoutInvalid = false;
+
+        markDamaged();
+    }
+
+    @Override
+    public void keyPressed(final KeyboardAction key) {
+        if (submenu != null) {
+            final int keyCode = key.getKeyCode();
+            if (keyCode == KeyEvent.VK_ESCAPE) {
+                markDamaged();
+                invalidateLayout();
+                submenu = null;
+                key.consume();
+
+            } else if (getParent() != null && keyCode == KeyEvent.VK_LEFT) {
+                markDamaged();
+                invalidateLayout();
+                submenu = null;
+                key.consume();
+            } else {
+                submenu.keyPressed(key);
+            }
+        } else {
+            menu.keyPressed(key);
+        }
+    }
+
+    @Override
+    public void invalidateLayout() {
+        isLayoutInvalid = true;
+    }
+
+    @Override
+    public void draw(final Canvas canvas) {
+        super.draw(canvas);
+        if (menu != null) {
+            final Canvas menuCanvas = canvas.createSubcanvas(menu.getBounds());
+            menu.draw(menuCanvas);
+        }
+        if (submenu != null) {
+            final Canvas submenuCanvas = canvas.createSubcanvas(submenu.getBounds());
+            submenu.draw(submenuCanvas);
+        }
+
+        if (Toolkit.debug) {
+            canvas.drawRectangleAround(getBounds(), Toolkit.getColor(ColorsAndFonts.COLOR_DEBUG_BOUNDS_VIEW));
+        }
+    }
+
+    @Override
+    public void firstClick(final Click click) {
+        final Location location = click.getLocation();
+        if (menu.getBounds().contains(location)) {
+            click.subtract(menu.getLocation());
+            menu.firstClick(click);
+        } else if (submenu != null && submenu.getBounds().contains(location)) {
+            click.subtract(submenu.getLocation());
+            submenu.firstClick(click);
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/eb613703/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/menu/UserActionSetImpl.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/menu/UserActionSetImpl.java b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/menu/UserActionSetImpl.java
new file mode 100644
index 0000000..d24f8e3
--- /dev/null
+++ b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/menu/UserActionSetImpl.java
@@ -0,0 +1,170 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.isis.viewer.dnd.view.menu;
+
+import java.util.Vector;
+
+import org.apache.isis.core.commons.lang.ToString;
+import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
+import org.apache.isis.core.metamodel.consent.Allow;
+import org.apache.isis.core.metamodel.consent.Consent;
+import org.apache.isis.core.metamodel.spec.ActionType;
+import org.apache.isis.core.metamodel.spec.ObjectSpecification;
+import org.apache.isis.viewer.dnd.drawing.Color;
+import org.apache.isis.viewer.dnd.drawing.ColorsAndFonts;
+import org.apache.isis.viewer.dnd.drawing.Location;
+import org.apache.isis.viewer.dnd.view.Toolkit;
+import org.apache.isis.viewer.dnd.view.UserAction;
+import org.apache.isis.viewer.dnd.view.UserActionSet;
+import org.apache.isis.viewer.dnd.view.View;
+import org.apache.isis.viewer.dnd.view.Workspace;
+import org.apache.isis.viewer.dnd.view.action.OptionFactory;
+
+public class UserActionSetImpl implements UserActionSet {
+
+    private Color backgroundColor;
+
+    private final String groupName;
+    private final boolean includeDebug;
+    private final boolean includeExploration;
+    private final boolean includePrototype;
+    private final Vector options = new Vector();
+    private final ActionType type;
+
+    public UserActionSetImpl(final boolean includeExploration, final boolean includePrototype, final boolean includeDebug, final ActionType type) {
+        this("", type, includeExploration, includePrototype, includeDebug, Toolkit.getColor(ColorsAndFonts.COLOR_DEBUG_BASELINE));
+    }
+
+    private UserActionSetImpl(final String groupName, final UserActionSetImpl parent) {
+        this(groupName, parent, parent.getType());
+    }
+
+    private UserActionSetImpl(final String groupName, final UserActionSetImpl parent, final ActionType type) {
+        this(groupName, type, parent.includeExploration, parent.includePrototype, parent.includeDebug, parent.getColor());
+    }
+
+    private UserActionSetImpl(final String groupName, final ActionType type, final boolean includeExploration, final boolean includePrototype, final boolean includeDebug, final Color backgroundColor) {
+        this.groupName = groupName;
+        this.type = type;
+        this.includeExploration = includeExploration;
+        this.includePrototype = includePrototype;
+        this.includeDebug = includeDebug;
+        this.backgroundColor = backgroundColor;
+    }
+
+    @Override
+    public UserActionSet addNewActionSet(final String name) {
+        final UserActionSetImpl set = new UserActionSetImpl(name, this);
+        add(set);
+        return set;
+    }
+
+    @Override
+    public UserActionSet addNewActionSet(final String name, final ActionType type) {
+        final UserActionSetImpl set = new UserActionSetImpl(name, this, type);
+        add(set);
+        return set;
+    }
+
+    /**
+     * Add the specified option if it is of the right type for this menu.
+     */
+    @Override
+    public void add(final UserAction option) {
+        final ActionType section = option.getType();
+        if (section == ActionType.USER || (includeExploration && section == ActionType.EXPLORATION) || (includePrototype && section == ActionType.PROTOTYPE) || (includeDebug && section == ActionType.DEBUG)) {
+            options.addElement(option);
+        }
+    }
+
+    @Override
+    public Consent disabled(final View view) {
+        return Allow.DEFAULT;
+    }
+
+    @Override
+    public void execute(final Workspace workspace, final View view, final Location at) {
+    }
+
+    /**
+     * Returns the background colour for the menu
+     */
+    @Override
+    public Color getColor() {
+        return backgroundColor;
+    }
+
+    @Override
+    public String getDescription(final View view) {
+        return "";
+    }
+
+    @Override
+    public String getHelp(final View view) {
+        return "";
+    }
+
+    @Override
+    public UserAction[] getUserActions() {
+        final UserAction[] v = new UserAction[options.size()];
+        for (int i = 0; i < v.length; i++) {
+            v[i] = (UserAction) options.elementAt(i);
+        }
+        return v;
+    }
+
+    @Override
+    public String getName(final View view) {
+        return groupName;
+    }
+
+    @Override
+    public ActionType getType() {
+        return type;
+    }
+
+    /**
+     * Specifies the background colour for the menu
+     */
+    @Override
+    public void setColor(final Color color) {
+        backgroundColor = color;
+    }
+
+    @Override
+    public String toString() {
+        final ToString str = new ToString(this);
+        str.append("type", type);
+        for (int i = 0, size = options.size(); i < size; i++) {
+            str.append(((UserAction) options.elementAt(i)).getClass() + " ,");
+        }
+        return str.toString();
+    }
+
+    @Override
+    public void addCreateOptions(final ObjectSpecification specification) {
+        OptionFactory.addCreateOptions(specification, this);
+    }
+
+    @Override
+    public void addObjectMenuOptions(final ObjectAdapter object) {
+        OptionFactory.addObjectMenuOptions(object, this);
+    }
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/eb613703/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/message/DetailedMessageViewSpecification.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/message/DetailedMessageViewSpecification.java b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/message/DetailedMessageViewSpecification.java
new file mode 100644
index 0000000..0acae98
--- /dev/null
+++ b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/message/DetailedMessageViewSpecification.java
@@ -0,0 +1,186 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.isis.viewer.dnd.view.message;
+
+import java.util.StringTokenizer;
+
+import org.apache.isis.viewer.dnd.drawing.Canvas;
+import org.apache.isis.viewer.dnd.drawing.Color;
+import org.apache.isis.viewer.dnd.drawing.ColorsAndFonts;
+import org.apache.isis.viewer.dnd.drawing.Location;
+import org.apache.isis.viewer.dnd.drawing.Size;
+import org.apache.isis.viewer.dnd.drawing.Text;
+import org.apache.isis.viewer.dnd.view.Axes;
+import org.apache.isis.viewer.dnd.view.ButtonAction;
+import org.apache.isis.viewer.dnd.view.Content;
+import org.apache.isis.viewer.dnd.view.FocusManager;
+import org.apache.isis.viewer.dnd.view.Toolkit;
+import org.apache.isis.viewer.dnd.view.View;
+import org.apache.isis.viewer.dnd.view.ViewAreaType;
+import org.apache.isis.viewer.dnd.view.ViewRequirement;
+import org.apache.isis.viewer.dnd.view.ViewSpecification;
+import org.apache.isis.viewer.dnd.view.Workspace;
+import org.apache.isis.viewer.dnd.view.base.AbstractView;
+import org.apache.isis.viewer.dnd.view.border.ButtonBorder;
+import org.apache.isis.viewer.dnd.view.border.ScrollBorder;
+import org.apache.isis.viewer.dnd.view.control.AbstractButtonAction;
+import org.apache.isis.viewer.dnd.view.control.CancelAction;
+import org.apache.isis.viewer.dnd.view.debug.DebugOutput;
+
+public class DetailedMessageViewSpecification implements ViewSpecification {
+
+    @Override
+    public boolean canDisplay(final ViewRequirement requirement) {
+        final Content content = requirement.getContent();
+        return content instanceof MessageContent && ((MessageContent) content).getDetail() != null;
+    }
+
+    @Override
+    public String getName() {
+        return "Detailed Message";
+    }
+
+    @Override
+    public View createView(final Content content, final Axes axes, final int sequence) {
+        final ButtonAction actions[] = new ButtonAction[] { new AbstractButtonAction("Print...") {
+            @Override
+            public void execute(final Workspace workspace, final View view, final Location at) {
+                DebugOutput.print("Print exception", extract(view));
+            }
+        }, new AbstractButtonAction("Save...") {
+            @Override
+            public void execute(final Workspace workspace, final View view, final Location at) {
+                DebugOutput.saveToFile("Save exception", "Exception", extract(view));
+            }
+        }, new AbstractButtonAction("Copy") {
+            @Override
+            public void execute(final Workspace workspace, final View view, final Location at) {
+                DebugOutput.saveToClipboard(extract(view));
+            }
+        }, new CancelAction(),
+
+        };
+
+        final DetailedMessageView messageView = new DetailedMessageView(content, this);
+        return new ButtonBorder(actions, new ScrollBorder(messageView));
+    }
+
+    private String extract(final View view) {
+        final Content content = view.getContent();
+        final String message = ((MessageContent) content).getMessage();
+        final String heading = ((MessageContent) content).title();
+        final String detail = ((MessageContent) content).getDetail();
+
+        final StringBuffer text = new StringBuffer();
+        text.append(heading);
+        text.append("\n\n");
+        text.append(message);
+        text.append("\n\n");
+        text.append(detail);
+        text.append("\n\n");
+        return text.toString();
+    }
+
+    @Override
+    public boolean isAligned() {
+        return false;
+    }
+
+    @Override
+    public boolean isOpen() {
+        return true;
+    }
+
+    @Override
+    public boolean isReplaceable() {
+        return false;
+    }
+
+    @Override
+    public boolean isSubView() {
+        return false;
+    }
+
+    @Override
+    public boolean isResizeable() {
+        return true;
+    }
+}
+
+class DetailedMessageView extends AbstractView {
+    protected DetailedMessageView(final Content content, final ViewSpecification specification) {
+        super(content, specification);
+    }
+
+    @Override
+    public Size getRequiredSize(final Size availableSpace) {
+        final Size size = new Size();
+        size.extendHeight(Toolkit.getText(ColorsAndFonts.TEXT_TITLE).getTextHeight());
+        size.extendHeight(30);
+
+        final String message = ((MessageContent) getContent()).getMessage();
+        size.ensureWidth(500);
+        size.extendHeight(Toolkit.getText(ColorsAndFonts.TEXT_NORMAL).stringHeight(message, 500));
+        size.extendHeight(30);
+
+        final String detail = ((MessageContent) getContent()).getDetail();
+        final StringTokenizer st = new StringTokenizer(detail, "\n\r");
+        while (st.hasMoreTokens()) {
+            final String line = st.nextToken();
+            final Text text = Toolkit.getText(ColorsAndFonts.TEXT_NORMAL);
+            size.ensureWidth((line.startsWith("\t") ? 20 : 0) + text.stringWidth(line));
+            size.extendHeight(text.getTextHeight());
+        }
+
+        size.extend(40, 20);
+        return size;
+    }
+
+    @Override
+    public void draw(final Canvas canvas) {
+        super.draw(canvas);
+
+        final int left = 10;
+        final Text title = Toolkit.getText(ColorsAndFonts.TEXT_TITLE);
+        int y = 10 + title.getAscent();
+        final String message = ((MessageContent) getContent()).getMessage();
+        final String heading = ((MessageContent) getContent()).title();
+        final String detail = ((MessageContent) getContent()).getDetail();
+
+        final Color black = Toolkit.getColor(ColorsAndFonts.COLOR_BLACK);
+        canvas.drawText(heading, left, y, black, title);
+        y += title.getTextHeight();
+        final Text text = Toolkit.getText(ColorsAndFonts.TEXT_NORMAL);
+        canvas.drawText(message, left, y, 500, black, text);
+
+        y += text.stringHeight(message, 500);
+        canvas.drawText(detail, left, y, 1000, Toolkit.getColor(ColorsAndFonts.COLOR_PRIMARY1), text);
+    }
+
+    @Override
+    public ViewAreaType viewAreaType(final Location mouseLocation) {
+        return ViewAreaType.VIEW;
+    }
+
+    @Override
+    public void setFocusManager(final FocusManager focusManager) {
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/eb613703/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/message/ExceptionMessageContent.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/message/ExceptionMessageContent.java b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/message/ExceptionMessageContent.java
new file mode 100644
index 0000000..d43983b
--- /dev/null
+++ b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/message/ExceptionMessageContent.java
@@ -0,0 +1,191 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.isis.viewer.dnd.view.message;
+
+import org.apache.isis.core.commons.debug.DebugBuilder;
+import org.apache.isis.core.commons.exceptions.IsisApplicationException;
+import org.apache.isis.core.commons.lang.NameUtils;
+import org.apache.isis.core.commons.lang.ThrowableUtils;
+import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
+import org.apache.isis.core.metamodel.adapter.version.ConcurrencyException;
+import org.apache.isis.core.metamodel.consent.Consent;
+import org.apache.isis.core.metamodel.consent.Veto;
+import org.apache.isis.core.metamodel.spec.ObjectSpecification;
+import org.apache.isis.viewer.dnd.drawing.Image;
+import org.apache.isis.viewer.dnd.view.Content;
+import org.apache.isis.viewer.dnd.view.UserActionSet;
+
+public class ExceptionMessageContent implements MessageContent {
+
+    protected String message;
+    protected String name;
+    protected String trace;
+    protected String title;
+    private final String icon;
+
+    public ExceptionMessageContent(final Throwable error) {
+        String fullName = error.getClass().getName();
+        fullName = fullName.substring(fullName.lastIndexOf('.') + 1);
+        name = NameUtils.naturalName(fullName);
+        message = error.getMessage();
+        trace = ThrowableUtils.stackTraceFor(error);
+        if (trace.indexOf("\tat") != -1) {
+            trace = trace.substring(trace.indexOf("\tat"));
+        }
+
+        if (name == null) {
+            name = "";
+        }
+        if (message == null) {
+            message = "";
+        }
+        if (trace == null) {
+            trace = "";
+        }
+
+        if (error instanceof IsisApplicationException) {
+            title = "Application Error";
+            icon = "application-error";
+        } else if (error instanceof ConcurrencyException) {
+            title = "Concurrency Error";
+            icon = "concurrency-error";
+        } else {
+            title = "System Error";
+            icon = "system-error";
+        }
+
+    }
+
+    @Override
+    public String getMessage() {
+        return message;
+    }
+
+    @Override
+    public String getDetail() {
+        return trace;
+    }
+
+    @Override
+    public String getIconName() {
+        return icon;
+    }
+
+    @Override
+    public Consent canDrop(final Content sourceContent) {
+        return Veto.DEFAULT;
+    }
+
+    @Override
+    public void contentMenuOptions(final UserActionSet options) {
+    }
+
+    @Override
+    public void debugDetails(final DebugBuilder debug) {
+    }
+
+    @Override
+    public ObjectAdapter drop(final Content sourceContent) {
+        return null;
+    }
+
+    @Override
+    public String getDescription() {
+        return name;
+    }
+
+    @Override
+    public String getHelp() {
+        return "";
+    }
+
+    @Override
+    public Image getIconPicture(final int iconHeight) {
+        return null;
+    }
+
+    @Override
+    public String getId() {
+        return "message-exception";
+    }
+
+    @Override
+    public ObjectAdapter getAdapter() {
+        return null;
+    }
+
+    @Override
+    public ObjectAdapter[] getOptions() {
+        return null;
+    }
+
+    @Override
+    public ObjectSpecification getSpecification() {
+        return null;
+    }
+
+    @Override
+    public boolean isCollection() {
+        return false;
+    }
+
+    @Override
+    public boolean isObject() {
+        return false;
+    }
+
+    @Override
+    public boolean isOptionEnabled() {
+        return false;
+    }
+
+    @Override
+    public boolean isPersistable() {
+        return false;
+    }
+
+    @Override
+    public boolean isTransient() {
+        return false;
+    }
+
+    @Override
+    public boolean isTextParseable() {
+        return false;
+    }
+
+    public void parseTextEntry(final String entryText) {
+    }
+
+    @Override
+    public String title() {
+        return name;
+    }
+
+    @Override
+    public void viewMenuOptions(final UserActionSet options) {
+    }
+
+    @Override
+    public String windowTitle() {
+        return title;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/eb613703/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/message/MessageContent.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/message/MessageContent.java b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/message/MessageContent.java
new file mode 100644
index 0000000..b8e6c33
--- /dev/null
+++ b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/message/MessageContent.java
@@ -0,0 +1,28 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.isis.viewer.dnd.view.message;
+
+import org.apache.isis.viewer.dnd.view.Content;
+
+public interface MessageContent extends Content {
+    String getMessage();
+
+    String getDetail();
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/eb613703/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/message/MessageDialogSpecification.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/message/MessageDialogSpecification.java b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/message/MessageDialogSpecification.java
new file mode 100644
index 0000000..d7ec4d33
--- /dev/null
+++ b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/message/MessageDialogSpecification.java
@@ -0,0 +1,183 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.isis.viewer.dnd.view.message;
+
+import org.apache.isis.viewer.dnd.drawing.Canvas;
+import org.apache.isis.viewer.dnd.drawing.Color;
+import org.apache.isis.viewer.dnd.drawing.ColorsAndFonts;
+import org.apache.isis.viewer.dnd.drawing.Image;
+import org.apache.isis.viewer.dnd.drawing.ImageFactory;
+import org.apache.isis.viewer.dnd.drawing.Location;
+import org.apache.isis.viewer.dnd.drawing.Size;
+import org.apache.isis.viewer.dnd.drawing.Text;
+import org.apache.isis.viewer.dnd.view.Axes;
+import org.apache.isis.viewer.dnd.view.ButtonAction;
+import org.apache.isis.viewer.dnd.view.Content;
+import org.apache.isis.viewer.dnd.view.FocusManager;
+import org.apache.isis.viewer.dnd.view.Toolkit;
+import org.apache.isis.viewer.dnd.view.View;
+import org.apache.isis.viewer.dnd.view.ViewAreaType;
+import org.apache.isis.viewer.dnd.view.ViewRequirement;
+import org.apache.isis.viewer.dnd.view.ViewSpecification;
+import org.apache.isis.viewer.dnd.view.Workspace;
+import org.apache.isis.viewer.dnd.view.base.AbstractView;
+import org.apache.isis.viewer.dnd.view.border.ButtonBorder;
+import org.apache.isis.viewer.dnd.view.border.ScrollBorder;
+import org.apache.isis.viewer.dnd.view.control.AbstractButtonAction;
+import org.apache.isis.viewer.dnd.view.window.SubviewFocusManager;
+
+public class MessageDialogSpecification implements ViewSpecification {
+
+    @Override
+    public boolean canDisplay(final ViewRequirement requirement) {
+        return requirement.getContent() instanceof MessageContent;
+    }
+
+    @Override
+    public String getName() {
+        return "Message Dialog";
+    }
+
+    @Override
+    public View createView(final Content content, final Axes axes, final int sequence) {
+        final ButtonAction actions[] = new ButtonAction[] { new CloseViewAction() };
+        final MessageView messageView = new MessageView((MessageContent) content, this);
+        final View dialogView = new ButtonBorder(actions, new ScrollBorder(messageView));
+        dialogView.setFocusManager(new SubviewFocusManager(dialogView));
+        return dialogView;
+    }
+
+    @Override
+    public boolean isAligned() {
+        return false;
+    }
+
+    @Override
+    public boolean isOpen() {
+        return true;
+    }
+
+    @Override
+    public boolean isReplaceable() {
+        return false;
+    }
+
+    @Override
+    public boolean isResizeable() {
+        return true;
+    }
+
+    @Override
+    public boolean isSubView() {
+        return false;
+    }
+
+    public static class CloseViewAction extends AbstractButtonAction {
+        public CloseViewAction() {
+            super("Close");
+        }
+
+        @Override
+        public void execute(final Workspace workspace, final View view, final Location at) {
+            view.dispose();
+        }
+    }
+}
+
+class MessageView extends AbstractView {
+    private static final int MAX_TEXT_WIDTH = 400;
+    private static final int LEFT = 20;
+    private static final int RIGHT = 20;
+    private static final int TOP = 15;
+    private static final int PADDING = 10;
+    private Image errorIcon;
+    private FocusManager focusManager;
+
+    protected MessageView(final MessageContent content, final ViewSpecification specification) {
+        super(content, specification);
+        final String iconName = ((MessageContent) getContent()).getIconName();
+        errorIcon = ImageFactory.getInstance().loadIcon(iconName, 32, null);
+        if (errorIcon == null) {
+            errorIcon = ImageFactory.getInstance().loadDefaultIcon(32, null);
+        }
+    }
+
+    @Override
+    public Size getRequiredSize(final Size availableSpace) {
+        final Size size = new Size();
+
+        final String message = ((MessageContent) getContent()).getMessage();
+        final String heading = ((MessageContent) getContent()).title();
+
+        size.ensureHeight(errorIcon.getHeight());
+        final Text text = Toolkit.getText(ColorsAndFonts.TEXT_NORMAL);
+        final Text titleText = Toolkit.getText(ColorsAndFonts.TEXT_TITLE);
+        size.extendWidth(text.stringWidth(message, MAX_TEXT_WIDTH));
+        int textHeight = titleText.getLineHeight();
+        textHeight += text.stringHeight(message, MAX_TEXT_WIDTH);
+        size.ensureHeight(textHeight);
+
+        size.ensureWidth(titleText.stringWidth(heading));
+
+        size.extendWidth(errorIcon.getWidth());
+        size.extendWidth(PADDING);
+
+        size.extend(LEFT + RIGHT, TOP * 2);
+        return size;
+    }
+
+    @Override
+    public void draw(final Canvas canvas) {
+        super.draw(canvas);
+
+        final String message = ((MessageContent) getContent()).getMessage();
+        final String heading = ((MessageContent) getContent()).title();
+
+        clearBackground(canvas, Toolkit.getColor(ColorsAndFonts.COLOR_WHITE));
+
+        canvas.drawImage(errorIcon, LEFT, TOP);
+
+        final int x = LEFT + errorIcon.getWidth() + PADDING;
+        int y = TOP + 3 + Toolkit.getText(ColorsAndFonts.TEXT_NORMAL).getAscent();
+        final Color black = Toolkit.getColor(ColorsAndFonts.COLOR_BLACK);
+        if (!heading.equals("")) {
+            final Text title = Toolkit.getText(ColorsAndFonts.TEXT_TITLE);
+            canvas.drawText(heading, x, y, black, title);
+            y += title.getLineHeight();
+        }
+        canvas.drawText(message, x, y, MAX_TEXT_WIDTH, black, Toolkit.getText(ColorsAndFonts.TEXT_NORMAL));
+    }
+
+    @Override
+    public ViewAreaType viewAreaType(final Location mouseLocation) {
+        return ViewAreaType.VIEW;
+    }
+
+    @Override
+    public FocusManager getFocusManager() {
+        return focusManager == null ? super.getFocusManager() : focusManager;
+    }
+
+    @Override
+    public void setFocusManager(final FocusManager focusManager) {
+        this.focusManager = focusManager;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/eb613703/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/message/TextMessageContent.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/message/TextMessageContent.java b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/message/TextMessageContent.java
new file mode 100644
index 0000000..189fa9d
--- /dev/null
+++ b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/message/TextMessageContent.java
@@ -0,0 +1,165 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.isis.viewer.dnd.view.message;
+
+import org.apache.isis.core.commons.debug.DebugBuilder;
+import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
+import org.apache.isis.core.metamodel.consent.Consent;
+import org.apache.isis.core.metamodel.consent.Veto;
+import org.apache.isis.core.metamodel.spec.ObjectSpecification;
+import org.apache.isis.viewer.dnd.drawing.Image;
+import org.apache.isis.viewer.dnd.view.Content;
+import org.apache.isis.viewer.dnd.view.UserActionSet;
+
+public class TextMessageContent implements MessageContent {
+    protected final String message;
+    protected final String heading;
+    protected final String detail;
+    protected final String title;
+
+    public TextMessageContent(final String title, final String message) {
+        final int pos = message.indexOf(':');
+        if (pos > 2) {
+            this.heading = message.substring(0, pos).trim();
+            this.message = message.substring(pos + 1).trim();
+        } else {
+            this.heading = "";
+            this.message = message;
+        }
+        this.title = title;
+        this.detail = null;
+    }
+
+    @Override
+    public String getMessage() {
+        return message;
+    }
+
+    @Override
+    public String getDetail() {
+        return detail;
+    }
+
+    @Override
+    public Consent canDrop(final Content sourceContent) {
+        return Veto.DEFAULT;
+    }
+
+    @Override
+    public void contentMenuOptions(final UserActionSet options) {
+    }
+
+    @Override
+    public void debugDetails(final DebugBuilder debug) {
+    }
+
+    @Override
+    public ObjectAdapter drop(final Content sourceContent) {
+        return null;
+    }
+
+    @Override
+    public String getDescription() {
+        return title;
+    }
+
+    @Override
+    public String getHelp() {
+        return "";
+    }
+
+    @Override
+    public String getIconName() {
+        return "message";
+    }
+
+    @Override
+    public Image getIconPicture(final int iconHeight) {
+        return null;
+    }
+
+    @Override
+    public String getId() {
+        return "message-exception";
+    }
+
+    @Override
+    public ObjectAdapter getAdapter() {
+        return null;
+    }
+
+    @Override
+    public ObjectAdapter[] getOptions() {
+        return null;
+    }
+
+    @Override
+    public ObjectSpecification getSpecification() {
+        return null;
+    }
+
+    @Override
+    public boolean isCollection() {
+        return false;
+    }
+
+    @Override
+    public boolean isObject() {
+        return false;
+    }
+
+    @Override
+    public boolean isPersistable() {
+        return false;
+    }
+
+    @Override
+    public boolean isOptionEnabled() {
+        return false;
+    }
+
+    @Override
+    public boolean isTransient() {
+        return false;
+    }
+
+    @Override
+    public boolean isTextParseable() {
+        return false;
+    }
+
+    public void parseTextEntry(final String entryText) {
+    }
+
+    @Override
+    public String title() {
+        return heading;
+    }
+
+    @Override
+    public void viewMenuOptions(final UserActionSet options) {
+    }
+
+    @Override
+    public String windowTitle() {
+        return title;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/eb613703/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/option/CloseAllViewsForObjectOption.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/option/CloseAllViewsForObjectOption.java b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/option/CloseAllViewsForObjectOption.java
new file mode 100644
index 0000000..3e76ba3
--- /dev/null
+++ b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/option/CloseAllViewsForObjectOption.java
@@ -0,0 +1,49 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.isis.viewer.dnd.view.option;
+
+import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
+import org.apache.isis.viewer.dnd.drawing.Location;
+import org.apache.isis.viewer.dnd.view.View;
+import org.apache.isis.viewer.dnd.view.Workspace;
+
+public class CloseAllViewsForObjectOption extends UserActionAbstract {
+    public CloseAllViewsForObjectOption() {
+        super("Close all views for this object");
+    }
+
+    @Override
+    public void execute(final Workspace workspace, final View view, final Location at) {
+        final ObjectAdapter object = view.getContent().getAdapter();
+        final View views[] = workspace.getSubviews();
+        for (final View v : views) {
+            if (v.getContent().getAdapter() == object) {
+                v.dispose();
+            }
+        }
+    }
+
+    @Override
+    public String getDescription(final View view) {
+        final String title = view.getContent().title();
+        return "Close all views for '" + title + "'";
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/eb613703/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/option/CloseAllViewsOption.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/option/CloseAllViewsOption.java b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/option/CloseAllViewsOption.java
new file mode 100644
index 0000000..89e7fca
--- /dev/null
+++ b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/option/CloseAllViewsOption.java
@@ -0,0 +1,52 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.isis.viewer.dnd.view.option;
+
+import org.apache.isis.core.commons.lang.ToString;
+import org.apache.isis.viewer.dnd.drawing.Location;
+import org.apache.isis.viewer.dnd.view.View;
+import org.apache.isis.viewer.dnd.view.Workspace;
+
+public class CloseAllViewsOption extends UserActionAbstract {
+    public CloseAllViewsOption() {
+        super("Close all others");
+    }
+
+    @Override
+    public void execute(final Workspace workspace, final View view, final Location at) {
+        final View views[] = view.getWorkspace().getSubviews();
+
+        for (final View otherView : views) {
+            if (otherView.getSpecification().isOpen() && otherView != view) {
+                otherView.dispose();
+            }
+        }
+    }
+
+    @Override
+    public String getDescription(final View view) {
+        return "Close all views except " + view.getSpecification().getName().toLowerCase();
+    }
+
+    @Override
+    public String toString() {
+        return new ToString(this).toString();
+    }
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/eb613703/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/option/CloseOtherViewsForObjectOption.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/option/CloseOtherViewsForObjectOption.java b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/option/CloseOtherViewsForObjectOption.java
new file mode 100644
index 0000000..44d254d
--- /dev/null
+++ b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/option/CloseOtherViewsForObjectOption.java
@@ -0,0 +1,49 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.isis.viewer.dnd.view.option;
+
+import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
+import org.apache.isis.viewer.dnd.drawing.Location;
+import org.apache.isis.viewer.dnd.view.View;
+import org.apache.isis.viewer.dnd.view.Workspace;
+
+public class CloseOtherViewsForObjectOption extends UserActionAbstract {
+    public CloseOtherViewsForObjectOption() {
+        super("Close other views for this object");
+    }
+
+    @Override
+    public void execute(final Workspace workspace, final View view, final Location at) {
+        final ObjectAdapter object = view.getContent().getAdapter();
+        final View views[] = workspace.getSubviews();
+        for (final View v : views) {
+            if (view != v && v.getContent().getAdapter() == object) {
+                v.dispose();
+            }
+        }
+    }
+
+    @Override
+    public String getDescription(final View view) {
+        final String title = view.getContent().title();
+        return "Close other views for '" + title + "'";
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/eb613703/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/option/CloseViewOption.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/option/CloseViewOption.java b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/option/CloseViewOption.java
new file mode 100644
index 0000000..7a222d8
--- /dev/null
+++ b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/option/CloseViewOption.java
@@ -0,0 +1,46 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.isis.viewer.dnd.view.option;
+
+import org.apache.isis.core.commons.lang.ToString;
+import org.apache.isis.viewer.dnd.drawing.Location;
+import org.apache.isis.viewer.dnd.view.View;
+import org.apache.isis.viewer.dnd.view.Workspace;
+
+public class CloseViewOption extends UserActionAbstract {
+    public CloseViewOption() {
+        super("Close");
+    }
+
+    @Override
+    public void execute(final Workspace workspace, final View view, final Location at) {
+        view.dispose();
+    }
+
+    @Override
+    public String getDescription(final View view) {
+        return "Close this " + view.getSpecification().getName();
+    }
+
+    @Override
+    public String toString() {
+        return new ToString(this).toString();
+    }
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/eb613703/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/option/DisposeObjectOption.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/option/DisposeObjectOption.java b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/option/DisposeObjectOption.java
new file mode 100644
index 0000000..806d293
--- /dev/null
+++ b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/option/DisposeObjectOption.java
@@ -0,0 +1,101 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.isis.viewer.dnd.view.option;
+
+import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
+import org.apache.isis.core.metamodel.consent.Allow;
+import org.apache.isis.core.metamodel.consent.Consent;
+import org.apache.isis.core.metamodel.consent.Veto;
+import org.apache.isis.core.metamodel.spec.ActionType;
+import org.apache.isis.runtimes.dflt.runtime.system.context.IsisContext;
+import org.apache.isis.runtimes.dflt.runtime.system.persistence.Persistor;
+import org.apache.isis.runtimes.dflt.runtime.system.transaction.UpdateNotifier;
+import org.apache.isis.viewer.dnd.drawing.Location;
+import org.apache.isis.viewer.dnd.view.ObjectContent;
+import org.apache.isis.viewer.dnd.view.View;
+import org.apache.isis.viewer.dnd.view.Workspace;
+
+/**
+ * Destroy this object
+ */
+public class DisposeObjectOption extends UserActionAbstract {
+    public DisposeObjectOption() {
+        super("Dispose Object", ActionType.EXPLORATION);
+    }
+
+    @Override
+    public Consent disabled(final View view) {
+        final ObjectAdapter adapter = view.getContent().getAdapter();
+        if (adapter.isDestroyed()) {
+            // TODO: move logic into Facet
+            return new Veto("Can't do anything with a destroyed object");
+        }
+        if (isObjectInRootView(view)) {
+            return Allow.DEFAULT;
+        } else {
+            // TODO: move logic into Facet
+            return new Veto("Can't dispose an object from within another view.");
+        }
+    }
+
+    private boolean isObjectInRootView(final View view) {
+        final View rootView = rootView(view);
+        return view.getContent() == rootView.getContent();
+    }
+
+    private View rootView(final View view) {
+        final View parent = view.getParent();
+        if (view.getWorkspace() == parent) {
+            return view;
+        } else {
+            return rootView(parent);
+        }
+    }
+
+    @Override
+    public void execute(final Workspace workspace, final View view, final Location at) {
+
+        final ObjectAdapter object = ((ObjectContent) view.getContent()).getObject();
+
+        // xactn mgmt now done by PersistenceSession#destroyObject()
+        // getTransactionManager().startTransaction();
+
+        getPersistenceSession().destroyObject(object);
+
+        // getTransactionManager().endTransaction();
+
+        getUpdateNotifier().addDisposedObject(object);
+        view.getViewManager().disposeUnneededViews();
+        view.getFeedbackManager().showMessagesAndWarnings();
+    }
+
+    // /////////////////////////////////////////////////////
+    // Dependencies (from context)
+    // /////////////////////////////////////////////////////
+
+    private static Persistor getPersistenceSession() {
+        return IsisContext.getPersistenceSession();
+    }
+
+    private static UpdateNotifier getUpdateNotifier() {
+        return IsisContext.getUpdateNotifier();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/eb613703/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/option/IconizeViewOption.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/option/IconizeViewOption.java b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/option/IconizeViewOption.java
new file mode 100644
index 0000000..7581211
--- /dev/null
+++ b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/option/IconizeViewOption.java
@@ -0,0 +1,66 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.isis.viewer.dnd.view.option;
+
+import org.apache.isis.core.metamodel.consent.Allow;
+import org.apache.isis.core.metamodel.consent.Consent;
+import org.apache.isis.viewer.dnd.drawing.Location;
+import org.apache.isis.viewer.dnd.view.Toolkit;
+import org.apache.isis.viewer.dnd.view.View;
+import org.apache.isis.viewer.dnd.view.Workspace;
+
+public class IconizeViewOption extends UserActionAbstract {
+    public IconizeViewOption() {
+        super("Iconize");
+    }
+
+    @Override
+    public Consent disabled(final View view) {
+        return Allow.DEFAULT;
+    }
+
+    @Override
+    public void execute(final Workspace workspace, final View view, final Location at) {
+        final View minimizedView = Toolkit.getViewFactory().createMinimizedView(view);
+        minimizedView.setLocation(view.getLocation());
+        final View[] views = workspace.getSubviews();
+        for (final View view2 : views) {
+            if (view2 == view) {
+                workspace.removeView(view);
+                workspace.addView(minimizedView);
+                workspace.invalidateLayout();
+                return;
+            }
+        }
+
+        /*
+         * // TODO change so that an iconized version of the window is created
+         * and displayed, which holds the original view. View iconView = new
+         * RootIconSpecification().createView(view.getContent(), null);
+         * iconView.setLocation(view.getLocation()); workspace.replaceView(view,
+         * iconView);
+         */
+    }
+
+    @Override
+    public String getDescription(final View view) {
+        return "Show this object as an icon on the workspace";
+    }
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/eb613703/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/option/OpenViewOption.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/option/OpenViewOption.java b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/option/OpenViewOption.java
new file mode 100644
index 0000000..2127720
--- /dev/null
+++ b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/option/OpenViewOption.java
@@ -0,0 +1,64 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.isis.viewer.dnd.view.option;
+
+import org.apache.log4j.Logger;
+
+import org.apache.isis.viewer.dnd.drawing.Location;
+import org.apache.isis.viewer.dnd.view.Content;
+import org.apache.isis.viewer.dnd.view.Placement;
+import org.apache.isis.viewer.dnd.view.Toolkit;
+import org.apache.isis.viewer.dnd.view.View;
+import org.apache.isis.viewer.dnd.view.ViewSpecification;
+import org.apache.isis.viewer.dnd.view.Workspace;
+import org.apache.isis.viewer.dnd.view.content.FieldContent;
+
+public class OpenViewOption extends UserActionAbstract {
+    private static final Logger LOG = Logger.getLogger(OpenViewOption.class);
+    private final ViewSpecification specification;
+
+    public OpenViewOption(final ViewSpecification builder) {
+        super(builder.getName());
+        this.specification = builder;
+    }
+
+    @Override
+    public void execute(final Workspace workspace, final View view, final Location at) {
+        Content content = view.getContent();
+        if (content.getAdapter() != null && !(content instanceof FieldContent)) {
+            content = Toolkit.getContentFactory().createRootContent(content.getAdapter());
+        }
+        final View newView = specification.createView(content, view.getViewAxes(), -1);
+        LOG.debug("open view " + newView);
+        workspace.addWindow(newView, new Placement(view));
+        workspace.markDamaged();
+    }
+
+    @Override
+    public String getDescription(final View view) {
+        final String title = view.getContent().title();
+        return "Open '" + title + "' in a " + specification.getName() + " window";
+    }
+
+    @Override
+    public String toString() {
+        return super.toString() + " [prototype=" + specification.getName() + "]";
+    }
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/eb613703/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/option/ReplaceViewOption.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/option/ReplaceViewOption.java b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/option/ReplaceViewOption.java
new file mode 100644
index 0000000..6cdb8c3
--- /dev/null
+++ b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/option/ReplaceViewOption.java
@@ -0,0 +1,60 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.isis.viewer.dnd.view.option;
+
+import org.apache.log4j.Logger;
+
+import org.apache.isis.viewer.dnd.drawing.Location;
+import org.apache.isis.viewer.dnd.view.Axes;
+import org.apache.isis.viewer.dnd.view.View;
+import org.apache.isis.viewer.dnd.view.ViewSpecification;
+import org.apache.isis.viewer.dnd.view.Workspace;
+
+public class ReplaceViewOption extends UserActionAbstract {
+    private static final Logger LOG = Logger.getLogger(ReplaceViewOption.class);
+    private final ViewSpecification specification;
+
+    public ReplaceViewOption(final ViewSpecification specification) {
+        super(specification.getName());
+        this.specification = specification;
+    }
+
+    @Override
+    public String getDescription(final View view) {
+        return "Replace this " + view.getSpecification().getName() + " view with a " + specification.getName() + " view";
+    }
+
+    @Override
+    public void execute(final Workspace workspace, final View view, final Location at) {
+        final View replacement = specification.createView(view.getContent(), new Axes(), -1);
+        LOG.debug("replacement view " + replacement);
+        replace(view, replacement);
+    }
+
+    protected void replace(final View view, final View withReplacement) {
+        final View existingView = view.getView();
+        view.getParent().replaceView(existingView, withReplacement);
+    }
+
+    @Override
+    public String toString() {
+        return super.toString() + " [prototype=" + specification.getName() + "]";
+    }
+}