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

[28/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/option/UserActionAbstract.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/option/UserActionAbstract.java b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/option/UserActionAbstract.java
new file mode 100644
index 0000000..bc79070
--- /dev/null
+++ b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/option/UserActionAbstract.java
@@ -0,0 +1,96 @@
+/*
+ *  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.core.metamodel.consent.Allow;
+import org.apache.isis.core.metamodel.consent.Consent;
+import org.apache.isis.core.metamodel.spec.ActionType;
+import org.apache.isis.viewer.dnd.drawing.Location;
+import org.apache.isis.viewer.dnd.view.UserAction;
+import org.apache.isis.viewer.dnd.view.View;
+import org.apache.isis.viewer.dnd.view.Workspace;
+
+/**
+ * Each option that a user is shown in an objects popup menu a MenuOption. A
+ * MenuOption details: the name of an option (in the users language);
+ * <ul>
+ * the type of object that might result when requesting this option
+ * </ul>
+ * ; a way to determine whether a user can select this option on the current
+ * object.
+ */
+public abstract class UserActionAbstract implements UserAction {
+    private String description;
+    private String name;
+    private final ActionType type;
+
+    public UserActionAbstract(final String name) {
+        this(name, ActionType.USER);
+    }
+
+    public UserActionAbstract(final String name, final ActionType type) {
+        this.name = name;
+        this.type = type;
+    }
+
+    @Override
+    public Consent disabled(final View view) {
+        return Allow.DEFAULT;
+    }
+
+    @Override
+    public abstract void execute(final Workspace workspace, final View view, final Location at);
+
+    @Override
+    public String getDescription(final View view) {
+        return description;
+    }
+
+    @Override
+    public String getHelp(final View view) {
+        return "No help available for user action";
+    }
+
+    /**
+     * Returns the stored name of the menu option.
+     */
+    @Override
+    public String getName(final View view) {
+        return name;
+    }
+
+    @Override
+    public ActionType getType() {
+        return type;
+    }
+
+    public void setName(final String name) {
+        this.name = name;
+    }
+
+    @Override
+    public String toString() {
+        final ToString str = new ToString(this);
+        str.append("name", name);
+        str.append("type", type);
+        return str.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/text/CursorPosition.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/text/CursorPosition.java b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/text/CursorPosition.java
new file mode 100644
index 0000000..d5d9d2a
--- /dev/null
+++ b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/text/CursorPosition.java
@@ -0,0 +1,261 @@
+/*
+ *  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.text;
+
+import org.apache.isis.viewer.dnd.drawing.Location;
+
+/**
+ * Represents the position of a line cursor within a TextContent. The character
+ * position for a line extends from zero to text.length + 1. Where 0 means the
+ * cursor is to the left of the first character, and 1 is to right of the first
+ * character and to the left of the second character.
+ */
+public class CursorPosition {
+    private int character;
+    private int line;
+    private final TextContent textContent;
+
+    public CursorPosition(final TextContent content, final CursorPosition pos) {
+        this(content, pos.line, pos.character);
+    }
+
+    public CursorPosition(final TextContent content, final int line, final int afterCharacter) {
+        this.textContent = content;
+        this.line = line;
+        this.character = afterCharacter;
+    }
+
+    public void asFor(final CursorPosition pos) {
+        line = pos.line;
+        character = pos.character;
+    }
+
+    /**
+     * Move the cursor to the bottom-right of the field
+     */
+    public void bottom() {
+        line = textContent.getNoLinesOfContent() - 1;
+        textContent.alignDisplay(line);
+        end();
+    }
+
+    public void cursorAt(final Location atLocation) {
+        line = textContent.cursorAtLine(atLocation);
+        character = textContent.cursorAtCharacter(atLocation, line);
+
+        if (line >= textContent.getNoLinesOfContent()) {
+            line = textContent.getNoLinesOfContent() - 1;
+            end();
+        }
+    }
+
+    /**
+     * Move the cursor to the end of the line
+     */
+    public void end() {
+        final String text = textContent.getText(line);
+        character = text == null ? 0 : text.length();
+    }
+
+    /**
+     * @return the character within this line.
+     */
+    public int getCharacter() {
+        return character;
+    }
+
+    /**
+     * @return the line within the field
+     */
+    public int getLine() {
+        return line;
+    }
+
+    /**
+     * Move the cursor to the left end of the field
+     */
+    public void home() {
+        character = 0;
+    }
+
+    /**
+     * Movet the cursor left by one character.
+     */
+    public void left() {
+        if (!((line == 0) && (character == 0))) {
+            character--;
+
+            if (character < 0) {
+                line--;
+                textContent.alignDisplay(line);
+                end();
+            }
+        }
+    }
+
+    /**
+     * Move down one line.
+     */
+    public void lineDown() {
+        moveDown(1);
+    }
+
+    /**
+     * Move up one line.
+     */
+    public void lineUp() {
+        moveUp(1);
+    }
+
+    private void moveDown(final int byLines) {
+        final int size = textContent.getNoLinesOfContent();
+
+        if (line < (size - 1)) {
+            line += byLines;
+            line = Math.min(size - 1, line);
+
+            character = Math.min(character, textContent.getText(line).length());
+
+            textContent.alignDisplay(line);
+        }
+    }
+
+    private void moveUp(final int byLines) {
+        if (line > 0) {
+            line -= byLines;
+            line = Math.max(0, line);
+            textContent.alignDisplay(line);
+        }
+    }
+
+    /**
+     * Move down one page.
+     */
+    public void pageDown() {
+        moveDown(textContent.getNoDisplayLines() - 1);
+    }
+
+    /**
+     * Move cursor up by a page
+     */
+    public void pageUp() {
+        moveUp(textContent.getNoDisplayLines() - 1);
+    }
+
+    /**
+     * Move the cursor right by one character.
+     */
+    public void right() {
+        right(1);
+    }
+
+    /**
+     * Move the cursor right by one character.
+     */
+    public void right(final int characters) {
+        final int length = textContent.getText(line).length();
+
+        if ((character + characters) > length) {
+            if ((line + 1) < textContent.getNoLinesOfContent()) {
+                line++;
+                textContent.alignDisplay(line);
+
+                final int remainder = (character + characters) - length;
+                character = 0;
+                right(remainder);
+            }
+        } else {
+            character += characters;
+        }
+    }
+
+    /**
+     * Move the cursor to the top-left of the field
+     */
+    public void top() {
+        line = 0;
+        character = 0;
+        textContent.alignDisplay(line);
+    }
+
+    @Override
+    public String toString() {
+        return "CursorPosition [line=" + line + ",character=" + character + "]";
+    }
+
+    /**
+     * Move the cursor left to the beginning of the previous word.
+     */
+    public void wordLeft() {
+        if (!((line == 0) && (character == 0))) {
+            if (character == 0) {
+                line--;
+                end();
+            }
+
+            final String text = textContent.getText(line);
+
+            do {
+                character--;
+            } while ((character >= 0) && (text.charAt(character) == ' '));
+
+            while ((character >= 0) && (text.charAt(character) != ' ')) {
+                character--;
+            }
+
+            character++;
+        }
+    }
+
+    /**
+     * Move the cursor right to the end of the current word.
+     */
+    public void wordRight() {
+        final String text = textContent.getText(line);
+        final int lineLength = text.length();
+        if (!(line == textContent.getNoLinesOfContent() - 1 && character == lineLength - 1)) {
+            // skip spaces before
+            while (character < lineLength && text.charAt(character) == ' ') {
+                character++;
+            }
+            // skip characters (until next space)
+            while (character < lineLength && text.charAt(character) != ' ') {
+                character++;
+            }
+            // skip spaces after word
+            while (character < lineLength && text.charAt(character) == ' ') {
+                character++;
+            }
+            // wrap to nexrt line if at end
+            if (character >= lineLength && line + 1 < textContent.getNoLinesOfContent()) {
+                line++;
+                character = 0;
+            }
+        }
+    }
+
+    public boolean samePosition(final CursorPosition positionToCompare) {
+        return line == positionToCompare.line && character == positionToCompare.character;
+    }
+
+    public boolean isBefore(final CursorPosition positionToCompare) {
+        return line < positionToCompare.line || (line == positionToCompare.line && character < positionToCompare.character);
+    }
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/eb613703/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/text/ObjectTitleText.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/text/ObjectTitleText.java b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/text/ObjectTitleText.java
new file mode 100644
index 0000000..1bb723c
--- /dev/null
+++ b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/text/ObjectTitleText.java
@@ -0,0 +1,41 @@
+/*
+ *  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.text;
+
+import org.apache.isis.viewer.dnd.drawing.ColorsAndFonts;
+import org.apache.isis.viewer.dnd.drawing.Text;
+import org.apache.isis.viewer.dnd.view.Content;
+import org.apache.isis.viewer.dnd.view.Toolkit;
+import org.apache.isis.viewer.dnd.view.View;
+
+public class ObjectTitleText extends TitleText {
+    private final Content content;
+
+    public ObjectTitleText(final View view, final Text style) {
+        super(view, style, Toolkit.getColor(ColorsAndFonts.COLOR_BLACK));
+        content = view.getContent();
+    }
+
+    @Override
+    protected String title() {
+        return content.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/text/TextBlock.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/text/TextBlock.java b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/text/TextBlock.java
new file mode 100644
index 0000000..5747882
--- /dev/null
+++ b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/text/TextBlock.java
@@ -0,0 +1,223 @@
+/*
+ *  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.text;
+
+import org.apache.log4j.Logger;
+
+class TextBlock {
+    private static final Logger LOG = Logger.getLogger(TextBlock.class);
+    private static final Logger UI_LOG = Logger.getLogger("ui." + TextBlock.class.getName());
+    private final TextBlockTarget forField;
+    private String text;
+    private int[] lineBreaks;
+    private boolean isFormatted;
+    private int lineCount;
+    private boolean canWrap;
+
+    TextBlock(final TextBlockTarget forField, final String text, final boolean canWrap) {
+        this.forField = forField;
+        this.text = text;
+        isFormatted = false;
+        this.canWrap = canWrap;
+    }
+
+    public String getLine(final int line) {
+        if (line < 0 || line > lineCount) {
+            throw new IllegalArgumentException("line outside of block " + line);
+        }
+
+        format();
+
+        final int from = lineStart(line);
+        final int to = lineEnd(line);
+
+        return text.substring(from, to);
+    }
+
+    public String getText() {
+        return text;
+    }
+
+    public void deleteLeft(final int line, final int character) {
+        final int pos = pos(line, character);
+        if (pos > 0) {
+            text = text.substring(0, pos - 1) + text.substring(pos);
+            isFormatted = false;
+        }
+    }
+
+    public void delete(final int fromLine, final int fromCharacter, final int toLine, final int toCharacter) {
+        format();
+        final int from = pos(fromLine, fromCharacter);
+        final int to = pos(toLine, toCharacter);
+        text = text.substring(0, from) + text.substring(to);
+        isFormatted = false;
+    }
+
+    public void deleteTo(final int toLine, final int toCharacter) {
+        format();
+        final int from = 0;
+        final int to = pos(toLine, toCharacter);
+        text = text.substring(0, from) + text.substring(to);
+        isFormatted = false;
+    }
+
+    public void deleteFrom(final int fromLine, final int fromCharacter) {
+        format();
+        final int from = pos(fromLine, fromCharacter);
+        final int to = text.length();
+        text = text.substring(0, from) + text.substring(to);
+        isFormatted = false;
+    }
+
+    public void deleteRight(final int line, final int character) {
+        final int pos = pos(line, character);
+        if (pos < text.length()) {
+            text = text.substring(0, pos) + text.substring(pos + 1);
+            isFormatted = false;
+        }
+    }
+
+    public int noLines() {
+        format();
+        return lineCount + 1;
+    }
+
+    private void breakAt(final int breakAt) {
+        // TODO deal with growing array
+        lineBreaks[lineCount] = breakAt;
+        lineCount++;
+    }
+
+    private void format() {
+        if (canWrap && !isFormatted) {
+            lineBreaks = new int[100];
+            lineCount = 0;
+
+            final int length = text.length();
+
+            int lineWidth = 0;
+            int breakAt = -1;
+
+            for (int pos = 0; pos < length; pos++) {
+                final char ch = text.charAt(pos);
+
+                if (ch == '\n') {
+                    throw new IllegalStateException("Block must not contain newline characters");
+                }
+
+                lineWidth += forField.getText().charWidth(ch);
+
+                if (lineWidth > forField.getMaxFieldWidth()) {
+                    breakAt = (breakAt == -1) ? pos - 1 : breakAt;
+                    // ensures that a string without spaces doesn't loop forever
+                    breakAt(breakAt);
+
+                    // include the remaining chars in the starting width.
+                    lineWidth = forField.getText().stringWidth(text.substring(breakAt - 1, pos + 1));
+
+                    // reset for next line
+                    // start = breakAt;
+                    breakAt = -1;
+
+                    continue;
+                }
+
+                if (ch == ' ') {
+                    breakAt = pos + 1; // break at the character after the space
+                }
+            }
+
+            isFormatted = true;
+        }
+    }
+
+    public void insert(final int line, final int character, final String characters) {
+        if (characters.indexOf('\n') >= 0) {
+            throw new IllegalArgumentException("Insert characters cannot contain newline");
+        }
+        final int pos = pos(line, character);
+        text = text.substring(0, pos) + characters + text.substring(pos);
+        isFormatted = false;
+    }
+
+    private int pos(final int line, final int character) {
+        int pos = lineStart(line);
+        pos += character;
+        LOG.debug("position " + pos);
+        return pos;
+    }
+
+    private int lineStart(final int line) {
+        final int pos = line == 0 ? 0 : lineBreaks[line - 1];
+        UI_LOG.debug("line " + line + " starts at " + pos);
+        return pos;
+    }
+
+    private int lineEnd(final int line) {
+        final int pos = line >= lineCount ? text.length() : lineBreaks[line];
+        UI_LOG.debug("line " + line + " ends at " + pos);
+        return pos;
+    }
+
+    /**
+     * breaks a block at the cursor position by truncating this block and
+     * creating a new block and adding the removed text.
+     */
+    public TextBlock splitAt(final int line, final int character) {
+        format();
+        final int pos = pos(line, character);
+        final TextBlock newBlock = new TextBlock(forField, text.substring(pos), canWrap);
+        text = text.substring(0, pos);
+        isFormatted = false;
+        return newBlock;
+    }
+
+    public void setCanWrap(final boolean canWrap) {
+        this.canWrap = canWrap;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuffer content = new StringBuffer();
+        content.append("TextBlock [");
+        content.append("formatted=");
+        content.append(isFormatted);
+        content.append(",lines=");
+        content.append(lineCount);
+        content.append(",text=");
+        content.append(text);
+        content.append(",breaks=");
+        if (lineBreaks == null) {
+            content.append("none");
+        } else {
+            for (int i = 0; i < lineBreaks.length; i++) {
+                content.append(i == 0 ? "" : ",");
+                content.append(lineBreaks[i]);
+            }
+        }
+        content.append("]");
+        return content.toString();
+    }
+
+    public void join(final TextBlock textBlock) {
+        text += textBlock.text;
+    }
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/eb613703/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/text/TextBlockTarget.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/text/TextBlockTarget.java b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/text/TextBlockTarget.java
new file mode 100644
index 0000000..309e5c7
--- /dev/null
+++ b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/text/TextBlockTarget.java
@@ -0,0 +1,32 @@
+/*
+ *  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.text;
+
+import org.apache.isis.viewer.dnd.drawing.Text;
+
+public interface TextBlockTarget {
+
+    /**
+     * Maximum text entry width.
+     */
+    int getMaxFieldWidth();
+
+    Text getText();
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/eb613703/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/text/TextContent.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/text/TextContent.java b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/text/TextContent.java
new file mode 100644
index 0000000..97bf245
--- /dev/null
+++ b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/text/TextContent.java
@@ -0,0 +1,372 @@
+/*
+ *  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.text;
+
+import java.util.Enumeration;
+import java.util.Vector;
+
+import org.apache.log4j.Logger;
+
+import org.apache.isis.core.commons.ensure.Assert;
+import org.apache.isis.core.commons.exceptions.IsisException;
+import org.apache.isis.core.commons.lang.ToString;
+import org.apache.isis.viewer.dnd.drawing.Location;
+
+public class TextContent {
+    private static final Logger LOG = Logger.getLogger(TextContent.class);
+    private static final Logger UI_LOG = Logger.getLogger("ui." + TextContent.class.getName());
+    public static final int NO_WRAPPING = 1;
+    public static final int WRAPPING = 0;
+    private final Vector blocks;
+    private final TextBlockTarget target;
+    private int displayFromLine;
+    private int availableDisplayLines;
+    private final boolean useEmptyLines;
+    private final int wrap;
+
+    public TextContent(final TextBlockTarget target, final int noLines, final int wrapStyle) {
+        this(target, noLines, wrapStyle, false);
+    }
+
+    public TextContent(final TextBlockTarget target, final int noLines, final int wrapStyle, final boolean useEmptyLines) {
+        this.target = target;
+        this.wrap = wrapStyle;
+        this.blocks = new Vector();
+        this.useEmptyLines = useEmptyLines;
+        availableDisplayLines = noLines;
+        displayFromLine = 0;
+        addBlock("");
+        alignDisplay(0);
+    }
+
+    private void addBlock(final String text) {
+        final TextBlock block = new TextBlock(target, text, wrap == TextContent.WRAPPING);
+        LOG.debug("add block " + block);
+        blocks.addElement(block);
+    }
+
+    /**
+     * Returns the number of lines that this field will display the content.
+     * This can be smaller than the actual number of lines of content, but will
+     * be at least one.
+     */
+    public int getNoDisplayLines() {
+        return availableDisplayLines;
+    }
+
+    /**
+     * Aligns the lines of content so that the specified line is within the
+     * array of lines returned by getDisplayLines().
+     * 
+     * @see #getDisplayLines()
+     */
+    public void alignDisplay(final int line) {
+        final int noContentLines = getNoLinesOfContent();
+        final int lastLine = noContentLines - 1;
+
+        int displayToLine = Math.min(displayFromLine + availableDisplayLines, noContentLines);
+        if (noContentLines <= availableDisplayLines) {
+            displayFromLine = 0;
+        } else {
+            if (line >= displayToLine) {
+                displayToLine = line + 3;
+                displayToLine = Math.min(displayToLine, lastLine);
+
+                displayFromLine = displayToLine - availableDisplayLines + 1;
+                displayFromLine = Math.max(displayFromLine, 0);
+            }
+
+            if (line < displayFromLine) {
+                displayFromLine = line;
+                displayToLine = (displayFromLine + availableDisplayLines) - 1;
+
+                if (displayToLine >= noContentLines) {
+                    displayToLine = lastLine;
+                    displayFromLine = Math.max(0, displayToLine - availableDisplayLines);
+                }
+            }
+        }
+
+        LOG.debug("display line " + line + " " + displayFromLine + "~" + displayToLine);
+    }
+
+    public void breakBlock(final CursorPosition cursorAt) {
+        final BlockToLineMapping mapping = findBlockFor(cursorAt.getLine());
+        final TextBlock newBlock = mapping.textBlock.splitAt(mapping.line, cursorAt.getCharacter());
+        blocks.insertElementAt(newBlock, mapping.index + 1);
+    }
+
+    /**
+     * deletes the selected text
+     */
+    public void delete(final TextSelection selection) {
+        final CursorPosition from = selection.from();
+        final CursorPosition to = selection.to();
+
+        final BlockToLineMapping fromMapping = findBlockFor(from.getLine());
+        final int fromBlock = fromMapping.index;
+        final int fromLine = fromMapping.line;
+        final int fromCharacter = from.getCharacter();
+
+        final BlockToLineMapping toMapping = findBlockFor(to.getLine());
+        final int toBlock = toMapping.index;
+        final int toLine = toMapping.line;
+        final int toCharacter = to.getCharacter();
+
+        if (fromBlock == toBlock) {
+            final TextBlock block = (TextBlock) blocks.elementAt(fromBlock);
+            block.delete(fromLine, fromCharacter, toLine, toCharacter);
+        } else {
+            TextBlock block = (TextBlock) blocks.elementAt(toBlock);
+            block.deleteTo(toLine, toCharacter);
+
+            block = (TextBlock) blocks.elementAt(fromBlock);
+            block.deleteFrom(fromLine, fromCharacter);
+
+            fromMapping.textBlock.join(toMapping.textBlock);
+            blocks.removeElementAt(toMapping.index);
+
+            for (int i = fromBlock + 1; i < toBlock; i++) {
+                blocks.removeElementAt(i);
+            }
+        }
+    }
+
+    public void deleteLeft(final CursorPosition cursorAt) {
+        final BlockToLineMapping mapping = findBlockFor(cursorAt.getLine());
+        if (mapping == null || mapping.textBlock == null) {
+            throw new IsisException("invalid block " + mapping + " for line " + cursorAt.getLine());
+        }
+        mapping.textBlock.deleteLeft(mapping.line, cursorAt.getCharacter());
+    }
+
+    public void deleteRight(final CursorPosition cursorAt) {
+        final BlockToLineMapping mapping = findBlockFor(cursorAt.getLine());
+        mapping.textBlock.deleteRight(mapping.line, cursorAt.getCharacter());
+    }
+
+    private BlockToLineMapping findBlockFor(final int line) {
+        if (line < 0) {
+            throw new IllegalArgumentException("Line must be greater than, or equal to, zero: " + line);
+        }
+
+        int lineWithinBlock = line;
+        for (int i = 0; i < blocks.size(); i++) {
+            final TextBlock block = (TextBlock) blocks.elementAt(i);
+            final int noLines = block.noLines();
+            if (lineWithinBlock < noLines) {
+                UI_LOG.debug("block " + i + ", line " + lineWithinBlock);
+                return new BlockToLineMapping(i, block, lineWithinBlock);
+            }
+            lineWithinBlock -= noLines;
+        }
+        return null;
+        // throw new IllegalArgumentException("line number not valid " + line);
+
+    }
+
+    /**
+     * returns the entire text of the content, with a newline between each block
+     * (but not after the final block.
+     */
+    public String getText() {
+        final StringBuffer content = new StringBuffer();
+        final Enumeration e = blocks.elements();
+        while (e.hasMoreElements()) {
+            final TextBlock block = (TextBlock) e.nextElement();
+            if (content.length() > 0) {
+                content.append("\n");
+            }
+            content.append(block.getText());
+        }
+        return content.toString();
+    }
+
+    /**
+     * returns the text on the specified line
+     */
+    public String getText(final int forLine) {
+        final BlockToLineMapping block = findBlockFor(forLine);
+        if (block == null) {
+            return null;
+        }
+        return block.textBlock.getLine(block.line);
+    }
+
+    /**
+     * returns only the text that is selected
+     */
+    public String getText(final TextSelection selection) {
+        final CursorPosition from = selection.from();
+        final CursorPosition to = selection.to();
+
+        final int line = from.getLine();
+        String text = getText(line);
+        if (from.getLine() == to.getLine()) {
+            return text.substring(from.getCharacter(), to.getCharacter());
+
+        } else {
+            final StringBuffer str = new StringBuffer();
+            str.append(text.substring(from.getCharacter()));
+            for (int i = line + 1; i < line + (to.getLine() - from.getLine()); i++) {
+                text = getText(i);
+                str.append(text);
+            }
+            text = getText(line + (to.getLine() - from.getLine()));
+            str.append(text.substring(0, to.getCharacter()));
+            return str.toString();
+        }
+    }
+
+    public void insert(final CursorPosition cursorAt, final String characters) {
+        Assert.assertNotNull(cursorAt);
+
+        final BlockToLineMapping block = findBlockFor(cursorAt.getLine());
+
+        Assert.assertNotNull("failed to get block for line " + cursorAt.getLine(), block);
+
+        block.textBlock.insert(block.line, cursorAt.getCharacter(), characters);
+    }
+
+    /**
+     * Returns the number of lines required to display the content text in it
+     * entirety.
+     */
+    public int getNoLinesOfContent() {
+        int lineCount = 0;
+        final Enumeration e = blocks.elements();
+        while (e.hasMoreElements()) {
+            lineCount += ((TextBlock) e.nextElement()).noLines();
+        }
+        return lineCount;
+    }
+
+    public void setText(final String text) {
+        blocks.removeAllElements();
+
+        if (text == null || text.equals("")) {
+            addBlock("");
+        } else {
+            final String[] tokens = text.split("\\n");
+            for (final String token : tokens) {
+                if (useEmptyLines || token.length() > 0) {
+                    addBlock(token);
+                }
+            }
+        }
+    }
+
+    @Override
+    public String toString() {
+        final ToString content = new ToString(this);
+        content.append("field", target);
+        content.append("lines", availableDisplayLines);
+        content.append("blocks=", blocks.size());
+        /*
+         * for (int i = 0; i < blocks.size(); i++) { content.append(i == 0 ? " "
+         * : "\n "); content.append(blocks.elementAt(i)); }
+         */
+        return content.toString();
+    }
+
+    public String[] getDisplayLines() {
+        final String[] lines = new String[availableDisplayLines];
+        for (int i = 0, j = displayFromLine; i < lines.length; i++, j++) {
+            final String line = getText(j);
+            lines[i] = line == null ? "" : line;
+        }
+        return lines;
+    }
+
+    public int getDisplayFromLine() {
+        return displayFromLine;
+    }
+
+    public void setNoDisplayLines(final int noDisplayLines) {
+        this.availableDisplayLines = noDisplayLines;
+    }
+
+    public void increaseDepth() {
+        availableDisplayLines++;
+    }
+
+    public boolean decreaseDepth() {
+        if (availableDisplayLines > 1) {
+            availableDisplayLines--;
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    private static class BlockToLineMapping {
+        TextBlock textBlock;
+        int index;
+        int line;
+
+        public BlockToLineMapping(final int blockIndex, final TextBlock block, final int line) {
+            this.index = blockIndex;
+            this.textBlock = block;
+            this.line = line;
+        }
+    }
+
+    int cursorAtLine(final Location atLocation) {
+        LOG.debug("pointer at " + atLocation);
+        final int y = atLocation.getY();
+        int lineIndex = displayFromLine + (y / target.getText().getLineHeight());
+        lineIndex = Math.max(lineIndex, 0);
+        return lineIndex;
+    }
+
+    int cursorAtCharacter(final Location atLocation, final int lineOffset) {
+        final String text = getText(lineOffset);
+        if (text == null) {
+            for (int i = lineOffset; i >= 0; i--) {
+                final String text2 = getText(i);
+                if (text2 != null) {
+                    final int at = text2.length();
+                    LOG.debug("character at " + at + " line " + lineOffset);
+                    return at;
+                }
+            }
+        }
+
+        /*
+         * slightly offsetting mouse helps the user position the cursor between
+         * characters near the pointer rather than always after the pointer
+         */
+        final int x = atLocation.getX() - 3;
+
+        int at = 0;
+        final int endAt = text.length();
+
+        int width = 0;
+
+        while (at < endAt && x > width) {
+            width += target.getText().charWidth(text.charAt(at));
+            at++;
+        }
+
+        LOG.debug("character at " + at + " line " + lineOffset);
+        return at;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/eb613703/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/text/TextSelection.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/text/TextSelection.java b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/text/TextSelection.java
new file mode 100644
index 0000000..ff78e7b
--- /dev/null
+++ b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/text/TextSelection.java
@@ -0,0 +1,99 @@
+/*
+ *  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.text;
+
+import org.apache.isis.viewer.dnd.drawing.Location;
+
+public class TextSelection {
+    private final CursorPosition cursor;
+    private final CursorPosition start;
+
+    public TextSelection(final TextContent content) {
+        this.cursor = new CursorPosition(content, 0, 0);
+        this.start = new CursorPosition(content, 0, 0);
+    }
+
+    /**
+     * Determine if the selection is back to front. Returns true if the cursor
+     * position is before the start position.
+     */
+    private boolean backwardSelection() {
+        return cursor.isBefore(start);
+    }
+
+    public void extendTo(final CursorPosition pos) {
+        cursor.asFor(pos);
+    }
+
+    /**
+     * extends the selection so the end point is the same as the cursor.
+     */
+    public void extendTo(final Location at) {
+        cursor.cursorAt(at);
+    }
+
+    public CursorPosition from() {
+        return backwardSelection() ? cursor : start;
+    }
+
+    // private CursorPosition end = new CursorPosition(0,0);
+
+    /**
+     * returns true is a selection exists - if the start and end locations are
+     * not the same
+     */
+    public boolean hasSelection() {
+        return !cursor.samePosition(start);
+    }
+
+    /**
+     * clears the selection so nothing is selected. The start and end points are
+     * set to the same values as the cursor.
+     */
+    public void resetTo(final CursorPosition pos) {
+        start.asFor(pos);
+        cursor.asFor(pos);
+    }
+
+    public void selectSentence() {
+        resetTo(cursor);
+        start.home();
+        cursor.end();
+    }
+
+    /**
+     * set the selection to be for the word marked by the current cursor
+     * 
+     */
+    public void selectWord() {
+        resetTo(cursor);
+        start.wordLeft();
+        cursor.wordRight();
+    }
+
+    public CursorPosition to() {
+        return backwardSelection() ? start : cursor;
+    }
+
+    @Override
+    public String toString() {
+        return "Selection [from=" + start.getLine() + ":" + start.getCharacter() + ",to=" + cursor.getLine() + ":" + cursor.getCharacter() + "]";
+    }
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/eb613703/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/text/TextUtils.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/text/TextUtils.java b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/text/TextUtils.java
new file mode 100644
index 0000000..be45cfd
--- /dev/null
+++ b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/text/TextUtils.java
@@ -0,0 +1,63 @@
+/*
+ *  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.text;
+
+import org.apache.isis.viewer.dnd.drawing.Text;
+
+public class TextUtils {
+
+    private TextUtils() {
+    }
+
+    public static String limitText(final String xtext, final Text style, final int maxWidth) {
+        String text = xtext;
+        final int ellipsisWidth = style.stringWidth("...");
+        if (maxWidth > 0 && style.stringWidth(text) > maxWidth) {
+            int lastCharacterWithinAllowedWidth = 0;
+            for (int textWidth = ellipsisWidth; textWidth <= maxWidth;) {
+                final char character = text.charAt(lastCharacterWithinAllowedWidth);
+                textWidth += style.charWidth(character);
+                lastCharacterWithinAllowedWidth++;
+            }
+
+            int space = text.lastIndexOf(' ', lastCharacterWithinAllowedWidth - 1);
+            if (space > 0) {
+                while (space >= 0) {
+                    final char character = text.charAt(space - 1);
+                    if (Character.isLetterOrDigit(character)) {
+                        break;
+                    }
+                    space--;
+                }
+
+                text = text.substring(0, space);
+            } else {
+                if (lastCharacterWithinAllowedWidth > 0) {
+                    text = text.substring(0, lastCharacterWithinAllowedWidth - 1);
+                } else {
+                    text = "";
+                }
+            }
+            text += "...";
+        }
+        return text;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/eb613703/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/text/TitleText.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/text/TitleText.java b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/text/TitleText.java
new file mode 100644
index 0000000..6f414a3
--- /dev/null
+++ b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/text/TitleText.java
@@ -0,0 +1,125 @@
+/*
+ *  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.text;
+
+import org.apache.isis.core.commons.lang.ToString;
+import org.apache.isis.core.metamodel.adapter.ResolveException;
+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.Size;
+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.ViewState;
+
+/**
+ * TitleText draws the text derived from the subclass within a view. The text is
+ * properly truncated if longer than the specified maximum width.
+ */
+public abstract class TitleText {
+    private static final int NO_MAX_WIDTH = -1;
+    private final Color color;
+    private final Text style;
+    private final View view;
+    private boolean resolveFailure;
+
+    public TitleText(final View view, final Text style, final Color color) {
+        this.view = view;
+        this.style = style;
+        this.color = color;
+    }
+
+    /**
+     * Draw this TitleText's text stating from the specified x coordination and
+     * on the specified baseline.
+     */
+    public void draw(final Canvas canvas, final int x, final int baseline) {
+        draw(canvas, x, baseline, NO_MAX_WIDTH);
+    }
+
+    /**
+     * Draw this TitleText's text stating from the specified x coordination and
+     * on the specified baseline. If a maximum width is specified (ie it is
+     * positive) then the text drawn will not extend past that width.
+     * 
+     * @param maxWidth
+     *            the maximum width to display the text within; if negative no
+     *            limit is imposed
+     */
+    public void draw(final Canvas canvas, final int x, final int baseline, final int maxWidth) {
+        Color color;
+        final ViewState state = view.getState();
+        if (resolveFailure) {
+            color = Toolkit.getColor(ColorsAndFonts.COLOR_ERROR);
+        } else if (state.canDrop()) {
+            color = Toolkit.getColor(ColorsAndFonts.COLOR_VALID);
+        } else if (state.cantDrop()) {
+            color = Toolkit.getColor(ColorsAndFonts.COLOR_INVALID);
+        } else if (state.isObjectIdentified()) {
+            color = Toolkit.getColor(ColorsAndFonts.COLOR_IDENTIFIED);
+        } else {
+            color = this.color;
+        }
+
+        final String text = TextUtils.limitText(getTitle(), style, maxWidth);
+
+        final int xt = x;
+        final int yt = baseline;
+
+        if (Toolkit.debug) {
+            final int x2 = style.stringWidth(text);
+            canvas.drawDebugOutline(new Bounds(xt, yt - style.getAscent(), x2, style.getTextHeight()), baseline, Toolkit.getColor(ColorsAndFonts.COLOR_DEBUG_BOUNDS_DRAW));
+        }
+        canvas.drawText(text, xt, yt, color, style);
+    }
+
+    public Size getSize() {
+        final int height = style.getTextHeight();
+        final int width = style.stringWidth(getTitle());
+        return new Size(width, height);
+    }
+
+    private String getTitle() {
+        if (resolveFailure) {
+            return "Resolve Failure!";
+        }
+
+        String title;
+        try {
+            title = title();
+        } catch (final ResolveException e) {
+            resolveFailure = true;
+            title = "Resolve Failure!";
+        }
+        return title;
+    }
+
+    protected abstract String title();
+
+    @Override
+    public String toString() {
+        final ToString str = new ToString(this);
+        str.append("style", style);
+        str.append("color", color);
+        return str.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/undo/AssociateCommand.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/undo/AssociateCommand.java b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/undo/AssociateCommand.java
new file mode 100644
index 0000000..f1f163c
--- /dev/null
+++ b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/undo/AssociateCommand.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.undo;
+
+import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
+import org.apache.isis.core.metamodel.spec.feature.OneToOneAssociation;
+import org.apache.isis.viewer.dnd.view.Command;
+
+public class AssociateCommand implements Command {
+    private final String description;
+    private final OneToOneAssociation field;
+    private final ObjectAdapter object;
+    private final ObjectAdapter associatedObject;
+    private final String name;
+
+    public AssociateCommand(final ObjectAdapter object, final ObjectAdapter associatedObject, final OneToOneAssociation field) {
+        this.description = "Clear association of " + associatedObject.titleString();
+        this.name = "associate " + associatedObject.titleString();
+        this.object = object;
+        this.associatedObject = associatedObject;
+        this.field = field;
+    }
+
+    @Override
+    public String getDescription() {
+        return description;
+    }
+
+    @Override
+    public String getName() {
+        return name;
+    }
+
+    @Override
+    public void undo() {
+        field.clearAssociation(object);
+    }
+
+    @Override
+    public void execute() {
+        field.setAssociation(object, associatedObject);
+    }
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/eb613703/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/undo/SetValueCommand.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/undo/SetValueCommand.java b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/undo/SetValueCommand.java
new file mode 100644
index 0000000..2b04f0f
--- /dev/null
+++ b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/undo/SetValueCommand.java
@@ -0,0 +1,86 @@
+/*
+ *  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.undo;
+
+import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
+import org.apache.isis.core.metamodel.adapter.mgr.AdapterManager;
+import org.apache.isis.core.metamodel.facets.object.encodeable.EncodableFacet;
+import org.apache.isis.core.metamodel.spec.feature.OneToOneAssociation;
+import org.apache.isis.runtimes.dflt.runtime.system.context.IsisContext;
+import org.apache.isis.runtimes.dflt.runtime.system.persistence.AdapterManagerSpi;
+import org.apache.isis.runtimes.dflt.runtime.system.persistence.PersistenceSession;
+import org.apache.isis.viewer.dnd.view.Command;
+
+public class SetValueCommand implements Command {
+    private final String description;
+    private final OneToOneAssociation value;
+    private final ObjectAdapter object;
+    private final String oldValue;
+
+    public SetValueCommand(final ObjectAdapter object, final OneToOneAssociation value) {
+        final EncodableFacet facet = value.getFacet(EncodableFacet.class);
+        this.oldValue = facet.toEncodedString(object);
+        this.object = object;
+        this.value = value;
+
+        this.description = "reset the value to " + oldValue;
+    }
+
+    @Override
+    public String getDescription() {
+        return description;
+    }
+
+    @Override
+    public void undo() {
+        final EncodableFacet facet = value.getFacet(EncodableFacet.class);
+        final Object obj = facet.fromEncodedString(oldValue);
+        final ObjectAdapter adapter = getAdapterManager().adapterFor(obj);
+        value.setAssociation(object, adapter);
+        // have commented this out because it isn't needed; the transaction
+        // manager will do this
+        // for us on endTransaction. Still, if I'm wrong and it is needed,
+        // hopefully this
+        // comment will help...
+        // IsisContext.getObjectPersistor().objectChangedAllDirty();
+    }
+
+    @Override
+    public void execute() {
+    }
+
+    @Override
+    public String getName() {
+        return "entry";
+    }
+
+    // //////////////////////////////////////////////////////////////////
+    // Dependencies (from context)
+    // //////////////////////////////////////////////////////////////////
+
+    private static PersistenceSession getPersistenceSession() {
+        return IsisContext.getPersistenceSession();
+    }
+
+    private static AdapterManager getAdapterManager() {
+        return getPersistenceSession().getAdapterManager();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/eb613703/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/window/AbstractWindowBorder.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/window/AbstractWindowBorder.java b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/window/AbstractWindowBorder.java
new file mode 100644
index 0000000..0ec0d67
--- /dev/null
+++ b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/window/AbstractWindowBorder.java
@@ -0,0 +1,211 @@
+/*
+ *  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.window;
+
+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.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.ViewDragImpl;
+import org.apache.isis.viewer.dnd.view.Click;
+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.Workspace;
+import org.apache.isis.viewer.dnd.view.base.AbstractBorder;
+import org.apache.isis.viewer.dnd.view.border.BorderDrawing;
+
+public abstract class AbstractWindowBorder extends AbstractBorder {
+    protected static BorderDrawing borderRender;
+    protected WindowControl controls[];
+    private WindowControl overControl;
+
+    public static void setBorderRenderer(final BorderDrawing borderRender) {
+        AbstractWindowBorder.borderRender = borderRender;
+    }
+
+    public AbstractWindowBorder(final View enclosedView) {
+        super(enclosedView);
+        left = borderRender.getLeft();
+        right = borderRender.getRight();
+        top = borderRender.getTop();
+        bottom = borderRender.getBottom();
+    }
+
+    @Override
+    public void debugDetails(final DebugBuilder debug) {
+        super.debugDetails(debug);
+        borderRender.debugDetails(debug);
+        if (controls.length > 0) {
+            debug.appendln("controls:-");
+            debug.indent();
+            for (final WindowControl control : controls) {
+                debug.append(control);
+                debug.appendln();
+            }
+            debug.unindent();
+        }
+    }
+
+    @Override
+    public DragEvent dragStart(final DragStart drag) {
+        if (overBorder(drag.getLocation())) {
+            final Location location = drag.getLocation();
+            final View dragOverlay = Toolkit.getViewFactory().createDragViewOutline(getView());
+            return new ViewDragImpl(this, new Offset(location.getX(), location.getY()), dragOverlay);
+        } else {
+            return super.dragStart(drag);
+        }
+    }
+
+    protected void setControls(final WindowControl[] controls) {
+        this.controls = controls;
+    }
+
+    @Override
+    public void setSize(final Size size) {
+        super.setSize(size);
+        layoutControls(size);
+    }
+
+    @Override
+    public void setBounds(final Bounds bounds) {
+        super.setBounds(bounds);
+        layoutControls(bounds.getSize());
+    }
+
+    private void layoutControls(final Size size) {
+        left = borderRender.getLeft();
+        right = borderRender.getRight();
+        top = borderRender.getTop();
+        bottom = borderRender.getBottom();
+
+        borderRender.layoutControls(size, controls);
+    }
+
+    @Override
+    public void draw(final Canvas canvas) {
+        // blank background
+        final Bounds bounds = getBounds();
+        final Color color = Toolkit.getColor(ColorsAndFonts.COLOR_WINDOW + "." + getSpecification().getName());
+        canvas.drawSolidRectangle(1, 1, bounds.getWidth() - 2, bounds.getHeight() - 2, color);
+
+        final boolean hasFocus = containsFocus();
+        final ViewState state = getState();
+        borderRender.draw(canvas, getSize(), hasFocus, state, controls, title() + " (" + getSpecification().getName() + ")");
+        // canvas.drawRectangle(0, 0, getSize().getWidth(),
+        // borderRender.getTop(), Toolkit.getColor(0xfff));
+
+        // controls
+        for (int i = 0; controls != null && i < controls.length; i++) {
+            final Canvas controlCanvas = canvas.createSubcanvas(controls[i].getBounds());
+            controls[i].draw(controlCanvas);
+        }
+
+        super.draw(canvas);
+    }
+
+    protected abstract String title();
+
+    @Override
+    public Size getRequiredSize(final Size maximumSize) {
+        left = borderRender.getLeft();
+        right = borderRender.getRight();
+        top = borderRender.getTop();
+        bottom = borderRender.getBottom();
+
+        final Size size = super.getRequiredSize(maximumSize);
+        borderRender.getRequiredSize(size, title(), controls);
+        return size;
+    }
+
+    @Override
+    public void secondClick(final Click click) {
+        final View control = overControl(click.getLocation());
+        if (control == null) {
+            super.secondClick(click);
+        }
+    }
+
+    @Override
+    public void thirdClick(final Click click) {
+        final View control = overControl(click.getLocation());
+        if (control == null) {
+            super.thirdClick(click);
+        }
+    }
+
+    @Override
+    public void firstClick(final Click click) {
+        final View control = overControl(click.getLocation());
+        if (control == null) {
+            if (overBorder(click.getLocation())) {
+                final Workspace workspace = getWorkspace();
+                if (workspace != null) {
+                    if (click.button2()) {
+                        workspace.lower(getView());
+                    } else if (click.button1()) {
+                        workspace.raise(getView());
+                    }
+                }
+            } else {
+                super.firstClick(click);
+            }
+
+        } else {
+            control.firstClick(click);
+        }
+    }
+
+    @Override
+    public void mouseMoved(final Location at) {
+        final WindowControl control = (WindowControl) overControl(at);
+        if (control != null) {
+            if (control != overControl) {
+                control.entered();
+                overControl = control;
+                return;
+            }
+        } else {
+            if (control != overControl) {
+                overControl.exited();
+                overControl = null;
+                return;
+            }
+        }
+        super.mouseMoved(at);
+    }
+
+    private View overControl(final Location location) {
+        for (final WindowControl control : controls) {
+            if (control.getBounds().contains(location)) {
+                return control;
+            }
+        }
+        return null;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/eb613703/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/window/CloseWindowControl.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/window/CloseWindowControl.java b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/window/CloseWindowControl.java
new file mode 100644
index 0000000..d497a82
--- /dev/null
+++ b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/window/CloseWindowControl.java
@@ -0,0 +1,41 @@
+/*
+ *  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.window;
+
+import org.apache.isis.viewer.dnd.drawing.Canvas;
+import org.apache.isis.viewer.dnd.view.View;
+import org.apache.isis.viewer.dnd.view.option.CloseViewOption;
+
+public class CloseWindowControl extends WindowControl {
+    private static CloseWindowRender render;
+
+    public static void setRender(final CloseWindowRender render) {
+        CloseWindowControl.render = render;
+    }
+
+    public CloseWindowControl(final View target) {
+        super(new CloseViewOption(), target);
+    }
+
+    @Override
+    public void draw(final Canvas canvas) {
+        render.draw(canvas, WIDTH, HEIGHT, action.disabled(this).isVetoed(), isOver(), isPressed());
+    }
+}

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

http://git-wip-us.apache.org/repos/asf/isis/blob/eb613703/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/window/DialogBorder.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/window/DialogBorder.java b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/window/DialogBorder.java
new file mode 100644
index 0000000..4823b05
--- /dev/null
+++ b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/window/DialogBorder.java
@@ -0,0 +1,41 @@
+/*
+ *  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.window;
+
+import org.apache.isis.viewer.dnd.view.View;
+import org.apache.isis.viewer.dnd.view.border.ScrollBorder;
+
+public class DialogBorder extends AbstractWindowBorder {
+
+    public DialogBorder(final View wrappedView, final boolean scrollable) {
+        super(scrollable ? new ScrollBorder(wrappedView) : wrappedView);
+        setControls(new WindowControl[] { new CloseWindowControl(this) });
+    }
+
+    @Override
+    protected String title() {
+        return getContent().windowTitle();
+    }
+
+    @Override
+    public String toString() {
+        return wrappedView.toString() + "/DialogBorder [" + 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/window/IconizeWindowControl.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/window/IconizeWindowControl.java b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/window/IconizeWindowControl.java
new file mode 100644
index 0000000..9c54db3
--- /dev/null
+++ b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/window/IconizeWindowControl.java
@@ -0,0 +1,42 @@
+/*
+ *  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.window;
+
+import org.apache.isis.viewer.dnd.drawing.Canvas;
+import org.apache.isis.viewer.dnd.view.View;
+import org.apache.isis.viewer.dnd.view.option.IconizeViewOption;
+
+public class IconizeWindowControl extends WindowControl {
+    private static IconizeWindowRender render;
+
+    public static void setRender(final IconizeWindowRender render) {
+        IconizeWindowControl.render = render;
+    }
+
+    public IconizeWindowControl(final View target) {
+        super(new IconizeViewOption(), target);
+    }
+
+    @Override
+    public void draw(final Canvas canvas) {
+        render.draw(canvas, WIDTH, HEIGHT, false, isOver(), isPressed());
+    }
+
+}

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

http://git-wip-us.apache.org/repos/asf/isis/blob/eb613703/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/window/ResizeWindowControl.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/window/ResizeWindowControl.java b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/window/ResizeWindowControl.java
new file mode 100644
index 0000000..47afae4
--- /dev/null
+++ b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/window/ResizeWindowControl.java
@@ -0,0 +1,77 @@
+/*
+ *  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.window;
+
+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.viewer.dnd.drawing.Canvas;
+import org.apache.isis.viewer.dnd.drawing.Location;
+import org.apache.isis.viewer.dnd.view.UserAction;
+import org.apache.isis.viewer.dnd.view.View;
+import org.apache.isis.viewer.dnd.view.Workspace;
+
+public class ResizeWindowControl extends WindowControl {
+    private static ResizeWindowRender render;
+
+    public static void setRender(final ResizeWindowRender render) {
+        ResizeWindowControl.render = render;
+    }
+
+    public ResizeWindowControl(final View target) {
+        super(new UserAction() {
+
+            @Override
+            public Consent disabled(final View view) {
+                return Veto.DEFAULT;
+            }
+
+            @Override
+            public void execute(final Workspace workspace, final View view, final Location at) {
+            }
+
+            @Override
+            public String getDescription(final View view) {
+                return "";
+            }
+
+            @Override
+            public String getHelp(final View view) {
+                return "";
+            }
+
+            @Override
+            public ActionType getType() {
+                return ActionType.USER;
+            }
+
+            @Override
+            public String getName(final View view) {
+                return "Resize";
+            }
+        }, target);
+
+    }
+
+    @Override
+    public void draw(final Canvas canvas) {
+        render.draw(canvas, WIDTH, HEIGHT, action.disabled(this).isVetoed(), isOver(), isPressed());
+    }
+}

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

http://git-wip-us.apache.org/repos/asf/isis/blob/eb613703/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/window/SubviewFocusManager.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/window/SubviewFocusManager.java b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/window/SubviewFocusManager.java
new file mode 100644
index 0000000..f0d738f
--- /dev/null
+++ b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/window/SubviewFocusManager.java
@@ -0,0 +1,54 @@
+/*
+ *  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.window;
+
+import org.apache.isis.viewer.dnd.view.View;
+import org.apache.isis.viewer.dnd.view.base.AbstractFocusManager;
+
+public class SubviewFocusManager extends AbstractFocusManager {
+    private final WindowBorder windowBorder;
+
+    public SubviewFocusManager(final WindowBorder container) {
+        super(container);
+        windowBorder = container;
+    }
+
+    public SubviewFocusManager(final View container) {
+        super(container);
+        windowBorder = null;
+    }
+
+    public SubviewFocusManager(final View container, final View initalFocus) {
+        super(container, initalFocus);
+        windowBorder = null;
+    }
+
+    @Override
+    protected View[] getChildViews() {
+        final View[] subviews = container.getSubviews();
+        final View[] buttons = windowBorder == null ? new View[0] : windowBorder.getButtons();
+
+        final View[] views = new View[subviews.length + buttons.length];
+        System.arraycopy(subviews, 0, views, 0, subviews.length);
+        System.arraycopy(buttons, 0, views, subviews.length, buttons.length);
+        return views;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/eb613703/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/window/WindowBorder.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/window/WindowBorder.java b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/window/WindowBorder.java
new file mode 100644
index 0000000..5614a4f
--- /dev/null
+++ b/component/viewer/dnd/impl/src/main/java/org/apache/isis/viewer/dnd/view/window/WindowBorder.java
@@ -0,0 +1,132 @@
+/*
+ *  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.window;
+
+import java.util.Enumeration;
+
+import org.apache.isis.viewer.dnd.drawing.Canvas;
+import org.apache.isis.viewer.dnd.view.Click;
+import org.apache.isis.viewer.dnd.view.Content;
+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.ViewRequirement;
+import org.apache.isis.viewer.dnd.view.ViewSpecification;
+import org.apache.isis.viewer.dnd.view.border.ButtonBorder;
+import org.apache.isis.viewer.dnd.view.border.SaveTransientObjectBorder;
+import org.apache.isis.viewer.dnd.view.border.ScrollBorder;
+import org.apache.isis.viewer.dnd.view.option.CloseAllViewsForObjectOption;
+import org.apache.isis.viewer.dnd.view.option.CloseAllViewsOption;
+import org.apache.isis.viewer.dnd.view.option.CloseOtherViewsForObjectOption;
+import org.apache.isis.viewer.dnd.view.option.CloseViewOption;
+import org.apache.isis.viewer.dnd.view.option.IconizeViewOption;
+import org.apache.isis.viewer.dnd.view.option.ReplaceViewOption;
+
+public class WindowBorder extends AbstractWindowBorder {
+    private static final UserAction CLOSE_ALL_OPTION = new CloseAllViewsOption();
+    private static final UserAction CLOSE_OPTION = new CloseViewOption();
+    private static final UserAction CLOSE_VIEWS_FOR_OBJECT = new CloseAllViewsForObjectOption();
+    private static final UserAction CLOSE_OTHER_VIEWS_FOR_OBJECT = new CloseOtherViewsForObjectOption();
+    private static final IconizeViewOption iconizeOption = new IconizeViewOption();
+
+    public WindowBorder(final View wrappedView, final boolean scrollable) {
+        super(addTransientBorderIfNeccessary(scrollable ? new ScrollBorder(wrappedView) : wrappedView));
+
+        if (isTransient()) {
+            setControls(new WindowControl[] { new CloseWindowControl(this) });
+        } else {
+            setControls(new WindowControl[] { new IconizeWindowControl(this), new ResizeWindowControl(this), new CloseWindowControl(this) });
+        }
+    }
+
+    private static View addTransientBorderIfNeccessary(final View view) {
+        final Content content = view.getContent();
+        if (content.isPersistable() && content.isTransient()) {
+            return new SaveTransientObjectBorder(view);
+        } else {
+            return view;
+        }
+    }
+
+    /* TODO fix focus management and remove this hack */
+    public View[] getButtons() {
+        if (wrappedView instanceof ButtonBorder) {
+            return ((ButtonBorder) wrappedView).getButtons();
+        } else {
+            return new View[0];
+        }
+    }
+
+    @Override
+    public void draw(final Canvas canvas) {
+        super.draw(canvas);
+        if (isTransient()) {
+            borderRender.drawTransientMarker(canvas, getSize());
+        }
+    }
+
+    private boolean isTransient() {
+        final Content content = getContent();
+        return content.isPersistable() && content.isTransient();
+    }
+
+    @Override
+    public void viewMenuOptions(final UserActionSet menuOptions) {
+        menuOptions.add(iconizeOption);
+        menuOptions.add(CLOSE_OPTION);
+        menuOptions.add(CLOSE_ALL_OPTION);
+        menuOptions.add(CLOSE_VIEWS_FOR_OBJECT);
+        menuOptions.add(CLOSE_OTHER_VIEWS_FOR_OBJECT);
+
+        super.viewMenuOptions(menuOptions);
+
+        final Content content = getContent();
+        final UserActionSet suboptions = menuOptions.addNewActionSet("Replace with");
+        replaceOptions(Toolkit.getViewFactory().availableViews(new ViewRequirement(content, ViewRequirement.OPEN)), suboptions);
+        replaceOptions(Toolkit.getViewFactory().availableViews(new ViewRequirement(content, ViewRequirement.CLOSED)), 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));
+                }
+            }
+        }
+    }
+
+    @Override
+    public void secondClick(final Click click) {
+        if (overBorder(click.getLocation())) {
+            iconizeOption.execute(getWorkspace(), getView(), getAbsoluteLocation());
+        } else {
+            super.secondClick(click);
+        }
+    }
+
+    @Override
+    protected String title() {
+        return getContent().windowTitle();
+    }
+
+}