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/06 18:42:18 UTC

[45/52] [partial] ISIS-188: moving framework/ subdirs up to parent

http://git-wip-us.apache.org/repos/asf/isis/blob/255ef514/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/field/PasteValueOption.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/field/PasteValueOption.java b/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/field/PasteValueOption.java
new file mode 100644
index 0000000..138ae26
--- /dev/null
+++ b/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/field/PasteValueOption.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.field;
+
+import org.apache.isis.core.metamodel.consent.Consent;
+import org.apache.isis.viewer.dnd.drawing.Location;
+import org.apache.isis.viewer.dnd.view.View;
+import org.apache.isis.viewer.dnd.view.Workspace;
+
+public class PasteValueOption extends AbstractValueOption {
+
+    public PasteValueOption(final AbstractField field) {
+        super(field, "Paste");
+    }
+
+    @Override
+    public Consent disabled(final View view) {
+        final Consent changable = view.canChangeValue();
+        if (changable.isVetoed()) {
+            return changable;
+        } else {
+            return changable.setDescription(String.format("Replace field content with '%s' from clipboard", getClipboard(view)));
+        }
+    }
+
+    @Override
+    public void execute(final Workspace workspace, final View view, final Location at) {
+        field.pasteFromClipboard();
+    }
+
+    private String getClipboard(final View view) {
+        return (String) view.getViewManager().getClipboard(String.class);
+    }
+
+    @Override
+    public String toString() {
+        return "PasteValueOption";
+    }
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/255ef514/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/field/RevertFieldOption.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/field/RevertFieldOption.java b/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/field/RevertFieldOption.java
new file mode 100644
index 0000000..ba739de
--- /dev/null
+++ b/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/field/RevertFieldOption.java
@@ -0,0 +1,52 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+package org.apache.isis.viewer.dnd.field;
+
+import org.apache.isis.core.metamodel.consent.Consent;
+import org.apache.isis.core.metamodel.consent.ConsentAbstract;
+import org.apache.isis.viewer.dnd.drawing.Location;
+import org.apache.isis.viewer.dnd.view.View;
+import org.apache.isis.viewer.dnd.view.Workspace;
+import org.apache.isis.viewer.dnd.view.option.UserActionAbstract;
+
+public class RevertFieldOption extends UserActionAbstract {
+    private final TextField field;
+
+    public RevertFieldOption(final TextField field) {
+        super("Revert");
+        this.field = field;
+    }
+
+    @Override
+    public String getDescription(final View view) {
+        return "Revert the field to it original state";
+    }
+
+    @Override
+    public Consent disabled(final View view) {
+        return ConsentAbstract.allowIf(field.hasInvalidEntry());
+    }
+
+    @Override
+    public void execute(final Workspace workspace, final View view, final Location at) {
+        field.revertInvalidEntry();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/255ef514/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/field/SimpleDatePicker.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/field/SimpleDatePicker.java b/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/field/SimpleDatePicker.java
new file mode 100644
index 0000000..46f6408
--- /dev/null
+++ b/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/field/SimpleDatePicker.java
@@ -0,0 +1,392 @@
+/*
+ *  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.field;
+
+import java.awt.event.KeyEvent;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+
+import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
+import org.apache.isis.core.progmodel.facets.value.date.DateValueFacet;
+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.Shape;
+import org.apache.isis.viewer.dnd.drawing.Size;
+import org.apache.isis.viewer.dnd.drawing.Text;
+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.Toolkit;
+import org.apache.isis.viewer.dnd.view.base.AbstractView;
+import org.apache.isis.viewer.dnd.view.content.FieldContent;
+import org.apache.isis.viewer.dnd.view.content.TextParseableContent;
+
+public class SimpleDatePicker extends AbstractView implements DatePicker {
+    private static class Button {
+        private final char key;
+        private final int period;
+        private final int increment;
+
+        public Button(final char key, final int period, final int increment) {
+            this.key = key;
+            this.period = period;
+            this.increment = increment;
+        }
+
+        public void adjustDate(final Calendar date) {
+            date.add(period, increment);
+        }
+
+        public String getLabel() {
+            return "" + key;
+        }
+    }
+
+    private static final Button[] buttons = new Button[] { new Button('W', Calendar.WEEK_OF_YEAR, 1), new Button('F', Calendar.WEEK_OF_YEAR, 2), new Button('M', Calendar.MONTH, 1), new Button('Q', Calendar.MONTH, 3), new Button('Y', Calendar.YEAR, 1), new Button('D', Calendar.YEAR, 10),
+
+    new Button('w', Calendar.WEEK_OF_YEAR, -1), new Button('f', Calendar.WEEK_OF_YEAR, -2), new Button('m', Calendar.MONTH, -1), new Button('q', Calendar.MONTH, -3), new Button('y', Calendar.YEAR, -1), new Button('d', Calendar.YEAR, -10) };
+    private static final int ROWS = 7;
+    private static final int COLUMNS = 7;
+    private static final int PADDING = 5;
+    private final Calendar date;
+    private final Text style = Toolkit.getText(ColorsAndFonts.TEXT_NORMAL);
+    private final int labelWidth = style.stringWidth("XXX 0000") * 4 / 3;
+    private final int cellWidth = style.stringWidth("00") * 8 / 5;
+    private final int cellHeight = style.getLineHeight() * 4 / 3;
+    private final int headerHeight = style.getLineHeight() + PADDING * 2;
+    private final int firstRowBaseline = headerHeight + style.getLineHeight();
+    private final int calendarHeight = cellHeight * ROWS;
+    private final int controlWidth = style.charWidth('W') + 4;
+    private final int controlHeight = style.getLineHeight() * 11 / 10 + controlWidth + 4 + PADDING * 2;
+    private int mouseOverButton = -1;
+    private int mouseOverRow = -1;
+    private int mouseOverColumn;
+    private Calendar currentDate;
+    private final Calendar today;
+    private final DateFormat monthFormat = new SimpleDateFormat("MMM");
+    private final DateFormat dayFormat = new SimpleDateFormat("EEE");
+    private final boolean isEditable;
+
+    public SimpleDatePicker(final Content content) {
+        super(content);
+
+        isEditable = content instanceof FieldContent && ((FieldContent) content).isEditable().isAllowed();
+
+        today = Calendar.getInstance();
+        clearTime(today);
+
+        date = Calendar.getInstance();
+        final ObjectAdapter dateAdapter = ((TextParseableContent) getContent()).getAdapter();
+        if (dateAdapter != null) {
+            final DateValueFacet facet = dateAdapter.getSpecification().getFacet(DateValueFacet.class);
+            currentDate = Calendar.getInstance();
+            final Date dateValue = facet.dateValue(dateAdapter);
+            currentDate.setTime(dateValue);
+            clearTime(currentDate);
+            date.setTime(dateValue);
+        }
+        clearTime(date);
+        date.add(Calendar.DAY_OF_MONTH, -21);
+        roundDownDate();
+    }
+
+    private void roundDownDate() {
+        date.add(Calendar.DAY_OF_MONTH, date.getFirstDayOfWeek() - date.get(Calendar.DAY_OF_WEEK));
+    }
+
+    private void clearTime(final Calendar date) {
+        date.clear(Calendar.AM_PM);
+        date.clear(Calendar.HOUR);
+        date.clear(Calendar.HOUR_OF_DAY);
+        date.clear(Calendar.MINUTE);
+        date.clear(Calendar.SECOND);
+        date.clear(Calendar.MILLISECOND);
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see org.nakedobjects.plugins.dnd.field.DatePicker#getRequiredSize(org.
+     * nakedobjects.plugins.dnd.drawing.Size)
+     */
+    @Override
+    public Size getRequiredSize(final Size availableSpace) {
+        return new Size(labelWidth + COLUMNS * cellWidth + 2, headerHeight + calendarHeight + controlHeight + 2);
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see
+     * org.nakedobjects.plugins.dnd.field.DatePicker#draw(org.nakedobjects.plugins
+     * .dnd.drawing.Canvas)
+     */
+    @Override
+    public void draw(final Canvas canvas) {
+        final int width = getSize().getWidth();
+        final int height = getSize().getHeight();
+
+        final Color secondaryTextColor = Toolkit.getColor(ColorsAndFonts.COLOR_WHITE);
+        final Color mainTextColor = Toolkit.getColor(ColorsAndFonts.COLOR_BLACK);
+
+        drawBackground(canvas, width, height);
+        drawDaysOfWeek(canvas, secondaryTextColor);
+        if (isEditable) {
+            drawDayMarker(canvas);
+        }
+        drawMonthsAndWeeks(canvas, secondaryTextColor);
+        drawDays(canvas, mainTextColor);
+        drawControls(canvas, width);
+    }
+
+    private void drawBackground(final Canvas canvas, final int width, final int height) {
+        canvas.drawSolidRectangle(0, 0, width - 1, headerHeight, Toolkit.getColor(ColorsAndFonts.COLOR_SECONDARY1));
+        canvas.drawSolidRectangle(0, 0, labelWidth, height - 1, Toolkit.getColor(ColorsAndFonts.COLOR_SECONDARY1));
+        canvas.drawSolidRectangle(labelWidth, headerHeight, width - labelWidth - 1, height - cellHeight - 1, Toolkit.getColor(ColorsAndFonts.COLOR_WINDOW));
+        canvas.drawRectangle(0, 0, width - 1, height - 1, Toolkit.getColor(ColorsAndFonts.COLOR_SECONDARY1));
+    }
+
+    private void drawDaysOfWeek(final Canvas canvas, final Color textColor) {
+        final Calendar d = Calendar.getInstance();
+        d.setTime(date.getTime());
+        int x = labelWidth + cellWidth / 2;
+        final int y = PADDING + style.getAscent();
+        for (int column = 0; column < 7; column++) {
+            final String day = dayFormat.format(d.getTime()).substring(0, 1);
+            canvas.drawText(day, x - style.stringWidth(day) / 2, y, textColor, style);
+            x += cellWidth;
+            d.add(Calendar.DAY_OF_MONTH, 1);
+        }
+    }
+
+    private void drawDayMarker(final Canvas canvas) {
+        if (mouseOverColumn >= 0 && mouseOverColumn < COLUMNS && mouseOverRow >= 0 && mouseOverRow < ROWS) {
+            canvas.drawRectangle(labelWidth + mouseOverColumn * cellWidth, headerHeight + mouseOverRow * cellHeight, cellWidth, cellHeight, Toolkit.getColor(ColorsAndFonts.COLOR_PRIMARY3));
+        }
+    }
+
+    private int drawMonthsAndWeeks(final Canvas canvas, final Color color) {
+        final Calendar d = Calendar.getInstance();
+        d.setTime(date.getTime());
+        int y = firstRowBaseline;
+        String lastMonth = "";
+        for (int row = 0; row < ROWS; row++) {
+            final int x = labelWidth;
+            final String month = monthFormat.format(d.getTime()) + " " + d.get(Calendar.YEAR);
+            if (!month.equals(lastMonth)) {
+                canvas.drawText(month, x - style.stringWidth(month) - PADDING, y, color, style);
+                lastMonth = month;
+            } else {
+                final String week = "wk " + (d.get(Calendar.WEEK_OF_YEAR));
+                canvas.drawText(week == null ? "*" : week, x - style.stringWidth(week) - PADDING, y, color, style);
+            }
+            d.add(Calendar.DAY_OF_MONTH, 7);
+            y += cellHeight;
+        }
+        return y;
+    }
+
+    private int drawDays(final Canvas canvas, final Color mainTextColor) {
+        final Calendar d = Calendar.getInstance();
+        d.setTime(date.getTime());
+        int y = firstRowBaseline;
+        for (int row = 0; row < ROWS; row++) {
+            int x = labelWidth;
+            for (int column = 0; column < COLUMNS; column++) {
+                final String day = "" + d.get(Calendar.DAY_OF_MONTH);
+                if (currentDate != null && currentDate.equals(d)) {
+                    canvas.drawSolidRectangle(x, headerHeight + row * cellHeight, cellWidth, cellHeight, Toolkit.getColor(ColorsAndFonts.COLOR_PRIMARY2));
+                }
+                if (today.getTime().equals(d.getTime())) {
+                    canvas.drawRectangle(x, headerHeight + row * cellHeight, cellWidth, cellHeight, Toolkit.getColor(ColorsAndFonts.COLOR_SECONDARY2));
+                }
+                canvas.drawText(day, x - PADDING / 2 + cellWidth - style.stringWidth(day), y, mainTextColor, style);
+                x += cellWidth;
+                d.add(Calendar.DAY_OF_MONTH, 1);
+            }
+            y += cellHeight;
+        }
+        return y;
+    }
+
+    private void drawControls(final Canvas canvas, final int width) {
+        int x = labelWidth + PADDING;
+        final int y = headerHeight + calendarHeight + PADDING;
+
+        final int spaceTaken = width - labelWidth - 2 * PADDING - 6 * controlWidth;
+        final int spaceBetween = spaceTaken / 5;
+        final int adjustment = spaceTaken - 5 * spaceBetween - 2;
+
+        for (int i = 0; i < buttons.length / 2; i++) {
+            drawControl(canvas, x, y, controlWidth, controlHeight - PADDING * 2, buttons[i].getLabel(), i);
+            x += controlWidth + spaceBetween + (i == 3 ? adjustment : 0);
+        }
+    }
+
+    private void drawControl(final Canvas canvas, final int x, final int y, final int width, final int height, final String label, final int over) {
+        if (Toolkit.debug) {
+            canvas.drawRectangle(x - 2, y, width + 4, height, Toolkit.getColor(ColorsAndFonts.COLOR_DEBUG_BOUNDS_VIEW));
+        }
+        final Color color = Toolkit.getColor(ColorsAndFonts.COLOR_SECONDARY2);
+
+        final int arrowHeight = width / 2;
+        final Shape upArrow = new Shape(0, arrowHeight);
+        upArrow.addVector(arrowHeight, -arrowHeight);
+        upArrow.addVector(arrowHeight, arrowHeight);
+        if (mouseOverButton == over + 6) {
+            canvas.drawSolidShape(upArrow, x, y + 2, color);
+        } else {
+            canvas.drawShape(upArrow, x, y + 2, color);
+        }
+
+        final Shape downArrow = new Shape(0, 0);
+        downArrow.addVector(arrowHeight, arrowHeight);
+        downArrow.addVector(arrowHeight, -arrowHeight);
+        if (mouseOverButton == over) {
+            canvas.drawSolidShape(downArrow, x, y + height - 4 - arrowHeight, color);
+        } else {
+            canvas.drawShape(downArrow, x, y + height - 4 - arrowHeight, color);
+        }
+        final int charWidth = style.stringWidth(label);
+        canvas.drawText(label, x + width / 2 - charWidth / 2, y + 2 + arrowHeight + style.getAscent(), color, style);
+
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see
+     * org.nakedobjects.plugins.dnd.field.DatePicker#mouseMoved(org.nakedobjects
+     * .plugins.dnd.drawing.Location)
+     */
+    @Override
+    public void mouseMoved(final Location location) {
+        final int over = overButton(location);
+        if (over == -1) {
+            mouseOverButton = -1;
+            final int col = column(location);
+            final int row = row(location);
+            if (col != mouseOverColumn || row != mouseOverRow) {
+                if (isEditable) {
+                    mouseOverColumn = col;
+                    mouseOverRow = row;
+                }
+                markDamaged();
+            }
+        } else if (over != mouseOverButton) {
+            mouseOverButton = over;
+            markDamaged();
+        }
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see org.nakedobjects.plugins.dnd.field.DatePicker#exited()
+     */
+    @Override
+    public void exited() {
+        mouseOverButton = -1;
+        super.exited();
+    }
+
+    private int overButton(final Location location) {
+        final int x = location.getX();
+        final int y = location.getY();
+        final int verticalBoundary = headerHeight + calendarHeight + PADDING;
+        final int height = controlHeight - PADDING * 2;
+        if (x > labelWidth && y > verticalBoundary && y < verticalBoundary + height) {
+            int column = (x - labelWidth) / ((getSize().getWidth() - labelWidth) / 6);
+            if (y <= verticalBoundary + height / 2) {
+                column += 6;
+            }
+            return column;
+        } else {
+            return -1;
+        }
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see
+     * org.nakedobjects.plugins.dnd.field.DatePicker#firstClick(org.nakedobjects
+     * .plugins.dnd.view.Click)
+     */
+    @Override
+    public void firstClick(final Click click) {
+        if (mouseOverButton != -1) {
+            buttons[mouseOverButton].adjustDate(date);
+            roundDownDate();
+            markDamaged();
+            return;
+        }
+
+        if (isEditable) {
+            final Location location = click.getLocation();
+            final int col = column(location);
+            final int row = row(location);
+            if (col >= 0 && col < COLUMNS && row >= 0 && row < ROWS) {
+                date.add(Calendar.DAY_OF_MONTH, row * 7 + col);
+                final Content content = getContent();
+                final DateValueFacet facet = content.getSpecification().getFacet(DateValueFacet.class);
+                final ObjectAdapter value = facet.createValue(date.getTime());
+                ((TextParseableContent) content).parseTextEntry(value.titleString());
+                ((TextParseableContent) content).entryComplete();
+                /*
+                 * if (content.isObject()) { ((ObjectContent)
+                 * content).setObject(value); }
+                 */
+                // content.
+                getView().refresh();
+
+                // content.
+            }
+        }
+        getViewManager().clearOverlayView();
+    }
+
+    private int row(final Location location) {
+        return (location.getY() - headerHeight) / cellHeight;
+    }
+
+    private int column(final Location location) {
+        return (location.getX() - labelWidth) / cellWidth;
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see
+     * org.nakedobjects.plugins.dnd.field.DatePicker#keyPressed(org.nakedobjects
+     * .plugins.dnd.view.KeyboardAction)
+     */
+    @Override
+    public void keyPressed(final KeyboardAction key) {
+        if (isEditable && key.getKeyCode() == KeyEvent.VK_ESCAPE) {
+            getViewManager().clearOverlayView();
+        } else {
+            super.keyPressed(key);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/255ef514/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/field/SingleLineTextField.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/field/SingleLineTextField.java b/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/field/SingleLineTextField.java
new file mode 100644
index 0000000..0794e3b
--- /dev/null
+++ b/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/field/SingleLineTextField.java
@@ -0,0 +1,129 @@
+/*
+ *  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.field;
+
+import org.apache.isis.core.commons.exceptions.IsisException;
+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.ViewConstants;
+import org.apache.isis.viewer.dnd.view.ViewSpecification;
+import org.apache.isis.viewer.dnd.view.content.TextParseableContent;
+import org.apache.isis.viewer.dnd.view.text.TextContent;
+import org.apache.isis.viewer.dnd.view.text.TextUtils;
+
+public class SingleLineTextField extends TextField {
+    private static final int LIMIT = 20;
+    private int offset = 0;
+
+    public SingleLineTextField(final TextParseableContent content, final ViewSpecification specification, final boolean showLines) {
+        super(content, specification, showLines, TextContent.NO_WRAPPING);
+    }
+
+    @Override
+    protected void align() {
+        final String line = textContent.getText(0);
+        if (line != null) {
+            final int maxWidth = getMaxFieldWidth();
+            final int leftLimit = offset + LIMIT;
+            final int rightLimit = offset + maxWidth - LIMIT;
+
+            if (cursor.getCharacter() > line.length()) {
+                cursor.end();
+            }
+
+            final int cursorPosition = style.stringWidth(line.substring(0, cursor.getCharacter()));
+            if (cursorPosition > rightLimit) {
+                offset = offset + (cursorPosition - rightLimit);
+                offset = Math.min(style.stringWidth(line), offset);
+            } else if (cursorPosition < leftLimit) {
+                offset = offset - (leftLimit - cursorPosition);
+                offset = Math.max(0, offset);
+            }
+        }
+    }
+
+    @Override
+    protected void drawHighlight(final Canvas canvas, final int maxWidth) {
+        final int baseline = getBaseline();
+        final int top = baseline - style.getAscent();
+
+        int from = selection.from().getCharacter();
+        int to = selection.to().getCharacter();
+
+        final String line = textContent.getText(0);
+        if (to >= line.length()) {
+            to = line.length();
+        }
+        if (from >= line.length()) {
+            from = line.length();
+        }
+        if (line != null) {
+            final int start = style.stringWidth(line.substring(0, from));
+            final int end = style.stringWidth(line.substring(0, to));
+            canvas.drawSolidRectangle(start + (ViewConstants.HPADDING), top, end - start, style.getLineHeight(), Toolkit.getColor(ColorsAndFonts.COLOR_TEXT_HIGHLIGHT));
+        }
+    }
+
+    @Override
+    protected void drawLines(final Canvas canvas, final Color color, final int width) {
+        final int baseline = getBaseline();
+        canvas.drawLine(ViewConstants.HPADDING, baseline, ViewConstants.HPADDING + width, baseline, color);
+    }
+
+    @Override
+    protected void drawText(final Canvas canvas, final Color textColor, final int width) {
+        final String[] lines = textContent.getDisplayLines();
+        if (lines.length > 1) {
+            throw new IsisException("Single line text field should contain a string that contains no line breaks; contains " + lines.length);
+        }
+
+        final String chars = lines[0];
+        if (chars == null) {
+            throw new IsisException();
+        }
+        if (chars.endsWith("\n")) {
+            throw new RuntimeException();
+        }
+
+        final int baseline = getBaseline();
+
+        // draw cursor
+        if (hasFocus() && canChangeValue().isAllowed()) {
+            final int at = Math.min(cursor.getCharacter(), chars.length());
+            final int pos = style.stringWidth(chars.substring(0, at)) - offset + ViewConstants.HPADDING;
+            canvas.drawLine(pos, (baseline + style.getDescent()), pos, baseline - style.getAscent(), Toolkit.getColor(ColorsAndFonts.COLOR_TEXT_CURSOR));
+        }
+
+        // draw text
+        final String line = hasFocus() ? chars : TextUtils.limitText(chars, style, width);
+        canvas.drawText(line, ViewConstants.HPADDING - offset, baseline, textColor, style);
+    }
+
+    @Override
+    public void setMaximumSize(final Size size) {
+        final int width = Math.max(180, size.getWidth() - ViewConstants.HPADDING);
+        setWidth(width);
+        invalidateLayout();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/255ef514/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/field/TextField.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/field/TextField.java b/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/field/TextField.java
new file mode 100644
index 0000000..58c3605
--- /dev/null
+++ b/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/field/TextField.java
@@ -0,0 +1,757 @@
+/*
+ *  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.field;
+
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import org.apache.log4j.Logger;
+
+import org.apache.isis.core.commons.debug.DebugBuilder;
+import org.apache.isis.core.commons.exceptions.IsisException;
+import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
+import org.apache.isis.core.metamodel.adapter.version.ConcurrencyException;
+import org.apache.isis.core.metamodel.facets.object.parseable.InvalidEntryException;
+import org.apache.isis.core.metamodel.facets.object.parseable.TextEntryParseException;
+import org.apache.isis.viewer.dnd.drawing.Canvas;
+import org.apache.isis.viewer.dnd.drawing.Color;
+import org.apache.isis.viewer.dnd.drawing.ColorsAndFonts;
+import org.apache.isis.viewer.dnd.drawing.Location;
+import org.apache.isis.viewer.dnd.drawing.Size;
+import org.apache.isis.viewer.dnd.drawing.Text;
+import org.apache.isis.viewer.dnd.interaction.SimpleInternalDrag;
+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.InternalDrag;
+import org.apache.isis.viewer.dnd.view.KeyboardAction;
+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.ViewConstants;
+import org.apache.isis.viewer.dnd.view.ViewSpecification;
+import org.apache.isis.viewer.dnd.view.base.TextView;
+import org.apache.isis.viewer.dnd.view.border.BackgroundBorder;
+import org.apache.isis.viewer.dnd.view.border.LineBorder;
+import org.apache.isis.viewer.dnd.view.content.TextParseableContent;
+import org.apache.isis.viewer.dnd.view.text.CursorPosition;
+import org.apache.isis.viewer.dnd.view.text.TextBlockTarget;
+import org.apache.isis.viewer.dnd.view.text.TextContent;
+import org.apache.isis.viewer.dnd.view.text.TextSelection;
+
+public abstract class TextField extends TextParseableFieldAbstract implements TextBlockTarget {
+    private static final Logger LOG = Logger.getLogger(TextField.class);
+    protected static final Text style = Toolkit.getText(ColorsAndFonts.TEXT_NORMAL);
+    protected CursorPosition cursor;
+    private boolean identified;
+    private String invalidReason = null;
+    private boolean isSaved = true;
+    private final int maxLength;
+    private int displayWidth;
+    protected TextSelection selection;
+    private final boolean showLines;
+    protected TextContent textContent;
+    protected boolean useEmptyLines;
+
+    public TextField(final TextParseableContent content, final ViewSpecification specification, final boolean showLines, final int wrapStyle) {
+        this(content, specification, showLines, wrapStyle, false);
+    }
+
+    public TextField(final TextParseableContent content, final ViewSpecification specification, final boolean showLines, final int wrapStyle, final boolean useEmptyLines) {
+        super(content, specification);
+        this.showLines = showLines;
+
+        int typicalLength = content.getTypicalLineLength();
+        typicalLength = typicalLength == 0 ? TEXT_WIDTH : typicalLength / content.getNoLines();
+        setTextWidth(typicalLength);
+
+        this.maxLength = content.getMaximumLength();
+        this.useEmptyLines = useEmptyLines;
+
+        textContent = new TextContent(this, 1, wrapStyle, this.useEmptyLines);
+        cursor = new CursorPosition(textContent, 0, 0);
+        selection = new TextSelection(textContent);
+        final ObjectAdapter value = getValue();
+        textContent.setText(value == null ? "" : titleString(value));
+        cursor.home();
+        isSaved = true;
+    }
+
+    protected abstract void align();
+
+    @Override
+    public void contentMenuOptions(final UserActionSet options) {
+        super.contentMenuOptions(options);
+        options.add(new RevertFieldOption(this));
+    }
+
+    @Override
+    protected void clear() {
+        clearValue();
+        editComplete(false, false);
+    }
+
+    void clearValue() {
+        textContent.setText("");
+        cursor.home();
+        selection.resetTo(cursor);
+        changeMade();
+    }
+
+    @Override
+    protected void copyToClipboard() {
+        final boolean noSelection = selection.to().samePosition(selection.from());
+        final String text = noSelection ? textContent.getText() : textContent.getText(selection);
+        getViewManager().setClipboard(text, String.class);
+        LOG.debug("copied " + text);
+    }
+
+    @Override
+    public void debug(final DebugBuilder debug) {
+        super.debug(debug);
+        debug.appendln("text", textContent);
+    }
+
+    /**
+     * Delete the character to the left of the cursor.
+     */
+    public void delete() {
+        if (selection.hasSelection()) {
+            textContent.delete(selection);
+            selection.resetTo(selection.from());
+        } else {
+            textContent.deleteLeft(cursor);
+            cursor.left();
+            selection.resetTo(cursor);
+        }
+        changeMade();
+    }
+
+    /**
+     * Delete the character to the right of the cursor.
+     */
+    public void deleteForward() {
+        if (selection.hasSelection()) {
+            textContent.delete(selection);
+            selection.resetTo(selection.from());
+        } else {
+            textContent.deleteRight(cursor);
+        }
+        changeMade();
+    }
+
+    protected void down(final boolean shift) {
+        cursor.lineDown();
+        highlight(shift);
+        markDamaged();
+    }
+
+    @Override
+    public void drag(final InternalDrag drag) {
+        if (canChangeValue().isAllowed()) {
+            selection.extendTo(drag.getLocation());
+            markDamaged();
+        }
+    }
+
+    @Override
+    public DragEvent dragStart(final DragStart drag) {
+        final Location at = drag.getLocation();
+
+        final Location anchor = getAbsoluteLocation();
+        // TODO adjust anchor so only the field is rubberbanded
+        /*
+         * final Size size = getView().getSize(); final ViewAxis axis =
+         * getViewAxis(LabelAxis.class); if (axis instanceof LabelAxis) { final
+         * int width = ((LabelAxis) axis).getWidth(); size.contractWidth(width);
+         * anchor.add(width, 0); }
+         */
+        if (canChangeValue().isAllowed()) {
+            cursor.cursorAt(at);
+            resetSelection();
+            return new SimpleInternalDrag(this, anchor);
+        }
+
+        markDamaged();
+
+        return null;
+    }
+
+    @Override
+    public void dragTo(final InternalDrag drag) {
+        final Location at = drag.getLocation();
+        if (canChangeValue().isAllowed()) {
+            selection.extendTo(at);
+            markDamaged();
+        }
+    }
+
+    @Override
+    public void draw(final Canvas canvas) {
+        super.draw(canvas);
+
+        final int width = getMaxFieldWidth();
+
+        align();
+
+        if (hasFocus() && selection.hasSelection()) {
+            drawHighlight(canvas, width);
+        }
+
+        /*
+         * if (showLines == true && canChangeValue().isAllowed()) { Color color
+         * = identified ? Toolkit.getColor(ColorsAndFonts.COLOR_IDENTIFIED) :
+         * Toolkit.getColor(ColorsAndFonts.COLOR_SECONDARY3); color = hasFocus()
+         * ? Toolkit.getColor(ColorsAndFonts.COLOR_PRIMARY1) : color;
+         * drawLines(canvas, color, width); }
+         */
+        Color textColor;
+        Color lineColor;
+        if (getState().isInvalid()) {
+            textColor = Toolkit.getColor(ColorsAndFonts.COLOR_INVALID);
+            lineColor = Toolkit.getColor(ColorsAndFonts.COLOR_SECONDARY3);
+        } else if (hasFocus()) {
+            if (isSaved) {
+                textColor = Toolkit.getColor(ColorsAndFonts.COLOR_TEXT_SAVED);
+                lineColor = Toolkit.getColor(ColorsAndFonts.COLOR_PRIMARY1);
+            } else {
+                textColor = Toolkit.getColor(ColorsAndFonts.COLOR_TEXT_EDIT);
+                lineColor = Toolkit.getColor(ColorsAndFonts.COLOR_PRIMARY1);
+            }
+        } else if (identified) {
+            textColor = Toolkit.getColor(ColorsAndFonts.COLOR_IDENTIFIED);
+            lineColor = Toolkit.getColor(ColorsAndFonts.COLOR_IDENTIFIED);
+        } else {
+            textColor = Toolkit.getColor(ColorsAndFonts.COLOR_BLACK);
+            lineColor = Toolkit.getColor(ColorsAndFonts.COLOR_SECONDARY3);
+        }
+
+        if (showLines == true && canChangeValue().isAllowed()) {
+            drawLines(canvas, lineColor, width);
+        }
+        drawText(canvas, textColor, width);
+    }
+
+    protected abstract void drawHighlight(final Canvas canvas, final int maxWidth);
+
+    protected abstract void drawLines(final Canvas canvas, final Color color, final int width);
+
+    protected abstract void drawText(final Canvas canvas, final Color textColor, final int width);
+
+    @Override
+    public void editComplete(final boolean moveFocus, final boolean toNextField) {
+        if (canChangeValue().isAllowed() && !isSaved) {
+            isSaved = true;
+            initiateSave(moveFocus);
+        } else if (moveFocus) {
+            if (toNextField) {
+                getFocusManager().focusNextView();
+            } else {
+                getFocusManager().focusPreviousView();
+            }
+        }
+    }
+
+    protected void end(final boolean alt, final boolean shift) {
+        if (alt) {
+            cursor.bottom();
+        } else {
+            cursor.end();
+        }
+
+        highlight(shift);
+        markDamaged();
+    }
+
+    /**
+     * Called when 'enter' has been pressed. Return indicates whether event has
+     * been consumed; by default it hasn't. This default implementation marks
+     * field as having edit completed.
+     */
+    protected boolean enter() {
+        editComplete(false, false);
+        return false;
+    }
+
+    @Override
+    public void entered() {
+        if (canChangeValue().isAllowed()) {
+            getFeedbackManager().showTextCursor();
+            identified = true;
+            markDamaged();
+        }
+        super.entered();
+    }
+
+    protected void escape() {
+        if (isSaved) {
+            clearValue();
+        } else {
+            invalidReason = null;
+            refresh();
+            markDamaged();
+        }
+    }
+
+    @Override
+    public void exited() {
+        if (canChangeValue().isAllowed()) {
+            getFeedbackManager().showDefaultCursor();
+            identified = false;
+            markDamaged();
+        }
+        super.exited();
+    }
+
+    /**
+     * Responds to first click by placing the cursor between the two characters
+     * nearest the point of the mouse.
+     */
+    @Override
+    public void firstClick(final Click click) {
+        if (canChangeValue().isAllowed()) {
+            final Location at = click.getLocation();
+            at.subtract(ViewConstants.HPADDING, ViewConstants.VPADDING);
+            cursor.cursorAt(at);
+            resetSelection();
+
+            // testing
+            if (cursor.getLine() > textContent.getNoLinesOfContent()) {
+                throw new IsisException("not inside content for line " + cursor.getLine() + " : " + textContent.getNoLinesOfContent());
+            }
+
+            markDamaged();
+        }
+
+        if (!canChangeValue().isAllowed() || click.isShift() || click.button2()) {
+            final ObjectAdapter valueAdapter = getContent().getAdapter();
+            if (valueAdapter != null && valueAdapter.titleString().length() > 0) {
+                final View textView = new BackgroundBorder(Toolkit.getColor(ColorsAndFonts.COLOR_PRIMARY3), new LineBorder(1, Toolkit.getColor(ColorsAndFonts.COLOR_PRIMARY1), new TextView(getContent(), null)));
+                getViewManager().setOverlayView(textView);
+
+                final int offset = getView().getPadding().getLeft();
+                final Location location = getAbsoluteLocation();
+                location.add(offset, 0);
+                textView.setLocation(location);
+                textView.markDamaged();
+            }
+        }
+    }
+
+    @Override
+    public void focusLost() {
+        super.focusLost();
+        editComplete(false, false);
+    }
+
+    @Override
+    public void focusReceived() {
+        getFeedbackManager().setError(invalidReason == null ? "" : invalidReason);
+        resetSelection();
+    }
+
+    @Override
+    public int getBaseline() {
+        return getText().getAscent();
+    }
+
+    @Override
+    public Size getRequiredSize(final Size availableSpace) {
+        final int width = ViewConstants.HPADDING + displayWidth + ViewConstants.HPADDING;
+        int height;
+        if (textContent.getNoDisplayLines() == 1) {
+            height = getText().getTextHeight();
+        } else {
+            height = textContent.getNoDisplayLines() * getText().getLineHeight();
+        }
+        height = Math.max(height, Toolkit.defaultFieldHeight());
+        return new Size(width, height);
+    }
+
+    @Override
+    public int getMaxFieldWidth() {
+        return displayWidth;
+    }
+
+    @Override
+    public Text getText() {
+        return style;
+    }
+
+    @Override
+    String getSelectedText() {
+        return textContent.getText(selection);
+    }
+
+    private ObjectAdapter getValue() {
+        return getContent().getAdapter();
+    }
+
+    /**
+     * modifies the selection object so that text is selected if the flag is
+     * true, or text is unselected if false.
+     */
+    private void highlight(final boolean select) {
+        if (canChangeValue().isAllowed()) {
+            if (!select) {
+                selection.resetTo(cursor);
+            } else {
+                selection.extendTo(cursor);
+            }
+        }
+    }
+
+    protected void home(final boolean alt, final boolean shift) {
+        if (alt) {
+            cursor.top();
+        } else {
+            cursor.home();
+        }
+
+        highlight(shift);
+        markDamaged();
+    }
+
+    protected void changeMade() {
+        isSaved = false;
+        markDamaged();
+        if (getState().isInvalid()) {
+            getState().clearInvalid();
+            getFeedbackManager().clearError();
+        }
+    }
+
+    private void insert(final String characters) {
+        if (withinMaximum(characters.length())) {
+            final int noLines = textContent.getNoDisplayLines();
+            textContent.insert(cursor, characters);
+            cursor.right(characters.length());
+            if (textContent.getNoDisplayLines() != noLines) {
+                invalidateLayout();
+            }
+            changeMade();
+        } else {
+            getFeedbackManager().setError("Entry can be no longer than " + maxLength + " characters");
+        }
+    }
+
+    public boolean isIdentified() {
+        return identified;
+    }
+
+    /**
+     * Called when the user presses any key on the keyboard while this view has
+     * the focus.
+     */
+    @Override
+    public void keyPressed(final KeyboardAction key) {
+        if (!canChangeValue().isAllowed()) {
+            return;
+        }
+
+        final int keyCode = key.getKeyCode();
+        if (keyCode == KeyEvent.VK_CONTROL || keyCode == KeyEvent.VK_SHIFT || keyCode == KeyEvent.VK_ALT) {
+            return;
+        }
+
+        final int modifiers = key.getModifiers();
+        // modifiers
+        final boolean alt = (modifiers & InputEvent.ALT_MASK) > 0;
+        final boolean shift = (modifiers & InputEvent.SHIFT_MASK) > 0;
+        final boolean ctrl = (modifiers & InputEvent.CTRL_MASK) > 0;
+
+        switch (keyCode) {
+        case KeyEvent.VK_PAGE_UP:
+            key.consume();
+            pageUp(shift, ctrl);
+            break;
+        case KeyEvent.VK_PAGE_DOWN:
+            key.consume();
+            pageDown(shift, ctrl);
+            break;
+        case KeyEvent.VK_V:
+            if (ctrl) {
+                key.consume();
+                pasteFromClipboard();
+                highlight(false);
+            }
+            break;
+        case KeyEvent.VK_C:
+            if (ctrl) {
+                key.consume();
+                copyToClipboard();
+            }
+            break;
+        case KeyEvent.VK_DOWN:
+            key.consume();
+            down(shift);
+            break;
+        case KeyEvent.VK_UP:
+            key.consume();
+            up(shift);
+            break;
+        case KeyEvent.VK_HOME:
+            key.consume();
+            home(alt, shift);
+            break;
+        case KeyEvent.VK_END:
+            key.consume();
+            end(alt, shift);
+            break;
+        case KeyEvent.VK_LEFT:
+            key.consume();
+            left(alt, shift);
+            break;
+        case KeyEvent.VK_RIGHT:
+            key.consume();
+            right(alt, shift);
+            break;
+        case KeyEvent.VK_DELETE:
+            key.consume();
+            deleteForward();
+            break;
+        case KeyEvent.VK_BACK_SPACE:
+            key.consume();
+            delete();
+            break;
+        case KeyEvent.VK_TAB:
+            key.consume();
+            final boolean moveToNextField = !shift;
+            tab(moveToNextField);
+            break;
+        case KeyEvent.VK_ENTER:
+            if (!enter()) {
+                getParent().keyPressed(key);
+            }
+            break;
+        case KeyEvent.VK_ESCAPE:
+            key.consume();
+            escape();
+            break;
+
+        default:
+            break;
+        }
+
+        LOG.debug("character at " + cursor.getCharacter() + " line " + cursor.getLine());
+        LOG.debug(selection);
+    }
+
+    /**
+     * Called when the user releases any key on the keyboard while this view has
+     * the focus.
+     */
+    @Override
+    public void keyReleased(final KeyboardAction action) {
+    }
+
+    /**
+     * Called when the user presses a non-control key (i.e. data entry keys and
+     * not shift, up-arrow etc). Such a key press will result in a prior call to
+     * <code>keyPressed</code> and a subsequent call to <code>keyReleased</code>
+     * .
+     */
+    @Override
+    public void keyTyped(final KeyboardAction action) {
+        if (canChangeValue().isAllowed()) {
+            insert("" + action.getKeyChar());
+        }
+    }
+
+    protected void left(final boolean alt, final boolean shift) {
+        if (alt) {
+            cursor.wordLeft();
+        } else {
+            cursor.left();
+        }
+
+        highlight(shift);
+        markDamaged();
+    }
+
+    protected void pageDown(final boolean shift, final boolean ctrl) {
+        if (ctrl) {
+            if (textContent.decreaseDepth()) {
+                textContent.alignDisplay(cursor.getLine());
+                invalidateLayout();
+            }
+        } else {
+            cursor.pageDown();
+            highlight(shift);
+        }
+        markDamaged();
+    }
+
+    protected void pageUp(final boolean shift, final boolean ctrl) {
+        if (ctrl) {
+            textContent.increaseDepth();
+            textContent.alignDisplay(cursor.getLine());
+            invalidateLayout();
+        } else {
+            cursor.pageUp();
+            highlight(shift);
+        }
+        markDamaged();
+    }
+
+    @Override
+    protected void pasteFromClipboard() {
+        try {
+            final String text = (String) getViewManager().getClipboard(String.class);
+            insert(text);
+            LOG.debug("pasted " + text);
+        } catch (final Throwable e) {
+            LOG.error("invalid paste operation " + e);
+        }
+    }
+
+    private String titleString(final ObjectAdapter object) {
+        return ((TextParseableContent) getContent()).titleString(object);
+    }
+
+    @Override
+    public void refresh() {
+        super.refresh();
+        final ObjectAdapter object = getValue();
+        if (object == null) {
+            textContent.setText("");
+        } else {
+            textContent.setText(titleString(object));
+        }
+        isSaved = true;
+    }
+
+    private void resetSelection() {
+        selection.resetTo(cursor);
+    }
+
+    protected void right(final boolean alt, final boolean shift) {
+        if (alt) {
+            cursor.wordRight();
+        } else {
+            cursor.right();
+        }
+
+        highlight(shift);
+        markDamaged();
+    }
+
+    @Override
+    protected void save() {
+        final String entry = textContent.getText();
+
+        // do nothing if entry is same as the value object
+        final ObjectAdapter value = getValue();
+        if (!entry.equals(value == null ? "" : value.titleString())) {
+            LOG.debug("field edited: \'" + entry + "\' to replace \'" + (value == null ? "" : value.titleString()) + "\'");
+
+            try {
+                parseEntry(entry.toString());
+                invalidReason = null;
+                getViewManager().getSpy().addAction("VALID ENTRY: " + entry);
+                markDamaged();
+                getParent().invalidateContent();
+            } catch (final TextEntryParseException e) {
+                invalidReason = "Invalid Entry: " + e.getMessage();
+                getFeedbackManager().setError(invalidReason);
+                getState().setInvalid();
+                markDamaged();
+            } catch (final InvalidEntryException e) {
+                invalidReason = "Invalid Entry: " + e.getMessage();
+                getFeedbackManager().setError(invalidReason);
+                getState().setInvalid();
+                markDamaged();
+            } catch (final ConcurrencyException e) {
+                invalidReason = "Update Failure: " + e.getMessage();
+                getState().setOutOfSynch();
+                markDamaged();
+                throw e;
+            } catch (final IsisException e) {
+                invalidReason = "Update Failure: " + e.getMessage();
+                getFeedbackManager().setError(invalidReason);
+                getState().setOutOfSynch();
+                markDamaged();
+                throw e;
+            }
+        }
+    }
+
+    @Override
+    public void secondClick(final Click click) {
+        if (canChangeValue().isAllowed()) {
+            selection.selectWord();
+        }
+    }
+
+    /**
+     * Set the maximum width of the field, as a number of characters
+     */
+    private void setTextWidth(final int noCharacters) {
+        displayWidth = getText().charWidth('5') * (noCharacters + 3);
+    }
+
+    /**
+     * Set the width of the field, as a number of pixels
+     */
+    public void setWidth(final int width) {
+        displayWidth = width;
+    }
+
+    @Override
+    public void setSize(final Size size) {
+        super.setSize(size);
+        setWidth(size.getWidth() - 2 * ViewConstants.HPADDING);
+    }
+
+    protected void tab(final boolean moveToNextField) {
+        editComplete(true, moveToNextField);
+    }
+
+    @Override
+    public void thirdClick(final Click click) {
+        if (canChangeValue().isAllowed()) {
+            selection.selectSentence();
+            markDamaged();
+        }
+    }
+
+    protected void up(final boolean shift) {
+        cursor.lineUp();
+        highlight(shift);
+        markDamaged();
+    }
+
+    private boolean withinMaximum(final int characters) {
+        return maxLength == 0 || textContent.getText().length() + characters <= maxLength;
+    }
+
+    void revertInvalidEntry() {
+        invalidReason = null;
+        refresh();
+        cursor.home();
+        getState().clearInvalid();
+        getFeedbackManager().clearError();
+        markDamaged();
+    }
+
+    public boolean hasInvalidEntry() {
+        return invalidReason != null;
+    }
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/255ef514/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/field/TextFieldBorder.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/field/TextFieldBorder.java b/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/field/TextFieldBorder.java
new file mode 100644
index 0000000..9826266
--- /dev/null
+++ b/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/field/TextFieldBorder.java
@@ -0,0 +1,49 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+package org.apache.isis.viewer.dnd.field;
+
+import org.apache.isis.viewer.dnd.drawing.Canvas;
+import org.apache.isis.viewer.dnd.drawing.ColorsAndFonts;
+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;
+
+/**
+ * Border decorator to draw a white background and 3D style border around a text
+ * field.
+ */
+public class TextFieldBorder extends AbstractBorder {
+
+    public TextFieldBorder(final View view) {
+        super(view);
+        top = bottom = left = right = 2;
+    }
+
+    @Override
+    public void draw(final Canvas canvas) {
+        final int height = getSize().getHeight() - 2;
+        final int width = getSize().getWidth();
+        canvas.drawSolidRectangle(0, 1, width - 1, height - 2, Toolkit.getColor(ColorsAndFonts.COLOR_WHITE));
+        canvas.drawRectangle(0, 1, width - 3, height - 2, Toolkit.getColor(ColorsAndFonts.COLOR_SECONDARY1));
+        canvas.drawRectangle(1, 2, width - 1, height - 2, Toolkit.getColor(ColorsAndFonts.COLOR_WHITE));
+
+        super.draw(canvas);
+    }
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/255ef514/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/field/TextFieldSpecification.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/field/TextFieldSpecification.java b/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/field/TextFieldSpecification.java
new file mode 100644
index 0000000..ee38309
--- /dev/null
+++ b/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/field/TextFieldSpecification.java
@@ -0,0 +1,59 @@
+/*
+ *  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.field;
+
+import org.apache.isis.viewer.dnd.view.Axes;
+import org.apache.isis.viewer.dnd.view.Content;
+import org.apache.isis.viewer.dnd.view.View;
+import org.apache.isis.viewer.dnd.view.ViewRequirement;
+import org.apache.isis.viewer.dnd.view.base.AbstractFieldSpecification;
+import org.apache.isis.viewer.dnd.view.border.TextFieldResizeBorder;
+import org.apache.isis.viewer.dnd.view.content.TextParseableContent;
+import org.apache.isis.viewer.dnd.view.lookup.OpenValueDropDownBorder;
+
+/**
+ * Creates a single line text field with the base line drawn.
+ */
+public class TextFieldSpecification extends AbstractFieldSpecification {
+    @Override
+    public boolean canDisplay(final ViewRequirement requirement) {
+        return requirement.isTextParseable() && ((TextParseableContent) requirement.getContent()).getNoLines() == 1;
+    }
+
+    @Override
+    public View createView(final Content content, final Axes axes, final int sequence) {
+        final View field = new TextFieldResizeBorder(new SingleLineTextField((TextParseableContent) content, this, true));
+        if (content.isOptionEnabled()) {
+            return new OpenValueDropDownBorder(field);
+        } else {
+            return field;
+        }
+    }
+
+    @Override
+    public String getName() {
+        return "Single Line Text Field";
+    }
+
+    @Override
+    public boolean isAligned() {
+        return false;
+    }
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/255ef514/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/field/TextParseableFieldAbstract.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/field/TextParseableFieldAbstract.java b/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/field/TextParseableFieldAbstract.java
new file mode 100644
index 0000000..bc1a1c8
--- /dev/null
+++ b/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/field/TextParseableFieldAbstract.java
@@ -0,0 +1,106 @@
+/*
+ *  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.field;
+
+import org.apache.log4j.Logger;
+
+import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
+import org.apache.isis.core.metamodel.consent.Consent;
+import org.apache.isis.viewer.dnd.view.Content;
+import org.apache.isis.viewer.dnd.view.ViewSpecification;
+import org.apache.isis.viewer.dnd.view.content.TextParseableContent;
+
+public abstract class TextParseableFieldAbstract extends AbstractField {
+    private static final Logger LOG = Logger.getLogger(TextField.class);
+
+    protected TextParseableFieldAbstract(final Content content, final ViewSpecification design) {
+        super(content, design);
+    }
+
+    @Override
+    protected boolean provideClearCopyPaste() {
+        return true;
+    }
+
+    @Override
+    protected void pasteFromClipboard() {
+        try {
+            final String text = (String) getViewManager().getClipboard(String.class);
+            final TextParseableContent content = (TextParseableContent) getContent();
+            content.parseTextEntry(text);
+            content.entryComplete();
+            LOG.debug("pasted " + text);
+        } catch (final Throwable e) {
+            LOG.error("invalid paste operation " + e);
+        }
+    }
+
+    @Override
+    protected Consent canClear() {
+        final TextParseableContent field = (TextParseableContent) getContent();
+        return field.canClear();
+    }
+
+    @Override
+    protected void clear() {
+        try {
+            final TextParseableContent content = (TextParseableContent) getContent();
+            content.parseTextEntry("");
+            content.entryComplete();
+            LOG.debug("cleared");
+        } catch (final Throwable e) {
+            LOG.error("invalid paste operation " + e);
+        }
+    }
+
+    @Override
+    protected void copyToClipboard() {
+        final TextParseableContent content = (TextParseableContent) getContent();
+        final ObjectAdapter object = content.getAdapter();
+        if (object != null) {
+            final String text = object.titleString();
+            getViewManager().setClipboard(text, String.class);
+            LOG.debug("copied " + text);
+        }
+    }
+
+    @Override
+    public boolean isEmpty() {
+        final TextParseableContent content = (TextParseableContent) getContent();
+        return content.isEmpty();
+    }
+
+    @Override
+    public Consent canChangeValue() {
+        final TextParseableContent cont = (TextParseableContent) getContent();
+        return cont.isEditable();
+    }
+
+    protected void saveValue(final ObjectAdapter value) {
+        parseEntry(value.titleString());
+    }
+
+    protected void parseEntry(final String entryText) {
+        final TextParseableContent content = (TextParseableContent) getContent();
+        content.parseTextEntry(entryText);
+        content.entryComplete();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/255ef514/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/field/WrappedTextField.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/field/WrappedTextField.java b/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/field/WrappedTextField.java
new file mode 100644
index 0000000..34c4fab
--- /dev/null
+++ b/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/field/WrappedTextField.java
@@ -0,0 +1,158 @@
+/*
+ *  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.field;
+
+import org.apache.log4j.Logger;
+
+import org.apache.isis.core.commons.exceptions.IsisException;
+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.ViewConstants;
+import org.apache.isis.viewer.dnd.view.ViewSpecification;
+import org.apache.isis.viewer.dnd.view.content.TextParseableContent;
+import org.apache.isis.viewer.dnd.view.text.CursorPosition;
+import org.apache.isis.viewer.dnd.view.text.TextContent;
+
+public class WrappedTextField extends TextField {
+    private static final Logger LOG = Logger.getLogger(WrappedTextField.class);
+
+    public WrappedTextField(final TextParseableContent content, final ViewSpecification specification, final boolean showLines) {
+        super(content, specification, showLines, TextContent.WRAPPING);
+    }
+
+    public void setWrapping(final boolean wrapping) {
+    }
+
+    @Override
+    protected void drawLines(final Canvas canvas, final Color color, final int width) {
+        int baseline = getBaseline();
+        final int noDisplayLines = textContent.getNoDisplayLines();
+        for (int line = 0; line < noDisplayLines; line++) {
+            canvas.drawLine(ViewConstants.HPADDING, baseline, ViewConstants.HPADDING + width, baseline, color);
+            baseline += getText().getLineHeight();
+        }
+    }
+
+    @Override
+    protected void drawHighlight(final Canvas canvas, final int maxWidth) {
+        final int baseline = getBaseline();
+        int top = baseline - style.getAscent();
+
+        final CursorPosition from = selection.from();
+        final CursorPosition to = selection.to();
+
+        final String[] lines = textContent.getDisplayLines();
+        final int displayFromLine = textContent.getDisplayFromLine();
+        final int displayToLine = displayFromLine + lines.length;
+        for (int i = displayFromLine; i <= displayToLine; i++) {
+            if ((i >= from.getLine()) && (i <= to.getLine())) {
+                final String line = textContent.getText(i);
+                int start = 0;
+                int end = style.stringWidth(line);
+
+                if (from.getLine() == i) {
+                    final int at = Math.min(from.getCharacter(), line.length());
+                    start = style.stringWidth(line.substring(0, at));
+                }
+
+                if (to.getLine() == i) {
+                    final int at = Math.min(to.getCharacter(), line.length());
+                    end = style.stringWidth(line.substring(0, at));
+                }
+
+                canvas.drawSolidRectangle(start + (ViewConstants.HPADDING), top, end - start, getText().getLineHeight(), Toolkit.getColor(ColorsAndFonts.COLOR_TEXT_HIGHLIGHT));
+            }
+
+            top += getText().getLineHeight();
+        }
+    }
+
+    @Override
+    protected void drawText(final Canvas canvas, final Color textColor, final int width) {
+        int baseline = getBaseline();
+        final String[] lines = textContent.getDisplayLines();
+        final int cursorLine = cursor.getLine() - textContent.getDisplayFromLine();
+        for (int i = 0; i < lines.length; i++) {
+            final String chars = lines[i];
+            if (chars == null) {
+                throw new IsisException();
+            }
+            if (chars.endsWith("\n")) {
+                throw new RuntimeException();
+            }
+
+            // draw cursor
+            if (hasFocus() && canChangeValue().isAllowed() && cursorLine == i) {
+                final int at = Math.min(cursor.getCharacter(), chars.length());
+                final int pos = style.stringWidth(chars.substring(0, at)) + ViewConstants.HPADDING;
+                canvas.drawLine(pos, (baseline + style.getDescent()), pos, baseline - style.getAscent(), Toolkit.getColor(ColorsAndFonts.COLOR_TEXT_CURSOR));
+            }
+
+            // draw text
+            canvas.drawText(chars, ViewConstants.HPADDING, baseline, textColor, style);
+            baseline += getText().getLineHeight();
+        }
+        /*
+         * if (end < entryLength) { int x = style.stringWidth(new String(buffer,
+         * start, end)); g.setColor(Color.red); g.drawString("\u00bb", x,
+         * baseline - lineHeight()); }
+         */
+    }
+
+    @Override
+    protected boolean enter() {
+        textContent.breakBlock(cursor);
+        cursor.lineDown();
+        cursor.home();
+        markDamaged();
+        return true;
+    }
+
+    /**
+     * Sets the number of lines to display
+     */
+    public void setNoLines(final int noLines) {
+        textContent.setNoDisplayLines(noLines);
+    }
+
+    @Override
+    public void setSize(final Size size) {
+        super.setSize(size);
+        textContent.setNoDisplayLines(size.getHeight() / style.getLineHeight());
+    }
+
+    @Override
+    public void setMaximumSize(final Size size) {
+        final int lines = Math.max(1, size.getHeight() / getText().getLineHeight());
+        setNoLines(lines);
+        final int width = Math.max(180, size.getWidth() - ViewConstants.HPADDING);
+        setWidth(width);
+        LOG.debug(lines + " x " + width);
+        invalidateLayout();
+    }
+
+    @Override
+    protected void align() {
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/255ef514/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/form/AbstractFormSpecification.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/form/AbstractFormSpecification.java b/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/form/AbstractFormSpecification.java
new file mode 100644
index 0000000..70cb041
--- /dev/null
+++ b/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/form/AbstractFormSpecification.java
@@ -0,0 +1,52 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+package org.apache.isis.viewer.dnd.form;
+
+import org.apache.isis.viewer.dnd.view.Content;
+import org.apache.isis.viewer.dnd.view.ViewFactory;
+import org.apache.isis.viewer.dnd.view.ViewRequirement;
+import org.apache.isis.viewer.dnd.view.composite.StandardFields;
+
+public abstract class AbstractFormSpecification extends AbstractObjectViewSpecification {
+
+    @Override
+    protected ViewFactory createFieldFactory() {
+        return new StandardFields() {
+            @Override
+            protected int collectionRequirement() {
+                return AbstractFormSpecification.this.collectionRequirement();
+            }
+
+            @Override
+            protected boolean include(final Content content, final int sequence) {
+                return AbstractFormSpecification.this.include(content, sequence);
+            }
+        };
+    }
+
+    protected int collectionRequirement() {
+        return ViewRequirement.OPEN | ViewRequirement.SUBVIEW;
+    }
+
+    protected boolean include(final Content content, final int sequence) {
+        return true;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/255ef514/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/form/AbstractObjectViewSpecification.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/form/AbstractObjectViewSpecification.java b/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/form/AbstractObjectViewSpecification.java
new file mode 100644
index 0000000..65feca0
--- /dev/null
+++ b/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/form/AbstractObjectViewSpecification.java
@@ -0,0 +1,55 @@
+/*
+ *  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.form;
+
+import org.apache.isis.viewer.dnd.view.Axes;
+import org.apache.isis.viewer.dnd.view.Content;
+import org.apache.isis.viewer.dnd.view.ViewFactory;
+import org.apache.isis.viewer.dnd.view.ViewRequirement;
+import org.apache.isis.viewer.dnd.view.base.Layout;
+import org.apache.isis.viewer.dnd.view.composite.CompositeViewSpecification;
+import org.apache.isis.viewer.dnd.view.composite.ObjectFieldBuilder;
+import org.apache.isis.viewer.dnd.view.composite.StackLayout;
+import org.apache.isis.viewer.dnd.view.composite.StandardFields;
+
+public abstract class AbstractObjectViewSpecification extends CompositeViewSpecification {
+
+    public AbstractObjectViewSpecification() {
+        builder = new ObjectFieldBuilder(createFieldFactory());
+        init();
+    }
+
+    protected void init() {
+    }
+
+    protected ViewFactory createFieldFactory() {
+        return new StandardFields();
+    }
+
+    @Override
+    public Layout createLayout(final Content content, final Axes axes) {
+        return new StackLayout();
+    }
+
+    @Override
+    public boolean canDisplay(final ViewRequirement requirement) {
+        return requirement.isObject() && !requirement.isTextParseable() && requirement.hasReference() && requirement.isOpen();
+    }
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/255ef514/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/form/ExpandableFormSpecification.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/form/ExpandableFormSpecification.java b/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/form/ExpandableFormSpecification.java
new file mode 100644
index 0000000..173eb26
--- /dev/null
+++ b/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/form/ExpandableFormSpecification.java
@@ -0,0 +1,49 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+package org.apache.isis.viewer.dnd.form;
+
+import org.apache.isis.viewer.dnd.view.ViewRequirement;
+import org.apache.isis.viewer.dnd.view.border.IconBorder;
+import org.apache.isis.viewer.dnd.view.composite.FieldLabelsDecorator;
+
+public class ExpandableFormSpecification extends AbstractFormSpecification {
+
+    @Override
+    public boolean canDisplay(final ViewRequirement requirement) {
+        return super.canDisplay(requirement) && !requirement.is(ViewRequirement.SUBVIEW);
+    }
+
+    @Override
+    protected void init() {
+        addSubviewDecorator(new ExpandableViewBorder.Factory());
+        addSubviewDecorator(new FieldLabelsDecorator());
+        addViewDecorator(new IconBorder.Factory());
+    }
+
+    @Override
+    protected int collectionRequirement() {
+        return super.collectionRequirement() | ViewRequirement.EXPANDABLE;
+    }
+
+    @Override
+    public String getName() {
+        return "Expanding Form (experimental)";
+    }
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/255ef514/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/form/ExpandableViewBorder.java
----------------------------------------------------------------------
diff --git a/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/form/ExpandableViewBorder.java b/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/form/ExpandableViewBorder.java
new file mode 100644
index 0000000..1a07ff0
--- /dev/null
+++ b/component/viewer/dnd/src/main/java/org/apache/isis/viewer/dnd/form/ExpandableViewBorder.java
@@ -0,0 +1,246 @@
+/*
+ *  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.form;
+
+import java.util.List;
+
+import org.apache.isis.applib.annotation.Where;
+import org.apache.isis.core.commons.debug.DebugBuilder;
+import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
+import org.apache.isis.core.metamodel.facets.collections.modify.CollectionFacet;
+import org.apache.isis.core.metamodel.facets.collections.modify.CollectionFacetUtils;
+import org.apache.isis.core.metamodel.spec.feature.ObjectAssociation;
+import org.apache.isis.core.metamodel.spec.feature.ObjectAssociationFilters;
+import org.apache.isis.core.metamodel.spec.feature.OneToOneAssociation;
+import org.apache.isis.runtimes.dflt.runtime.system.context.IsisContext;
+import org.apache.isis.viewer.dnd.drawing.Canvas;
+import org.apache.isis.viewer.dnd.drawing.ColorsAndFonts;
+import org.apache.isis.viewer.dnd.drawing.Shape;
+import org.apache.isis.viewer.dnd.icon.SubviewIconSpecification;
+import org.apache.isis.viewer.dnd.list.InternalListSpecification;
+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.ObjectContent;
+import org.apache.isis.viewer.dnd.view.SubviewDecorator;
+import org.apache.isis.viewer.dnd.view.Toolkit;
+import org.apache.isis.viewer.dnd.view.View;
+import org.apache.isis.viewer.dnd.view.ViewAxis;
+import org.apache.isis.viewer.dnd.view.ViewRequirement;
+import org.apache.isis.viewer.dnd.view.ViewSpecification;
+import org.apache.isis.viewer.dnd.view.base.AbstractBorder;
+import org.apache.isis.viewer.dnd.view.collection.CollectionContent;
+import org.apache.isis.viewer.dnd.view.collection.CollectionElement;
+import org.apache.isis.viewer.dnd.view.content.FieldContent;
+import org.apache.isis.viewer.dnd.view.field.OneToOneFieldImpl;
+
+public class ExpandableViewBorder extends AbstractBorder {
+    public static final int CAN_OPEN = 1;
+    public static final int CANT_OPEN = 2;
+    public static final int UNKNOWN = 0;
+
+    public static class Factory implements SubviewDecorator {
+        private final ViewSpecification openObjectViewSpecification;
+        private final ViewSpecification closedViewSpecification;
+        private final ViewSpecification openCollectionViewSpecification;
+
+        public Factory() {
+            this.closedViewSpecification = new SubviewIconSpecification();
+            this.openObjectViewSpecification = new InternalFormSpecification();
+            this.openCollectionViewSpecification = new InternalListSpecification();
+        }
+
+        public Factory(final ViewSpecification closedViewSpecification, final ViewSpecification openObjectViewSpecification, final ViewSpecification openCollectionViewSpecification) {
+            this.closedViewSpecification = closedViewSpecification;
+            this.openObjectViewSpecification = openObjectViewSpecification;
+            this.openCollectionViewSpecification = openCollectionViewSpecification;
+        }
+
+        @Override
+        public ViewAxis createAxis(final Content content) {
+            return null;
+        }
+
+        @Override
+        public View decorate(final Axes axes, final View view) {
+            if (view.getContent().isObject()) {
+                return new ExpandableViewBorder(view, closedViewSpecification, openObjectViewSpecification);
+            } else if (view.getContent().isCollection()) {
+                return new ExpandableViewBorder(view, closedViewSpecification, openCollectionViewSpecification);
+            } else {
+                return view;
+            }
+        }
+    }
+
+    private boolean isOpen = false;
+    private final ViewSpecification openViewSpecification;
+    private final ViewSpecification closedViewSpecification;
+    private int canOpen;
+
+    // REVIEW: should provide this rendering context, rather than hardcoding.
+    // the net effect currently is that class members annotated with 
+    // @Hidden(where=Where.ANYWHERE) or @Disabled(where=Where.ANYWHERE) will indeed
+    // be hidden/disabled, but will be visible/enabled (perhaps incorrectly) 
+    // for any other value for Where
+    private final Where where = Where.ANYWHERE;
+
+    public ExpandableViewBorder(final View view, final ViewSpecification closedViewSpecification, final ViewSpecification openViewSpecification) {
+        super(view);
+        left = Toolkit.defaultFieldHeight();
+        this.openViewSpecification = openViewSpecification;
+        this.closedViewSpecification = closedViewSpecification;
+        canOpen();
+    }
+
+    @Override
+    protected void debugDetails(final DebugBuilder debug) {
+        super.debugDetails(debug);
+        debug.appendln("open spec", openViewSpecification);
+        debug.appendln("closed spec", closedViewSpecification);
+        debug.appendln("open", isOpen);
+    }
+
+    @Override
+    public void draw(final Canvas canvas) {
+        Shape pointer;
+        if (isOpen) {
+            pointer = new Shape(0, left / 2);
+            pointer.addPoint(left - 2 - 2, left / 2);
+            pointer.addPoint(left / 2 - 2, left - 2);
+        } else {
+            pointer = new Shape(2, 2);
+            pointer.addPoint(2, left - 2);
+            pointer.addPoint(left / 2, 2 + (left - 2) / 2);
+        }
+        if (canOpen == CAN_OPEN) {
+            canvas.drawSolidShape(pointer, Toolkit.getColor(ColorsAndFonts.COLOR_PRIMARY1));
+        } else if (canOpen == UNKNOWN) {
+            canvas.drawShape(pointer, Toolkit.getColor(ColorsAndFonts.COLOR_PRIMARY1));
+        } else {
+            canvas.drawShape(pointer, Toolkit.getColor(ColorsAndFonts.COLOR_SECONDARY3));
+        }
+
+        super.draw(canvas);
+    }
+
+    @Override
+    public void firstClick(final Click click) {
+        if (click.getLocation().getX() < left) {
+            if (canOpen == UNKNOWN) {
+                resolveContent();
+                markDamaged();
+            }
+            if (canOpen != CANT_OPEN) {
+                isOpen = !isOpen;
+
+                final View parent = wrappedView.getParent();
+
+                getViewManager().removeFromNotificationList(wrappedView);
+                if (isOpen) {
+                    wrappedView = createOpenView();
+                } else {
+                    wrappedView = createClosedView();
+                }
+                setView(this);
+                setParent(parent);
+                getParent().invalidateLayout();
+                canOpen();
+            }
+        } else {
+            super.firstClick(click);
+        }
+    }
+
+    private View createClosedView() {
+        return closedViewSpecification.createView(getContent(), getViewAxes(), -1);
+    }
+
+    private View createOpenView() {
+        return openViewSpecification.createView(getContent(), getViewAxes(), -1);
+    }
+
+    @Override
+    public void update(final ObjectAdapter object) {
+        super.update(object);
+        canOpen();
+    }
+
+    private void canOpen() {
+        final Content content = getContent();
+        if (content.isCollection()) {
+            canOpen = canOpenCollection(content);
+        } else if (content.isObject()) {
+            canOpen = canOpenObject(content);
+        }
+    }
+
+    private int canOpenCollection(final Content content) {
+        final ObjectAdapter collection = ((CollectionContent) content).getCollection();
+        if (collection.isGhost()) {
+            return UNKNOWN;
+        } else {
+            final CollectionFacet facet = CollectionFacetUtils.getCollectionFacetFromSpec(collection);
+            return facet.size(collection) > 0 ? CAN_OPEN : CANT_OPEN;
+        }
+    }
+
+    private int canOpenObject(final Content content) {
+        final ObjectAdapter object = ((ObjectContent) content).getObject();
+        if (object != null) {
+            final List<ObjectAssociation> fields = object.getSpecification().getAssociations(ObjectAssociationFilters.dynamicallyVisible(IsisContext.getAuthenticationSession(), object, where));
+            for (int i = 0; i < fields.size(); i++) {
+                if (fields.get(i).isOneToManyAssociation()) {
+                    return CAN_OPEN;
+                } else if (fields.get(i).isOneToOneAssociation() && !fields.get(i).getSpecification().isParseable() && fieldContainsReference(object, fields.get(i))) {
+                    return CAN_OPEN;
+                }
+            }
+        }
+        final boolean openForObjectsWithOutReferences = true;
+        return openForObjectsWithOutReferences ? CAN_OPEN : CANT_OPEN;
+    }
+
+    private boolean fieldContainsReference(final ObjectAdapter parent, final ObjectAssociation field) {
+        final OneToOneAssociation association = (OneToOneAssociation) field;
+        final OneToOneFieldImpl fieldContent = new OneToOneFieldImpl(parent, field.get(parent), association);
+        if (openViewSpecification.canDisplay(new ViewRequirement(fieldContent, ViewRequirement.OPEN))) {
+            return true;
+        }
+        return false;
+    }
+
+    private void resolveContent() {
+        ObjectAdapter parent = getParent().getContent().getAdapter();
+        if (!(parent instanceof ObjectAdapter)) {
+            parent = getParent().getParent().getContent().getAdapter();
+        }
+
+        if (getContent() instanceof FieldContent) {
+            final ObjectAssociation field = ((FieldContent) getContent()).getField();
+            IsisContext.getPersistenceSession().resolveField(parent, field);
+        } else if (getContent() instanceof CollectionContent) {
+            IsisContext.getPersistenceSession().resolveImmediately(parent);
+        } else if (getContent() instanceof CollectionElement) {
+            IsisContext.getPersistenceSession().resolveImmediately(getContent().getAdapter());
+        }
+    }
+
+}