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:01 UTC

[21/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/border/LabelBorder.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/LabelBorder.java b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/LabelBorder.java
new file mode 100644
index 0000000..305d8d6
--- /dev/null
+++ b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/LabelBorder.java
@@ -0,0 +1,148 @@
+/*
+ *  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.border;
+
+import org.apache.isis.core.commons.debug.DebugBuilder;
+import org.apache.isis.core.commons.lang.ToString;
+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.Text;
+import org.apache.isis.viewer.dnd.view.Toolkit;
+import org.apache.isis.viewer.dnd.view.View;
+import org.apache.isis.viewer.dnd.view.ViewConstants;
+import org.apache.isis.viewer.dnd.view.action.ParameterContent;
+import org.apache.isis.viewer.dnd.view.axis.LabelAxis;
+import org.apache.isis.viewer.dnd.view.base.AbstractBorder;
+import org.apache.isis.viewer.dnd.view.content.FieldContent;
+
+public class LabelBorder extends AbstractBorder {
+    public static final int NORMAL = 0;
+    public static final int DISABLED = 1;
+    public static final int MANDATORY = 2;
+
+    public static View createFieldLabelBorder(final LabelAxis axis, final View wrappedView) {
+        final FieldContent fieldContent = (FieldContent) wrappedView.getContent();
+        return new LabelBorder(fieldContent, axis, wrappedView);
+    }
+
+    public static View createValueParameterLabelBorder(final LabelAxis axis, final View wrappedView) {
+        final ParameterContent fieldContent = (ParameterContent) wrappedView.getContent();
+        return new LabelBorder(fieldContent, axis, wrappedView);
+    }
+
+    private final String label;
+    private Text style;
+    private Color color;
+    private final LabelAxis axis;
+
+    protected LabelBorder(final FieldContent fieldContent, final LabelAxis axis, final View wrappedView) {
+        super(wrappedView);
+        this.axis = axis;
+        if (fieldContent.isEditable().isVetoed()) {
+            setDisabledStyling();
+        } else if (fieldContent.isMandatory()) {
+            setMandatoryStyling();
+        } else {
+            setOptionalStyling();
+        }
+
+        final String name = fieldContent.getFieldName();
+        this.label = name + ":";
+
+        final int width = ViewConstants.HPADDING + style.stringWidth(this.label) + ViewConstants.HPADDING;
+        if (axis == null) {
+            left = width;
+        } else {
+            axis.accommodateWidth(width);
+        }
+    }
+
+    protected LabelBorder(final ParameterContent fieldContent, final LabelAxis axis, final View wrappedView) {
+        super(wrappedView);
+        this.axis = axis;
+        if (fieldContent.isRequired()) {
+            setMandatoryStyling();
+        } else {
+            setOptionalStyling();
+        }
+
+        final String name = fieldContent.getParameterName();
+        this.label = name + ":";
+
+        final int width = ViewConstants.HPADDING + style.stringWidth(this.label) + ViewConstants.HPADDING;
+        if (axis == null) {
+            left = width;
+        } else {
+            axis.accommodateWidth(width);
+        }
+    }
+
+    private void setOptionalStyling() {
+        style = Toolkit.getText(ColorsAndFonts.TEXT_LABEL);
+        color = Toolkit.getColor(ColorsAndFonts.COLOR_LABEL);
+    }
+
+    private void setMandatoryStyling() {
+        style = Toolkit.getText(ColorsAndFonts.TEXT_LABEL_MANDATORY);
+        color = Toolkit.getColor(ColorsAndFonts.COLOR_LABEL_MANDATORY);
+    }
+
+    private void setDisabledStyling() {
+        style = Toolkit.getText(ColorsAndFonts.TEXT_LABEL_DISABLED);
+        color = Toolkit.getColor(ColorsAndFonts.COLOR_LABEL_DISABLED);
+    }
+
+    @Override
+    protected int getLeft() {
+        if (axis == null) {
+            return left;
+        } else {
+            return axis.getWidth();
+        }
+    }
+
+    @Override
+    public void debugDetails(final DebugBuilder debug) {
+        super.debugDetails(debug);
+        debug.appendln("label", "'" + label + "'");
+        debug.appendln("axis", axis);
+    }
+
+    @Override
+    public void draw(final Canvas canvas) {
+        final Color color = textColor();
+        canvas.drawText(label, left, wrappedView.getBaseline(), color, style);
+        super.draw(canvas);
+    }
+
+    protected Color textColor() {
+        return color;
+    }
+
+    @Override
+    public String toString() {
+        return wrappedView.toString() + "/" + ToString.name(this);
+    }
+
+    public View getWrapped() {
+        return wrappedView;
+    }
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/eb613703/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/LineBorder.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/LineBorder.java b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/LineBorder.java
new file mode 100644
index 0000000..563af18
--- /dev/null
+++ b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/LineBorder.java
@@ -0,0 +1,110 @@
+/*
+ *  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.border;
+
+import org.apache.isis.core.commons.debug.DebugBuilder;
+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.Size;
+import org.apache.isis.viewer.dnd.view.Toolkit;
+import org.apache.isis.viewer.dnd.view.View;
+import org.apache.isis.viewer.dnd.view.base.AbstractBorder;
+
+/**
+ * A line border draws a simple box around a view of a given width and color.
+ */
+public class LineBorder extends AbstractBorder {
+    private Color color;
+    private final int arcRadius;
+    private int width;
+    private int padding;
+
+    public LineBorder(final View wrappedView) {
+        this(1, wrappedView);
+    }
+
+    public LineBorder(final int size, final View wrappedView) {
+        this(size, 0, Toolkit.getColor(ColorsAndFonts.COLOR_BLACK), wrappedView);
+    }
+
+    public LineBorder(final int size, final int arcRadius, final View wrappedView) {
+        this(size, arcRadius, Toolkit.getColor(ColorsAndFonts.COLOR_BLACK), wrappedView);
+    }
+
+    public LineBorder(final Color color, final View wrappedView) {
+        this(1, 0, color, wrappedView);
+    }
+
+    public LineBorder(final int width, final Color color, final View wrappedView) {
+        this(width, 0, color, wrappedView);
+    }
+
+    public LineBorder(final int width, final int arcRadius, final Color color, final View wrappedView) {
+        super(wrappedView);
+        setWidth(width);
+        this.arcRadius = arcRadius;
+        this.color = color;
+    }
+
+    @Override
+    protected void debugDetails(final DebugBuilder debug) {
+        debug.append("LineBorder " + top + " pixels\n");
+    }
+
+    @Override
+    public void draw(final Canvas canvas) {
+        super.draw(canvas);
+        final Size s = getSize();
+        final int width = s.getWidth();
+        for (int i = 0; i < left - padding; i++) {
+            // canvas.drawRectangle(i, i, width - 2 * i, s.getHeight() - 2 * i,
+            // color);
+            canvas.drawRoundedRectangle(i, i, width - 2 * i, s.getHeight() - 2 * i, arcRadius, arcRadius, color);
+        }
+    }
+
+    @Override
+    public String toString() {
+        return wrappedView.toString() + "/LineBorder";
+    }
+
+    public void setWidth(final int width) {
+        this.width = width;
+        calculateBorderWidth();
+    }
+
+    public void setPadding(final int padding) {
+        this.padding = padding;
+        calculateBorderWidth();
+    }
+
+    private void calculateBorderWidth() {
+        top = width + padding;
+        left = width + padding;
+        bottom = width + padding;
+        right = width + padding;
+    }
+
+    public void setColor(final Color color) {
+        this.color = color;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/eb613703/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/ObjectBorder.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/ObjectBorder.java b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/ObjectBorder.java
new file mode 100644
index 0000000..a17e970
--- /dev/null
+++ b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/ObjectBorder.java
@@ -0,0 +1,167 @@
+/*
+ *  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.border;
+
+import org.apache.isis.core.commons.debug.DebugBuilder;
+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.Offset;
+import org.apache.isis.viewer.dnd.drawing.Size;
+import org.apache.isis.viewer.dnd.interaction.ViewDragImpl;
+import org.apache.isis.viewer.dnd.view.DragEvent;
+import org.apache.isis.viewer.dnd.view.DragStart;
+import org.apache.isis.viewer.dnd.view.Toolkit;
+import org.apache.isis.viewer.dnd.view.View;
+import org.apache.isis.viewer.dnd.view.ViewState;
+import org.apache.isis.viewer.dnd.view.base.AbstractBorder;
+import org.apache.isis.viewer.dnd.view.base.DragViewOutline;
+
+/**
+ * A border for objects providing
+ * <ol>
+ * <li>Ability to drag out a new view of the object.</li>
+ * <li>State change when moving over object.
+ * <li>Feedback of the state of the view, eg drop valid, identified etc.
+ * </ol>
+ */
+public class ObjectBorder extends AbstractBorder {
+    private static final int BORDER = 13;
+
+    public ObjectBorder(final int size, final View wrappedView) {
+        super(wrappedView);
+
+        top = size;
+        left = size;
+        bottom = size;
+        right = size + BORDER;
+    }
+
+    public ObjectBorder(final View wrappedView) {
+        this(1, wrappedView);
+    }
+
+    @Override
+    protected void debugDetails(final DebugBuilder debug) {
+        super.debugDetails(debug);
+        debug.appendln("line thickness", left);
+    }
+
+    @Override
+    public DragEvent dragStart(final DragStart drag) {
+        if (drag.getLocation().getX() > getSize().getWidth() - right) {
+            if (getContent().getAdapter() == null) {
+                return null;
+            }
+            final View dragOverlay = new DragViewOutline(getView());
+            return new ViewDragImpl(this, new Offset(drag.getLocation()), dragOverlay);
+        } else {
+            return super.dragStart(drag);
+        }
+    }
+
+    @Override
+    public void draw(final Canvas canvas) {
+        super.draw(canvas);
+
+        Color color = null;
+        final ViewState state = getState();
+        final boolean hasFocus = getViewManager().hasFocus(getView());
+        if (state.canDrop()) {
+            color = Toolkit.getColor(ColorsAndFonts.COLOR_VALID);
+        } else if (state.cantDrop()) {
+            color = Toolkit.getColor(ColorsAndFonts.COLOR_INVALID);
+        } else if (hasFocus) {
+            color = Toolkit.getColor(ColorsAndFonts.COLOR_IDENTIFIED);
+        } else if (state.isObjectIdentified()) {
+            color = Toolkit.getColor(ColorsAndFonts.COLOR_SECONDARY2);
+        }
+        final Size s = getSize();
+
+        if (getContent().isPersistable() && getContent().isTransient()) {
+            final int x = s.getWidth() - 13;
+            final int y = 0;
+            final Image icon = ImageFactory.getInstance().loadIcon("transient", 8, null);
+            if (icon == null) {
+                canvas.drawText("*", x, y + Toolkit.getText(ColorsAndFonts.TEXT_NORMAL).getAscent(), Toolkit.getColor(ColorsAndFonts.COLOR_BLACK), Toolkit.getText(ColorsAndFonts.TEXT_NORMAL));
+            } else {
+                canvas.drawImage(icon, x, y, 12, 12);
+            }
+        }
+
+        if (color != null) {
+            if (hasFocus) {
+                final int xExtent = s.getWidth() - left;
+                for (int i = 0; i < left; i++) {
+                    canvas.drawRectangle(i, i, xExtent - 2 * i, s.getHeight() - 2 * i, color);
+                }
+            } else {
+                final int xExtent = s.getWidth();
+                for (int i = 0; i < left; i++) {
+                    canvas.drawRectangle(i, i, xExtent - 2 * i, s.getHeight() - 2 * i, color);
+                }
+                canvas.drawLine(xExtent - BORDER, top, xExtent - BORDER, top + s.getHeight(), color);
+                canvas.drawSolidRectangle(xExtent - BORDER + 1, top, BORDER - 2, s.getHeight() - 2 * top, Toolkit.getColor(ColorsAndFonts.COLOR_SECONDARY3));
+            }
+        }
+    }
+
+    @Override
+    public void entered() {
+        getState().setContentIdentified();
+        getState().setViewIdentified();
+        wrappedView.entered();
+        markDamaged();
+    }
+
+    @Override
+    public void exited() {
+        getState().clearObjectIdentified();
+        getState().clearViewIdentified();
+        wrappedView.exited();
+        markDamaged();
+    }
+
+    /*
+     * @Override public void viewMenuOptions(final UserActionSet options) {
+     * super.viewMenuOptions(options); Content content = getContent();
+     * UserActionSet suboptions = options.addNewActionSet("Replace with");
+     * replaceOptions(Toolkit.getViewFactory().availableViews(new
+     * ViewRequirement(content, ViewRequirement.OPEN |
+     * ViewRequirement.REPLACEABLE | ViewRequirement.SUBVIEW)), suboptions);
+     * replaceOptions(Toolkit.getViewFactory().availableViews(new
+     * ViewRequirement(content, ViewRequirement.CLOSED |
+     * ViewRequirement.REPLACEABLE | ViewRequirement.SUBVIEW)), suboptions); }
+     * 
+     * protected void replaceOptions(final Enumeration possibleViews, final
+     * UserActionSet options) { if (possibleViews.hasMoreElements()) { while
+     * (possibleViews.hasMoreElements()) { final ViewSpecification specification
+     * = (ViewSpecification) possibleViews.nextElement(); if (specification !=
+     * getSpecification()) { options.add(new ReplaceViewOption(specification) {
+     * protected void replace(View view, View withReplacement) {
+     * replaceWrappedView(withReplacement); } }); } } } }
+     */
+    @Override
+    public String toString() {
+        return wrappedView.toString() + "/ObjectBorder [" + getSpecification() + "]";
+    }
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/eb613703/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/ResizeBorder.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/ResizeBorder.java b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/ResizeBorder.java
new file mode 100644
index 0000000..63f9f2f
--- /dev/null
+++ b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/ResizeBorder.java
@@ -0,0 +1,243 @@
+/*
+ *  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.border;
+
+import org.apache.log4j.Logger;
+
+import org.apache.isis.core.commons.debug.DebugBuilder;
+import org.apache.isis.viewer.dnd.drawing.Bounds;
+import org.apache.isis.viewer.dnd.drawing.Canvas;
+import org.apache.isis.viewer.dnd.drawing.Location;
+import org.apache.isis.viewer.dnd.drawing.Size;
+import org.apache.isis.viewer.dnd.view.DragEvent;
+import org.apache.isis.viewer.dnd.view.DragStart;
+import org.apache.isis.viewer.dnd.view.InternalDrag;
+import org.apache.isis.viewer.dnd.view.UserActionSet;
+import org.apache.isis.viewer.dnd.view.View;
+import org.apache.isis.viewer.dnd.view.ViewAreaType;
+import org.apache.isis.viewer.dnd.view.Workspace;
+import org.apache.isis.viewer.dnd.view.base.AbstractBorder;
+import org.apache.isis.viewer.dnd.view.option.UserActionAbstract;
+
+public abstract class ResizeBorder extends AbstractBorder {
+    private static final Logger LOG = Logger.getLogger(ResizeBorder.class);
+    private static final Logger UI_LOG = Logger.getLogger("ui." + ResizeBorder.class.getName());
+    public static final int LEFT = 1;
+    public static final int RIGHT = 2;
+    public static final int UP = 4;
+    public static final int DOWN = 8;
+    private int width;
+    private int height;
+    private int requiredDirection;
+    private final int allowDirections;
+    protected boolean resizing;
+    private int onBorder;
+
+    // TODO allow a minimum and maximum sizes to be specified and then ensure
+    // the user doesn't go outside them.
+    public ResizeBorder(final View view, final int allowDirections, final int widthOnMovingSides, final int widthOnStaticSides) {
+        super(view);
+        this.allowDirections = allowDirections;
+        top = canExtend(UP) ? widthOnMovingSides : widthOnStaticSides;
+        bottom = canExtend(DOWN) ? widthOnMovingSides : widthOnStaticSides;
+        left = canExtend(LEFT) ? widthOnMovingSides : widthOnStaticSides;
+        right = canExtend(RIGHT) ? widthOnMovingSides : widthOnStaticSides;
+
+    }
+
+    @Override
+    protected void debugDetails(final DebugBuilder debug) {
+        super.debugDetails(debug);
+        debug.appendln("width", width == 0 ? "no change" : Integer.toString(width));
+        debug.appendln("height ", height == 0 ? "no change" : Integer.toString(height));
+        debug.appendln("resizable ", (canExtend(UP) ? "Up " : "") + (canExtend(DOWN) ? "Down " : "") + (canExtend(LEFT) ? "Left " : "") + (canExtend(RIGHT) ? "Right " : ""));
+    }
+
+    @Override
+    public void draw(final Canvas canvas) {
+        final Size size = getSize();
+        final int width = size.getWidth();
+        final int height = size.getHeight();
+        drawResizeBorder(canvas, size);
+
+        final Canvas subCanvas = canvas.createSubcanvas(left, top, width - left - right, height - top - bottom);
+        wrappedView.draw(subCanvas);
+    }
+
+    protected abstract void drawResizeBorder(final Canvas canvas, final Size size);
+
+    @Override
+    public ViewAreaType viewAreaType(final Location mouseLocation) {
+        if (isOnBorder()) {
+            return ViewAreaType.INTERNAL;
+        }
+        return super.viewAreaType(mouseLocation);
+    }
+
+    @Override
+    public void viewMenuOptions(final UserActionSet menuOptions) {
+        super.viewMenuOptions(menuOptions);
+        menuOptions.add(new UserActionAbstract("Clear resizing") {
+            @Override
+            public void execute(final Workspace workspace, final View view, final Location at) {
+                width = 0;
+                height = 0;
+                invalidateLayout();
+            }
+        });
+    }
+
+    @Override
+    public DragEvent dragStart(final DragStart drag) {
+        final Location location = drag.getLocation();
+        if (overBorder(location)) {
+            requiredDirection = onBorder(location);
+            if (requiredDirection > 0) {
+                return new ResizeDrag(this, new Bounds(getAbsoluteLocation(), getView().getSize()), requiredDirection);
+            }
+            return null;
+        } else {
+            return super.dragStart(drag);
+        }
+    }
+
+    @Override
+    public void drag(final InternalDrag drag) {
+        final ViewResizeOutline outline = ((ViewResizeOutline) drag.getOverlay());
+        if (outline == null) {
+            super.drag(drag);
+        }
+    }
+
+    @Override
+    public void dragTo(final InternalDrag drag) {
+        getFeedbackManager().showDefaultCursor();
+        final ViewResizeOutline outline = ((ViewResizeOutline) drag.getOverlay());
+        if (outline != null) {
+            resizing = false;
+            onBorder = 0;
+
+            if (requiredDirection == ResizeDrag.RIGHT || requiredDirection == ResizeDrag.BOTTOM_RIGHT) {
+                width = outline.getSize().getWidth();
+            }
+            if (requiredDirection == ResizeDrag.BOTTOM || requiredDirection == ResizeDrag.BOTTOM_RIGHT) {
+                height = outline.getSize().getHeight();
+            }
+
+            LOG.debug("resizing view " + width + "," + height);
+            invalidateLayout();
+        } else {
+            super.dragTo(drag);
+        }
+    }
+
+    @Override
+    public Size getRequiredSize(final Size maximumSize) {
+        maximumSize.contract(getLeft() + getRight(), getTop() + getBottom());
+        if (width > 0 && maximumSize.getWidth() > width) {
+            maximumSize.setWidth(width);
+        }
+        if (height > 0 && maximumSize.getHeight() > height) {
+            maximumSize.setHeight(height);
+        }
+        final Size size = wrappedView.getRequiredSize(maximumSize);
+        size.extend(getLeft() + getRight(), getTop() + getBottom());
+        if (width > 0) {
+            size.setWidth(width);
+        }
+        if (height > 0) {
+            size.setHeight(height);
+        }
+        return size;
+    }
+
+    /**
+     * Detects whether the point is on the resize border, and if so changes the
+     * cursor to show it can be resized.
+     */
+    @Override
+    public void mouseMoved(final Location at) {
+        final int onBorder = onBorder(at);
+        if (this.onBorder != onBorder) {
+            switch (onBorder) {
+            case ResizeDrag.RIGHT:
+                getFeedbackManager().showResizeRightCursor();
+                resizing = true;
+                markDamaged();
+                break;
+
+            case ResizeDrag.BOTTOM:
+                getFeedbackManager().showResizeDownCursor();
+                resizing = true;
+                markDamaged();
+                break;
+
+            case ResizeDrag.BOTTOM_RIGHT:
+                getFeedbackManager().showResizeDownRightCursor();
+                resizing = true;
+                markDamaged();
+                break;
+
+            default:
+                getFeedbackManager().showDefaultCursor();
+                super.mouseMoved(at);
+                resizing = false;
+                markDamaged();
+                break;
+            }
+            UI_LOG.debug("on resize border " + onBorder + " " + resizing);
+        }
+        this.onBorder = onBorder;
+    }
+
+    @Override
+    public void exited() {
+        getFeedbackManager().showDefaultCursor();
+        resizing = false;
+        onBorder = 0;
+        markDamaged();
+        UI_LOG.debug("off resize border " + onBorder + " " + resizing);
+        super.exited();
+    }
+
+    private int onBorder(final Location at) {
+        final Bounds area = contentArea();
+        final boolean right = canExtend(RIGHT) && at.getX() >= area.getWidth() && at.getX() <= area.getWidth() + getRight();
+        final boolean bottom = canExtend(DOWN) && at.getY() >= area.getHeight() && at.getY() <= area.getHeight() + getBottom();
+
+        final int status;
+        if (right && bottom) {
+            status = ResizeDrag.BOTTOM_RIGHT;
+        } else if (right) {
+            status = ResizeDrag.RIGHT;
+        } else if (bottom) {
+            status = ResizeDrag.BOTTOM;
+        } else {
+            status = 0;
+        }
+
+        return status;
+    }
+
+    private boolean canExtend(final int extend) {
+        return (extend & allowDirections) == extend;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/eb613703/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/ResizeDrag.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/ResizeDrag.java b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/ResizeDrag.java
new file mode 100644
index 0000000..08524b1
--- /dev/null
+++ b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/ResizeDrag.java
@@ -0,0 +1,215 @@
+/*
+ *  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.border;
+
+import org.apache.isis.viewer.dnd.drawing.Bounds;
+import org.apache.isis.viewer.dnd.drawing.Location;
+import org.apache.isis.viewer.dnd.drawing.Size;
+import org.apache.isis.viewer.dnd.interaction.DragImpl;
+import org.apache.isis.viewer.dnd.view.InternalDrag;
+import org.apache.isis.viewer.dnd.view.View;
+import org.apache.isis.viewer.dnd.view.Viewer;
+
+public class ResizeDrag extends DragImpl implements InternalDrag {
+    public static final int BOTTOM = 2;
+    public static final int BOTTOM_LEFT = 7;
+    public static final int BOTTOM_RIGHT = 8;
+    public static final int LEFT = 3;
+    public static final int RIGHT = 4;
+    public static final int TOP = 1;
+    public static final int TOP_LEFT = 5;
+    public static final int TOP_RIGHT = 6;
+    /**
+     * the location of the corner opposite the pointer that will form the
+     * resizing rectangle.
+     */
+    private final Location anchor;
+    private final int direction;
+    private final ViewResizeOutline overlay;
+    private final View view;
+    private final Size minimumSize;
+    private final Size maximumSize;
+
+    public ResizeDrag(final View view, final Bounds resizeArea, final int direction) {
+        this(view, resizeArea, direction, null, null);
+    }
+
+    public ResizeDrag(final View view, final Bounds resizeArea, final int direction, final Size minimumSize, final Size maximumSize) {
+        this.view = view;
+        this.direction = direction;
+        this.anchor = resizeArea.getLocation();
+        this.minimumSize = minimumSize;
+        this.maximumSize = maximumSize;
+        overlay = new ViewResizeOutline(resizeArea);
+        overlay.setLocation(resizeArea.getLocation());
+    }
+
+    @Override
+    public void cancel(final Viewer viewer) {
+        view.dragCancel(this);
+    }
+
+    @Override
+    public void drag(final View target, final Location location, final int mods) {
+
+        switch (direction) {
+        case TOP:
+            extendUpward(location);
+            break;
+
+        case BOTTOM:
+            extendDownward(location);
+            break;
+
+        case LEFT:
+            extendLeft(location);
+            break;
+
+        case RIGHT:
+            extendRight(location);
+            break;
+
+        case TOP_RIGHT:
+            extendRight(location);
+            extendUpward(location);
+            break;
+
+        case BOTTOM_RIGHT:
+            extendRight(location);
+            extendDownward(location);
+            break;
+
+        case TOP_LEFT:
+            extendLeft(location);
+            extendUpward(location);
+            break;
+
+        case BOTTOM_LEFT:
+            extendLeft(location);
+            extendDownward(location);
+            break;
+
+        default:
+            break;
+        }
+    }
+
+    @Override
+    public void end(final Viewer viewer) {
+        view.dragTo(this);
+        view.getViewManager().clearOverlayView(view);
+    }
+
+    /*
+     * public ViewResizeOutline(View forView, int direction) { this(forView,
+     * direction, forView.getAbsoluteLocation(), forView.getSize()); }
+     * 
+     * public ViewResizeOutline(View forView, int direction, Location location,
+     * Size size) { super(forView.getContent(), null, null);
+     * 
+     * Logger.getLogger(getClass()).debug("drag outline for " + forView);
+     * setLocation(location); setSize(size);
+     * 
+     * Logger.getLogger(getClass()).debug("drag outline initial size " +
+     * getSize() + " " + forView.getSize());
+     * 
+     * origin = getBounds();
+     * 
+     * switch (direction) { case TOP: getViewManager().showResizeUpCursor();
+     * break;
+     * 
+     * case BOTTOM: getViewManager().showResizeDownCursor(); break;
+     * 
+     * case LEFT: getViewManager().showResizeLeftCursor(); break;
+     * 
+     * case RIGHT: getViewManager().showResizeRightCursor(); break;
+     * 
+     * case TOP_LEFT: getViewManager().showResizeUpLeftCursor(); break;
+     * 
+     * case TOP_RIGHT: getViewManager().showResizeUpRightCursor(); break;
+     * 
+     * case BOTTOM_LEFT: getViewManager().showResizeDownLeftCursor(); break;
+     * 
+     * case BOTTOM_RIGHT: getViewManager().showResizeDownRightCursor(); break;
+     * 
+     * case CENTER: getViewManager().showMoveCursor(); break;
+     * 
+     * default : break; } }
+     */
+
+    private void extendDownward(final Location location) {
+        overlay.markDamaged();
+        final int height = location.getY() - anchor.getY();
+        final int width = overlay.getSize().getWidth();
+        overlay.setSize(new Size(width, height));
+        overlay.markDamaged();
+    }
+
+    private void extendLeft(final Location location) {
+        overlay.markDamaged();
+        final int height = overlay.getSize().getHeight();
+        final int width = anchor.getX() - location.getX();
+        overlay.setSize(new Size(width, height));
+        final int x = anchor.getX() - width;
+        final int y = anchor.getY();
+        overlay.setBounds(new Bounds(x, y, width, height));
+        overlay.markDamaged();
+    }
+
+    private void extendRight(final Location location) {
+        overlay.markDamaged();
+        final int height = overlay.getSize().getHeight();
+        int width = location.getX() - anchor.getX();
+        if (maximumSize != null && width > maximumSize.getWidth()) {
+            width = maximumSize.getWidth();
+        }
+        if (minimumSize != null && width < minimumSize.getWidth()) {
+            width = minimumSize.getWidth();
+        }
+        overlay.setSize(new Size(width, height));
+        overlay.markDamaged();
+    }
+
+    private void extendUpward(final Location location) {
+        overlay.markDamaged();
+        final int height = anchor.getY() - location.getY();
+        final int width = overlay.getSize().getWidth();
+        overlay.setSize(new Size(width, height));
+        final int x = anchor.getX();
+        final int y = anchor.getY() - height;
+        overlay.setBounds(new Bounds(x, y, width, height));
+        overlay.markDamaged();
+    }
+
+    public int getDirection() {
+        return direction;
+    }
+
+    @Override
+    public Location getLocation() {
+        final Size size = overlay.getSize();
+        return new Location(size.getWidth(), size.getHeight());
+    }
+
+    @Override
+    public View getOverlay() {
+        return overlay;
+    }
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/eb613703/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/ResizeViewRender.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/ResizeViewRender.java b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/ResizeViewRender.java
new file mode 100644
index 0000000..5e245f2
--- /dev/null
+++ b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/ResizeViewRender.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.border;
+
+import org.apache.isis.viewer.dnd.drawing.Canvas;
+
+public interface ResizeViewRender {
+
+    void draw(Canvas canvas, int x, int width, int height, boolean hasFocus);
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/eb613703/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/SaveState.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/SaveState.java b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/SaveState.java
new file mode 100644
index 0000000..d5b3fa1
--- /dev/null
+++ b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/SaveState.java
@@ -0,0 +1,56 @@
+/*
+ *  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.border;
+
+class SaveState {
+    StringBuffer missingFields = new StringBuffer();
+    StringBuffer invalidFields = new StringBuffer();
+
+    void addMissingField(final String parameterName) {
+        if (missingFields.length() > 0) {
+            missingFields.append(", ");
+        }
+        missingFields.append(parameterName);
+    }
+
+    void addInvalidField(final String parameterName) {
+        if (invalidFields.length() > 0) {
+            invalidFields.append(", ");
+        }
+        invalidFields.append(parameterName);
+    }
+
+    String getMessage() {
+        String error = "";
+        if (missingFields.length() > 0) {
+            if (error.length() > 0) {
+                error += "; ";
+            }
+            error += "Fields needed: " + missingFields;
+        }
+        if (invalidFields.length() > 0) {
+            if (error.length() > 0) {
+                error += "; ";
+            }
+            error += "Invalid fields: " + invalidFields;
+        }
+        return error;
+    }
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/eb613703/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/SaveTransientObjectBorder.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/SaveTransientObjectBorder.java b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/SaveTransientObjectBorder.java
new file mode 100644
index 0000000..2c5f50c
--- /dev/null
+++ b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/SaveTransientObjectBorder.java
@@ -0,0 +1,175 @@
+/*
+ *  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.border;
+
+import org.apache.log4j.Logger;
+
+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.ObjectSpecification;
+import org.apache.isis.runtimes.dflt.runtime.system.context.IsisContext;
+import org.apache.isis.runtimes.dflt.runtime.system.persistence.Persistor;
+import org.apache.isis.viewer.dnd.drawing.Location;
+import org.apache.isis.viewer.dnd.view.ButtonAction;
+import org.apache.isis.viewer.dnd.view.Content;
+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;
+import org.apache.isis.viewer.dnd.view.content.RootObject;
+import org.apache.isis.viewer.dnd.view.control.AbstractButtonAction;
+
+public class SaveTransientObjectBorder extends ButtonBorder {
+    private static final Logger LOG = Logger.getLogger(SaveTransientObjectBorder.class);
+
+    private static class CloseAction extends AbstractButtonAction {
+        public CloseAction() {
+            super("Discard");
+        }
+
+        @Override
+        public void execute(final Workspace workspace, final View view, final Location at) {
+            close(workspace, view);
+        }
+    }
+
+    private static class SaveAction extends AbstractButtonAction {
+        public SaveAction() {
+            super("Save");
+        }
+
+        @Override
+        public Consent disabled(final View view) {
+            return canSave(view);
+        }
+
+        @Override
+        public void execute(final Workspace workspace, final View view, final Location at) {
+            save(view);
+            // by recreating the view the transient border is removed
+            final ViewSpecification spec = view.getSpecification();
+            final View newView = spec.createView(view.getContent(), view.getViewAxes(), -1);
+            workspace.replaceView(view, newView);
+        }
+    }
+
+    private static Consent canSave(final View view) {
+
+        final ObjectAdapter transientNO = view.getContent().getAdapter();
+
+        // check each of the fields, and capture invalid state if known
+        final SaveState saveState = new SaveState();
+        checkFields(saveState, view, transientNO);
+        final StringBuilder errorBuf = new StringBuilder(saveState.getMessage());
+
+        final ObjectSpecification viewContentSpec = view.getContent().getSpecification();
+        final Consent consent = viewContentSpec.isValid(transientNO);
+        if (consent.isVetoed()) {
+            if (errorBuf.length() > 0) {
+                errorBuf.append("; ");
+            }
+            errorBuf.append(consent.getReason());
+        }
+
+        if (errorBuf.length() == 0) {
+            return Allow.DEFAULT;
+        } else {
+            return new Veto(errorBuf.toString());
+        }
+    }
+
+    private static void checkFields(final SaveState saveState, final View view, final ObjectAdapter forObject) {
+        if (view.getContent().getAdapter() != forObject) {
+            return;
+        }
+
+        final View[] subviews = view.getSubviews();
+        for (final View fieldView : subviews) {
+            final Content content = fieldView.getContent();
+            if (content instanceof RootObject) {
+                checkFields(saveState, fieldView, forObject);
+            } else if (content instanceof FieldContent) {
+                final boolean isMandatory = ((FieldContent) content).isMandatory();
+                final boolean isEditable = ((FieldContent) content).isEditable().isAllowed();
+                final ObjectAdapter field = content.getAdapter();
+                final boolean isFieldEmpty = field == null;
+                if (isMandatory && isEditable && isFieldEmpty) {
+                    final String parameterName = ((FieldContent) content).getFieldName();
+                    saveState.addMissingField(parameterName);
+
+                } else if (fieldView.getState().isInvalid()) {
+                    final String parameterName = ((FieldContent) content).getFieldName();
+                    saveState.addInvalidField(parameterName);
+                }
+            }
+        }
+    }
+
+    private static class SaveAndCloseAction extends AbstractButtonAction {
+        public SaveAndCloseAction() {
+            super("Save & Close");
+        }
+
+        @Override
+        public Consent disabled(final View view) {
+            return canSave(view);
+        }
+
+        @Override
+        public void execute(final Workspace workspace, final View view, final Location at) {
+            save(view);
+            close(workspace, view);
+        }
+    }
+
+    private static void close(final Workspace workspace, final View view) {
+        view.dispose();
+    }
+
+    private static ObjectAdapter save(final View view) {
+        final ObjectAdapter transientObject = view.getContent().getAdapter();
+        try {
+            getPersistenceSession().makePersistent(transientObject);
+        } catch (final RuntimeException e) {
+            LOG.info("exception saving " + transientObject + ", aborting transaction" + e.getMessage());
+            throw e;
+        }
+        return transientObject;
+    }
+
+    // /////////////////////////////////////////////////////
+    // Constructor
+    // /////////////////////////////////////////////////////
+
+    public SaveTransientObjectBorder(final View view) {
+        super(new ButtonAction[] { new SaveAction(), new SaveAndCloseAction(), new CloseAction(), }, view);
+    }
+
+    // /////////////////////////////////////////////////////
+    // Dependencies (from context)
+    // /////////////////////////////////////////////////////
+
+    private static Persistor getPersistenceSession() {
+        return IsisContext.getPersistenceSession();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/eb613703/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/ScrollBar.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/ScrollBar.java b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/ScrollBar.java
new file mode 100644
index 0000000..bf0f512
--- /dev/null
+++ b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/ScrollBar.java
@@ -0,0 +1,88 @@
+/*
+ *  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.border;
+
+public class ScrollBar {
+    private int maximum;
+    private int minimum;
+    private int scrollPosition = 0;
+    private int visibleAmount;
+
+    public ScrollBar() {
+        super();
+    }
+
+    public void setPostion(final int position) {
+        scrollPosition = Math.min(position, maximum);
+        scrollPosition = Math.max(scrollPosition, minimum);
+    }
+
+    public void firstClick(final int x, final boolean alt) {
+        if (alt) {
+            setPostion(x - visibleAmount / 2);
+        } else {
+            if (x < scrollPosition) {
+                setPostion(scrollPosition - visibleAmount);
+            } else if (x > scrollPosition + visibleAmount) {
+                setPostion(scrollPosition + visibleAmount);
+            }
+        }
+    }
+
+    public int getMaximum() {
+        return maximum;
+    }
+
+    public int getMinimum() {
+        return minimum;
+    }
+
+    public int getPosition() {
+        return scrollPosition;
+    }
+
+    public int getVisibleAmount() {
+        return visibleAmount;
+    }
+
+    public void limit() {
+        if (scrollPosition > maximum) {
+            scrollPosition = maximum;
+        }
+    }
+
+    public void reset() {
+        scrollPosition = 0;
+    }
+
+    public boolean isOnThumb(final int pos) {
+        return pos > scrollPosition && pos < scrollPosition + visibleAmount;
+    }
+
+    public void setSize(final int viewportSize, final int contentSize) {
+        visibleAmount = contentSize == 0 ? 0 : (viewportSize * viewportSize / contentSize);
+        maximum = viewportSize - visibleAmount;
+    }
+
+    public void secondClick(final int y) {
+        final int midpoint = (maximum + visibleAmount) / 2;
+        setPostion(y < midpoint ? minimum : maximum);
+    }
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/eb613703/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/ScrollBarRender.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/ScrollBarRender.java b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/ScrollBarRender.java
new file mode 100644
index 0000000..e3307fb
--- /dev/null
+++ b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/ScrollBarRender.java
@@ -0,0 +1,26 @@
+/*
+ *  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.border;
+
+import org.apache.isis.viewer.dnd.drawing.Canvas;
+
+public interface ScrollBarRender {
+    void draw(Canvas canvas, boolean isHorizontal, int x, int y, int width, int height, int scrollPosition, int visibleAmount);
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/eb613703/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/ScrollBorder.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/ScrollBorder.java b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/ScrollBorder.java
new file mode 100644
index 0000000..3e092b2
--- /dev/null
+++ b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/ScrollBorder.java
@@ -0,0 +1,773 @@
+/*
+ *  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.border;
+
+import org.apache.isis.core.commons.debug.DebugBuilder;
+import org.apache.isis.core.metamodel.spec.ActionType;
+import org.apache.isis.viewer.dnd.drawing.Bounds;
+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.Offset;
+import org.apache.isis.viewer.dnd.drawing.Size;
+import org.apache.isis.viewer.dnd.interaction.SimpleInternalDrag;
+import org.apache.isis.viewer.dnd.view.Click;
+import org.apache.isis.viewer.dnd.view.ContentDrag;
+import org.apache.isis.viewer.dnd.view.DragEvent;
+import org.apache.isis.viewer.dnd.view.DragStart;
+import org.apache.isis.viewer.dnd.view.InternalDrag;
+import org.apache.isis.viewer.dnd.view.Toolkit;
+import org.apache.isis.viewer.dnd.view.UserActionSet;
+import org.apache.isis.viewer.dnd.view.View;
+import org.apache.isis.viewer.dnd.view.ViewAreaType;
+import org.apache.isis.viewer.dnd.view.Workspace;
+import org.apache.isis.viewer.dnd.view.base.AbstractViewDecorator;
+import org.apache.isis.viewer.dnd.view.base.NullView;
+import org.apache.isis.viewer.dnd.view.option.UserActionAbstract;
+
+/**
+ * A scroll border provides a window on a larger view, providing scrollbars as a
+ * way of moving the visible part of that view around the actual visible viewing
+ * area. To achieve this the view is divided up into five main areas, not all of
+ * which are used. In the centre is the viewing area of the underlying view. At
+ * the bottom and to the right... At the top and to the left are headers that
+ */
+public class ScrollBorder extends AbstractViewDecorator {
+    private static ScrollBarRender render;
+    private static final int CENTER = 3;
+    private static final int NORTH = 1;
+    private static final int SOUTH = 5;
+    private static final int CORNER = 0;
+    private static final int SCROLLBAR_WIDTH = 16;
+    private static final int WEST = 2;
+    private static final int EAST = 4;
+
+    public static void setRender(final ScrollBarRender render) {
+        ScrollBorder.render = render;
+    }
+
+    private final ScrollBar horizontalScrollBar = new ScrollBar();
+    private final ScrollBar verticalScrollBar = new ScrollBar();
+    protected int bottom;
+    protected int left;
+    private View leftHeader;
+    protected int right;
+    private Size size = new Size();
+    protected int top;
+    private View topHeader;
+    private int dragArea = CENTER;
+    private int offsetToThumbEdge;
+
+    public ScrollBorder(final View view) {
+        this(view, new NullView(), new NullView());
+    }
+
+    /**
+     * Note - the leftHeader, if it is specified, view must be the same height
+     * as the content view and the rightHeader, if it is specified, must be the
+     * same width.
+     */
+    public ScrollBorder(final View content, final View leftHeader, final View topHeader) {
+        super(content);
+        bottom = right = SCROLLBAR_WIDTH;
+        horizontalScrollBar.setPostion(0);
+        verticalScrollBar.setPostion(0);
+        setLeftHeader(leftHeader);
+        setTopHeader(topHeader);
+    }
+
+    public void setTopHeader(final View topHeader) {
+        this.topHeader = topHeader;
+        topHeader.setParent(getView());
+        top = topHeader.getRequiredSize(new Size()).getHeight();
+    }
+
+    public void setLeftHeader(final View leftHeader) {
+        this.leftHeader = leftHeader;
+        leftHeader.setParent(getView());
+        left = leftHeader.getRequiredSize(new Size()).getWidth();
+    }
+
+    private int adjust(final Click click) {
+        return adjust(click.getLocation());
+    }
+
+    private int adjust(final ContentDrag drag) {
+        return adjust(drag.getTargetLocation());
+    }
+
+    private int adjust(final Location location) {
+        final Bounds contentArea = viewportArea();
+        final Offset offset = offset();
+        final int yOffset = offset.getDeltaY();
+        final int xOffset = offset.getDeltaX();
+        if (contentArea.contains(location)) {
+            location.subtract(left, top);
+            location.add(xOffset, yOffset);
+            return CENTER;
+        } else {
+            final int x = location.getX();
+            final int y = location.getY();
+
+            if (x > contentArea.getX2() && y >= contentArea.getY() && y <= contentArea.getY2()) {
+                // vertical scrollbar
+                location.subtract(0, contentArea.getY());
+                return EAST;
+            } else if (y > contentArea.getY2() && x >= contentArea.getX() && x <= contentArea.getX2()) {
+                // horzontal scrollbar
+                location.subtract(contentArea.getX(), 0);
+                return SOUTH;
+            } else if (y < contentArea.getY() && x >= contentArea.getX() && x <= contentArea.getX2()) {
+                // top border
+                location.subtract(left, 0);
+                location.add(xOffset, 0);
+                return NORTH;
+            } else if (x < contentArea.getX() && y >= contentArea.getY() && y <= contentArea.getY2()) {
+                // left border
+                location.subtract(0, top);
+                location.add(0, yOffset);
+                return WEST;
+            } else {
+                // ignore;
+                location.setX(-1);
+                location.setY(-1);
+                return CORNER;
+            }
+        }
+
+    }
+
+    protected Bounds viewportArea() {
+        return new Bounds(left, top, getSize().getWidth() - left - right, getSize().getHeight() - top - bottom);
+    }
+
+    @Override
+    protected void debugDetails(final DebugBuilder debug) {
+        super.debugDetails(debug);
+        debug.append("\n           Top header: " + (topHeader == null ? "none" : topHeader.toString()));
+        debug.append("\n           Left header: " + (leftHeader == null ? "none" : leftHeader.toString()));
+
+        debug.append("\n           Vertical scrollbar ");
+        debug.append("\n             offset " + top);
+        debug.append("\n             position " + verticalScrollBar.getPosition());
+        debug.append("\n             minimum " + verticalScrollBar.getMinimum());
+        debug.append("\n             maximum " + verticalScrollBar.getMaximum());
+        debug.append("\n             visible amount " + verticalScrollBar.getVisibleAmount());
+
+        debug.append("\n           Horizontal scrollbar ");
+        debug.append("\n             offset " + left);
+        debug.append("\n             position " + horizontalScrollBar.getPosition());
+        debug.append("\n             minimum " + horizontalScrollBar.getMinimum());
+        debug.append("\n             maximum " + horizontalScrollBar.getMaximum());
+        debug.append("\n             visible amount " + horizontalScrollBar.getVisibleAmount());
+        debug.append("\n           Viewport area " + viewportArea());
+        debug.appendln("\n           Offset " + offset());
+    }
+
+    @Override
+    public void drag(final InternalDrag drag) {
+        switch (dragArea) {
+        case NORTH:
+            drag.getLocation().subtract(offset().getDeltaX(), top);
+            topHeader.drag(drag);
+            break;
+
+        case WEST:
+            drag.getLocation().subtract(left, offset().getDeltaY());
+            leftHeader.drag(drag);
+            break;
+
+        case CENTER:
+            drag.getLocation().subtract(offset());
+            wrappedView.drag(drag);
+            break;
+
+        case SOUTH:
+            final int x = drag.getLocation().getX() - left;
+            horizontalScrollBar.setPostion(x - offsetToThumbEdge);
+            markDamaged();
+            break;
+
+        case EAST:
+            final int y = drag.getLocation().getY() - top;
+            verticalScrollBar.setPostion(y - offsetToThumbEdge);
+            markDamaged();
+            break;
+
+        default:
+            return;
+        }
+    }
+
+    @Override
+    public DragEvent dragStart(final DragStart drag) {
+        final int area = adjust(drag);
+        dragArea = area;
+        switch (dragArea) {
+        case NORTH:
+            return topHeader.dragStart(drag);
+
+        case WEST:
+            return leftHeader.dragStart(drag);
+
+        case CENTER:
+            return wrappedView.dragStart(drag);
+
+        case SOUTH:
+            return dragStartSouth(drag);
+
+        case EAST:
+            return dragStartEast(drag);
+
+        default:
+            return null;
+        }
+    }
+
+    @Override
+    public void dragCancel(final InternalDrag drag) {
+        switch (dragArea) {
+        case NORTH:
+            drag.getLocation().subtract(offset().getDeltaX(), top);
+            topHeader.dragCancel(drag);
+            break;
+
+        case WEST:
+            drag.getLocation().subtract(left, offset().getDeltaY());
+            leftHeader.dragCancel(drag);
+            break;
+
+        case CENTER:
+            drag.getLocation().subtract(offset());
+            wrappedView.dragCancel(drag);
+            break;
+        }
+    }
+
+    @Override
+    public void dragTo(final InternalDrag drag) {
+        switch (dragArea) {
+        case NORTH:
+            drag.getLocation().subtract(offset().getDeltaX(), top);
+            topHeader.dragTo(drag);
+            break;
+
+        case WEST:
+            drag.getLocation().subtract(left, offset().getDeltaY());
+            leftHeader.dragTo(drag);
+            break;
+
+        case CENTER:
+            drag.getLocation().subtract(offset());
+            wrappedView.dragTo(drag);
+            break;
+
+        case SOUTH:
+        case EAST:
+        default:
+            // ignore
+
+        }
+    }
+
+    @Override
+    public View dragFrom(final Location location) {
+        adjust(location);
+        switch (dragArea) {
+        case NORTH:
+            return topHeader.dragFrom(location);
+
+        case WEST:
+            return leftHeader.dragFrom(location);
+
+        case CENTER:
+            return wrappedView.dragFrom(location);
+        }
+
+        return null;
+    }
+
+    @Override
+    public void dragIn(final ContentDrag drag) {
+        adjust(drag);
+        switch (dragArea) {
+        case NORTH:
+            topHeader.dragIn(drag);
+            break;
+
+        case WEST:
+            leftHeader.dragIn(drag);
+            break;
+
+        case CENTER:
+            wrappedView.dragIn(drag);
+            break;
+
+        case SOUTH:
+        case EAST:
+        default:
+            System.out.println(this + " ignored");
+
+            // ignore
+        }
+    }
+
+    @Override
+    public void dragOut(final ContentDrag drag) {
+        adjust(drag);
+        switch (dragArea) {
+        case NORTH:
+            topHeader.dragOut(drag);
+            break;
+
+        case WEST:
+            leftHeader.dragOut(drag);
+            break;
+
+        case CENTER:
+            wrappedView.dragOut(drag);
+            break;
+
+        case SOUTH:
+        case EAST:
+        default:
+            // ignore
+        }
+    }
+
+    private DragEvent dragStartEast(final DragStart drag) {
+        final Location location = drag.getLocation();
+        final int y = location.getY();
+        if (verticalScrollBar.isOnThumb(y)) {
+            // offset is the distance from the left/top of the thumb to the
+            // pointer
+            offsetToThumbEdge = y - verticalScrollBar.getPosition();
+            return new SimpleInternalDrag(this, new Offset(super.getAbsoluteLocation()));
+        } else {
+            return null;
+        }
+    }
+
+    private DragEvent dragStartSouth(final DragStart drag) {
+        final Location location = drag.getLocation();
+        final int x = location.getX();
+        if (horizontalScrollBar.isOnThumb(x)) {
+            offsetToThumbEdge = x - horizontalScrollBar.getPosition();
+            return new SimpleInternalDrag(this, new Offset(super.getAbsoluteLocation()));
+        } else {
+            return null;
+        }
+    }
+
+    private int adjust(final DragStart drag) {
+        return adjust(drag.getLocation());
+    }
+
+    @Override
+    public void draw(final Canvas canvas) {
+        final Bounds contents = viewportArea();
+        final Offset offset = offset();
+        final int x = offset.getDeltaX();
+        final int y = offset.getDeltaY();
+
+        final int contentWidth = contents.getWidth();
+        final int contentHeight = contents.getHeight();
+
+        final Canvas headerCanvasLeft = canvas.createSubcanvas(0, top, left, contentHeight);
+        headerCanvasLeft.offset(0, -y);
+        leftHeader.draw(headerCanvasLeft);
+
+        final Canvas headerCanvasRight = canvas.createSubcanvas(left, 0, contentWidth, top);
+        headerCanvasRight.offset(-x, 0);
+        topHeader.draw(headerCanvasRight);
+
+        final Color thumbColor = Toolkit.getColor(ColorsAndFonts.COLOR_PRIMARY2);
+        drawVerticalScrollBar(canvas, contentWidth, contentHeight, thumbColor);
+        drawHorizontalScrollBar(canvas, contentWidth, contentHeight, thumbColor);
+
+        final Canvas contentCanvas = canvas.createSubcanvas(left, top, contentWidth, contentHeight);
+        contentCanvas.offset(-x, -y);
+
+        if (Toolkit.debug) {
+            canvas.drawRectangle(contents.getX(), contents.getY(), contents.getWidth(), contents.getHeight(), Toolkit.getColor(ColorsAndFonts.COLOR_DEBUG_BOUNDS_DRAW));
+        }
+
+        // drawContent(canvas, contentWidth, contentHeight);
+        wrappedView.draw(contentCanvas);
+
+        if (Toolkit.debug) {
+            final Size size = getSize();
+            canvas.drawRectangle(0, 0, size.getWidth(), size.getHeight(), Toolkit.getColor(ColorsAndFonts.COLOR_DEBUG_BOUNDS_VIEW));
+            canvas.drawLine(0, size.getHeight() / 2, size.getWidth() - 1, size.getHeight() / 2, Toolkit.getColor(ColorsAndFonts.COLOR_DEBUG_BOUNDS_VIEW));
+            canvas.drawLine(0, getBaseline(), size.getWidth() - 1, getBaseline(), Toolkit.getColor(ColorsAndFonts.COLOR_DEBUG_BASELINE));
+        }
+
+    }
+
+    // TODO merge these two methods
+    private void drawVerticalScrollBar(final Canvas canvas, final int contentWidth, final int contentHeight, final Color color) {
+        final int verticalVisibleAmount = verticalScrollBar.getVisibleAmount();
+        final int verticalScrollPosition = verticalScrollBar.getPosition();
+        if (right > 0 && (verticalScrollPosition > top || verticalVisibleAmount < contentHeight)) {
+            final int x = contentWidth + left;
+            render.draw(canvas, false, x, top, SCROLLBAR_WIDTH, contentHeight, verticalScrollPosition, verticalVisibleAmount);
+            /*
+             * canvas.drawSolidRectangle(x + 1, top, SCROLLBAR_WIDTH - 1,
+             * contentHeight,
+             * Toolkit.getColor(ColorsAndFonts.COLOR_SECONDARY3));
+             * canvas.drawSolidRectangle(x + 1, top + verticalScrollPosition,
+             * SCROLLBAR_WIDTH - 2, verticalVisibleAmount, color);
+             * canvas.drawRectangle(x, top, SCROLLBAR_WIDTH, contentHeight,
+             * Toolkit.getColor(ColorsAndFonts.COLOR_SECONDARY2));
+             * canvas.drawRectangle(x + 1, top + verticalScrollPosition,
+             * SCROLLBAR_WIDTH - 2, verticalVisibleAmount, Toolkit
+             * .getColor(ColorsAndFonts.COLOR_SECONDARY1));
+             * 
+             * DrawingUtil.drawHatching(canvas, x + 3, top +
+             * verticalScrollPosition + 4, SCROLLBAR_WIDTH - 6,
+             * verticalVisibleAmount - 8,
+             * Toolkit.getColor(ColorsAndFonts.COLOR_PRIMARY1),
+             * Toolkit.getColor(ColorsAndFonts.COLOR_PRIMARY3));
+             */
+        }
+    }
+
+    private void drawHorizontalScrollBar(final Canvas canvas, final int contentWidth, final int contentHeight, final Color color) {
+        final int horizontalScrollPosition = horizontalScrollBar.getPosition();
+        final int horizontalVisibleAmount = horizontalScrollBar.getVisibleAmount();
+        if (bottom > 0 && (horizontalScrollPosition > left || horizontalVisibleAmount < contentWidth)) {
+            final int x = 0; // left + horizontalScrollPosition;
+            final int y = contentHeight + top;
+            render.draw(canvas, true, x, y, contentWidth, SCROLLBAR_WIDTH, horizontalScrollPosition, horizontalVisibleAmount);
+            /*
+             * canvas.drawSolidRectangle(left, y + 1, contentWidth,
+             * SCROLLBAR_WIDTH - 1,
+             * Toolkit.getColor(ColorsAndFonts.COLOR_SECONDARY3));
+             * canvas.drawSolidRectangle(x, y + 1, horizontalVisibleAmount,
+             * SCROLLBAR_WIDTH - 2, color); canvas.drawRectangle(left, y,
+             * contentWidth, SCROLLBAR_WIDTH,
+             * Toolkit.getColor(ColorsAndFonts.COLOR_SECONDARY2 ));
+             * canvas.drawRectangle(x, y + 1, horizontalVisibleAmount,
+             * SCROLLBAR_WIDTH - 2,
+             * Toolkit.getColor(ColorsAndFonts.COLOR_SECONDARY1));
+             * 
+             * DrawingUtil.drawHatching(canvas, x + 5, y + 3,
+             * horizontalVisibleAmount - 10, SCROLLBAR_WIDTH - 6, Toolkit
+             * .getColor(ColorsAndFonts.COLOR_PRIMARY1),
+             * Toolkit.getColor(ColorsAndFonts.COLOR_PRIMARY3));
+             */
+        }
+    }
+
+    @Override
+    public void firstClick(final Click click) {
+        final int area = adjust(click);
+        switch (area) {
+        case NORTH:
+            topHeader.firstClick(click);
+            break;
+
+        case WEST:
+            leftHeader.firstClick(click);
+            break;
+
+        case CENTER:
+            wrappedView.firstClick(click);
+            break;
+
+        case SOUTH:
+            // TODO allow modified click to move thumb to the pointer, rather
+            // than paging.
+            horizontalScrollBar.firstClick(click.getLocation().getX(), click.button3());
+            break;
+
+        case EAST:
+            verticalScrollBar.firstClick(click.getLocation().getY(), click.button3());
+            break;
+
+        default:
+            break;
+        }
+    }
+
+    @Override
+    public Location getAbsoluteLocation() {
+        final Location location = super.getAbsoluteLocation();
+        location.subtract(offset());
+        return location;
+    }
+
+    @Override
+    public Bounds getBounds() {
+        return new Bounds(getLocation(), getSize());
+    }
+
+    @Override
+    public Size getRequiredSize(final Size maximumSize) {
+        final Size size = wrappedView.getRequiredSize(new Size(maximumSize));
+        if (size.getWidth() > maximumSize.getWidth()) {
+            size.extendHeight(SCROLLBAR_WIDTH);
+        }
+        if (size.getHeight() > maximumSize.getHeight()) {
+            size.extendWidth(SCROLLBAR_WIDTH);
+        }
+        size.extend(left, top);
+        size.limitSize(maximumSize);
+        return size;
+    }
+
+    @Override
+    public Size getSize() {
+        return new Size(size);
+    }
+
+    @Override
+    public View identify(final Location location) {
+        getViewManager().getSpy().addTrace(this, "mouse location within border", location);
+        getViewManager().getSpy().addTrace(this, "non border area", viewportArea());
+
+        final int area = adjust(location);
+        switch (area) {
+        case NORTH:
+            return topHeader.identify(location);
+
+        case WEST:
+            return leftHeader.identify(location);
+
+        case CENTER:
+            return wrappedView.identify(location);
+
+        case SOUTH:
+            getViewManager().getSpy().addTrace(this, "over scroll bar area", viewportArea());
+            return getView();
+
+        case EAST:
+            getViewManager().getSpy().addTrace(this, "over scroll bar area", viewportArea());
+            return getView();
+
+        default:
+            return null;
+        }
+    }
+
+    @Override
+    public void limitBoundsWithin(final Size size) {
+        super.limitBoundsWithin(size);
+        verticalScrollBar.limit();
+        horizontalScrollBar.limit();
+    }
+
+    @Override
+    public void markDamaged(final Bounds bounds) {
+        /*
+         * TODO this only works for the main content area, not for the headers.
+         * how do we figure out which area to adjust for?
+         */
+        final Offset offset = offset();
+        bounds.translate(-offset.getDeltaX(), -offset.getDeltaY());
+        bounds.translate(left, top);
+        super.markDamaged(bounds);
+    }
+
+    @Override
+    public void mouseMoved(final Location location) {
+        final int area = adjust(location);
+        switch (area) {
+        case NORTH:
+            topHeader.mouseMoved(location);
+            break;
+
+        case WEST:
+            leftHeader.mouseMoved(location);
+            break;
+
+        case CENTER:
+            // location.add(offset());
+            // location.move(-left, -top);
+            wrappedView.mouseMoved(location);
+            break;
+
+        case SOUTH:
+        case EAST:
+        default:
+            break;
+        }
+    }
+
+    private Offset offset() {
+        final Bounds contents = viewportArea();
+        final int width = contents.getWidth();
+        final int x = width == 0 ? 0 : horizontalScrollBar.getPosition() * wrappedView.getRequiredSize(Size.createMax()).getWidth() / width;
+        final int height = contents.getHeight();
+        final int y = height == 0 ? 0 : verticalScrollBar.getPosition() * wrappedView.getRequiredSize(Size.createMax()).getHeight() / height;
+        return new Offset(x, y);
+    }
+
+    protected boolean overContent(final Location location) {
+        return viewportArea().contains(location);
+    }
+
+    public void reset() {
+        horizontalScrollBar.reset();
+        verticalScrollBar.reset();
+    }
+
+    /**
+     * Moves the scrollbar to beginning or the end when a double click occurs on
+     * that side.
+     */
+    @Override
+    public void secondClick(final Click click) {
+        final int area = adjust(click);
+        switch (area) {
+        case NORTH:
+            topHeader.secondClick(click);
+            break;
+
+        case WEST:
+            leftHeader.secondClick(click);
+            break;
+
+        case CENTER:
+            wrappedView.secondClick(click);
+            break;
+
+        case SOUTH:
+            horizontalScrollBar.secondClick(click.getLocation().getX());
+            break;
+
+        case EAST:
+            verticalScrollBar.secondClick(click.getLocation().getY());
+            break;
+
+        default:
+            break;
+        }
+    }
+
+    @Override
+    public void thirdClick(final Click click) {
+        final int area = adjust(click);
+        switch (area) {
+        case NORTH:
+            topHeader.thirdClick(click);
+            break;
+
+        case WEST:
+            leftHeader.thirdClick(click);
+            break;
+
+        case CENTER:
+            wrappedView.thirdClick(click);
+            break;
+
+        case SOUTH:
+        case EAST:
+        default:
+            // ignore
+            break;
+        }
+    }
+
+    @Override
+    public void setBounds(final Bounds bounds) {
+        setLocation(bounds.getLocation());
+        setSize(bounds.getSize());
+    }
+
+    @Override
+    public void setSize(final Size size) {
+        // TODO need to restore the offset after size change - see limitBounds
+        // float verticalRatio = ((float) verticalScrollPosition) /
+        // contentArea().getHeight();
+
+        this.size = new Size(size);
+
+        final Size contentSize = wrappedView.getRequiredSize(Size.createMax());
+        wrappedView.setSize(contentSize);
+
+        final int availableHeight2 = size.getHeight() - top;
+        final int contentHeight2 = contentSize.getHeight();
+        right = availableHeight2 >= contentHeight2 ? 0 : SCROLLBAR_WIDTH;
+
+        final int availableWidth2 = size.getWidth() - left;
+        final int contentWidth2 = contentSize.getWidth();
+        bottom = availableWidth2 >= contentWidth2 ? 0 : SCROLLBAR_WIDTH;
+
+        final Bounds viewport = viewportArea();
+
+        final int viewportHeight = viewport.getHeight();
+        final int maxContentHeight = Math.max(viewportHeight, contentSize.getHeight());
+
+        verticalScrollBar.setSize(viewportHeight, maxContentHeight);
+        if (leftHeader != null) {
+            leftHeader.setSize(new Size(left, maxContentHeight));
+        }
+
+        final int viewportWidth = viewport.getWidth();
+        final int maxContentWidth = Math.max(viewportWidth, contentSize.getWidth());
+
+        horizontalScrollBar.setSize(viewportWidth, maxContentWidth);
+        if (topHeader != null) {
+            topHeader.setSize(new Size(maxContentWidth, top));
+        }
+    }
+
+    public int getVerticalPosition() {
+        return verticalScrollBar.getPosition();
+    }
+
+    public int getHorizontalPosition() {
+        return horizontalScrollBar.getPosition();
+    }
+
+    @Override
+    public ViewAreaType viewAreaType(final Location location) {
+        final int area = adjust(location);
+        switch (area) {
+        case NORTH:
+            return topHeader.viewAreaType(location);
+
+        case WEST:
+            return leftHeader.viewAreaType(location);
+
+        case CENTER:
+            return wrappedView.viewAreaType(location);
+
+        case SOUTH:
+        case EAST:
+        default:
+            return ViewAreaType.INTERNAL;
+        }
+    }
+
+    @Override
+    public void viewMenuOptions(final UserActionSet menuOptions) {
+        super.viewMenuOptions(menuOptions);
+        menuOptions.add(new UserActionAbstract("Reset scroll border", ActionType.DEBUG) {
+            @Override
+            public void execute(final Workspace workspace, final View view, final Location at) {
+                reset();
+                invalidateLayout();
+            }
+        });
+    }
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/eb613703/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/SelectObjectBorder.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/SelectObjectBorder.java b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/SelectObjectBorder.java
new file mode 100644
index 0000000..c99271d
--- /dev/null
+++ b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/SelectObjectBorder.java
@@ -0,0 +1,130 @@
+/*
+ *  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.border;
+
+import java.awt.event.KeyEvent;
+
+import org.apache.isis.core.commons.debug.DebugBuilder;
+import org.apache.isis.viewer.dnd.drawing.Canvas;
+import org.apache.isis.viewer.dnd.drawing.ColorsAndFonts;
+import org.apache.isis.viewer.dnd.drawing.Location;
+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.KeyboardAction;
+import org.apache.isis.viewer.dnd.view.SubviewDecorator;
+import org.apache.isis.viewer.dnd.view.Toolkit;
+import org.apache.isis.viewer.dnd.view.UserActionSet;
+import org.apache.isis.viewer.dnd.view.View;
+import org.apache.isis.viewer.dnd.view.ViewAxis;
+import org.apache.isis.viewer.dnd.view.Workspace;
+import org.apache.isis.viewer.dnd.view.base.AbstractBorder;
+import org.apache.isis.viewer.dnd.view.option.UserActionAbstract;
+
+public class SelectObjectBorder extends AbstractBorder {
+    private final SelectableViewAxis axis;
+
+    public static class Factory implements SubviewDecorator {
+        @Override
+        public ViewAxis createAxis(final Content content) {
+            return null;
+        }
+
+        @Override
+        public View decorate(final Axes axes, final View view) {
+            if (axes.contains(SelectableViewAxis.class)) {
+                final SelectableViewAxis axis = axes.getAxis(SelectableViewAxis.class);
+                return new SelectObjectBorder(view, axis);
+            } else {
+                return view;
+            }
+        }
+    }
+
+    protected SelectObjectBorder(final View view, final SelectableViewAxis axis) {
+        super(view);
+        this.axis = axis;
+    }
+
+    @Override
+    public Axes getViewAxes() {
+        final Axes viewAxes = super.getViewAxes();
+        viewAxes.add(axis);
+        return viewAxes;
+    }
+
+    @Override
+    protected void debugDetails(final DebugBuilder debug) {
+        super.debugDetails(debug);
+        debug.appendln("axis", axis);
+    }
+
+    @Override
+    public void keyPressed(final KeyboardAction key) {
+        if (key.getKeyCode() == KeyEvent.VK_SPACE) {
+            selectNode();
+        } else {
+            super.keyPressed(key);
+        }
+    }
+
+    @Override
+    public void firstClick(final Click click) {
+        final int x = click.getLocation().getX();
+        final int y = click.getLocation().getY();
+        if (withinSelectorBounds(x, y) && click.button1()) {
+            selectNode();
+        } else {
+            super.firstClick(click);
+        }
+    }
+
+    private void selectNode() {
+        axis.selected(getView());
+    }
+
+    private boolean withinSelectorBounds(final int x, final int y) {
+        return true;
+    }
+
+    @Override
+    public void viewMenuOptions(final UserActionSet options) {
+        super.viewMenuOptions(options);
+        options.add(new UserActionAbstract("Select node") {
+            @Override
+            public void execute(final Workspace workspace, final View view, final Location at) {
+                selectNode();
+            }
+
+            @Override
+            public String getDescription(final View view) {
+                return "Show this node in the right-hand pane";
+            }
+        });
+    }
+
+    @Override
+    public void draw(final Canvas canvas) {
+        if (axis.isSelected(getView())) {
+            clearBackground(canvas, Toolkit.getColor(ColorsAndFonts.COLOR_SECONDARY3));
+        }
+        super.draw(canvas);
+    }
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/eb613703/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/SelectableViewAxis.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/SelectableViewAxis.java b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/SelectableViewAxis.java
new file mode 100644
index 0000000..b763a66
--- /dev/null
+++ b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/border/SelectableViewAxis.java
@@ -0,0 +1,51 @@
+/*
+ *  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.border;
+
+import org.apache.isis.core.commons.lang.ToString;
+import org.apache.isis.viewer.dnd.view.Selectable;
+import org.apache.isis.viewer.dnd.view.View;
+import org.apache.isis.viewer.dnd.view.ViewAxis;
+
+public class SelectableViewAxis implements ViewAxis {
+    private View selectedView;
+    private final Selectable target;
+
+    public SelectableViewAxis(final Selectable view) {
+        target = view;
+    }
+
+    public void selected(final View view) {
+        selectedView = view;
+        target.setSelectedNode(selectedView);
+    }
+
+    public boolean isSelected(final View view) {
+        return selectedView == view;
+    }
+
+    @Override
+    public String toString() {
+        final ToString s = new ToString(this);
+        s.append("target", target.getId());
+        s.append("selected", selectedView == null ? "none" : selectedView.getId());
+        return s.toString();
+    }
+}