XML Editor in Source View of MultiView Component (Part 3)

Let's extend the visual editor (read part 1 and part 2 first) a bit further. We'll imagine we have an XML file with this content:

So we'll spend some time working on the visual MultiViewElement, so that the Visual Library ends up being used for setting the elements in the XML file:

For now, we will only synchronize from the visual view to the XML view, but not the other way round. In other words, when changes are made to the visual view, the source file will be updated. But when changes are made in the source file, nothing will happen in the visual view (except when the file is reopened in the IDE, of course, which is when it is first parsed).

Changes to the visual view will be done by moving a widget or by double-clicking on a name:

To achieve the above, first follow part 1 and part 2 and then do the following:

  1. Model the XML File. Here's a model class I created based on the XML file shown above:
    public class Task {
    
        String name;
        int x;
        int y;
        private PropertyChangeSupport propertyChangeSupport;
    
        public Task() {
            propertyChangeSupport = new PropertyChangeSupport(this);
        }
    
        public PropertyChangeSupport getPropertyChangeSupport() {
            return propertyChangeSupport;
        }
    
        public void setPropertyChangeSupport(PropertyChangeSupport propertyChangeSupport) {
            this.propertyChangeSupport = propertyChangeSupport;
        }
    
        public int getX() {
            return x;
        }
    
        public void setX(int newValue) {
            int oldValue = this.x;
            this.x = newValue;
            propertyChangeSupport.firePropertyChange("x", oldValue, newValue);
        }
    
        public int getY() {
            return y;
        }
    
        public void setY(int newValue) {
            int oldValue = this.y;
            this.y = newValue;
            propertyChangeSupport.firePropertyChange("y", oldValue, newValue);
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String newValue) {
            String oldValue = this.name;
            this.name = newValue;
            propertyChangeSupport.firePropertyChange("name", oldValue, newValue);
        }
    
        public void addPropertyChangeListener(PropertyChangeListener listener) {
            propertyChangeSupport.addPropertyChangeListener(listener);
        }
    
        public void removePropertyChangeListener(PropertyChangeListener listener) {
            propertyChangeSupport.removePropertyChangeListener(listener);
        }
    
    }

  2. Update the VisualView. Everything is controlled from the visual MultiViewElement. Open it and replace the content of the "createElement" method with the following:
    @Override
    public MultiViewElement createElement() {
        //Initialize ExplorerManager:
        em = new ExplorerManager();
        //Create the node hierarchy from the inputstream,
        //i.e., from the XML file:
        try {
            InputStream inputStream = support.getInputStream();
            em.setRootContext(new AbstractNode(Children.create(new ActivityChildFactory(inputStream), false)));
        } catch (Exception ex) {
        }
        //Set layout of MultiViewElement:
        this.setLayout(new BorderLayout());
        //Create new JScrollPane with Visual Library support,
        //passing in the 'AbcEditorSupport' class,
        //so that it can be serialized later:
        VisLibScrollPane scrollPane = new VisLibScrollPane(support);
        //Add the JScrollPane to the MultiViewElement:
        this.add(scrollPane, BorderLayout.CENTER);
        return this;
    }

  3. Create the Node Hierarchy. Here's the ChildFactory referred to above. Note that we parse the file, using the NetBeans Platform's XMLUtil class, but it could be parsed any other way you like:
    public class ActivityChildFactory extends ChildFactory<Task> {
    
        private final InputStream stream;
    
        public ActivityChildFactory(InputStream stream) {
            this.stream = stream;
        }
    
        @Override
        protected boolean createKeys(List<Task> list) {
            try {
                InputSource source = new InputSource(stream);
                org.w3c.dom.Document doc = XMLUtil.parse(source, true, true, null, null);
                org.w3c.dom.NodeList taskElements = doc.getElementsByTagName("task");
                for (int i = 0; i < taskElements.getLength(); i++) {
                    org.w3c.dom.Node taskNode = taskElements.item(i);
                    Task task = new Task();
                    NamedNodeMap map = taskNode.getAttributes();
                    for (int j = 0; j < map.getLength(); j++) {
                        org.w3c.dom.Node attrNode = map.item(j);
                        String attrName = attrNode.getNodeName();
                        if (attrName.equals("name")) {
                            task.setName(attrNode.getNodeValue());
                        }
                        if (attrName.equals("x")) {
                            task.setX(Integer.parseInt(attrNode.getNodeValue()));
                        }
                        if (attrName.equals("y")) {
                            task.setY(Integer.parseInt(attrNode.getNodeValue()));
                        }
                    }
                    list.add(task);
                }
            } catch (IOException ex) {
                Exceptions.printStackTrace(ex);
            } catch (SAXException ex) {
                Exceptions.printStackTrace(ex);
            }
            return true;
        }
    
        @Override
        protected Node createNodeForKey(Task task) {
            return new TaskNode(task);
        }
    
        private class TaskNode extends AbstractNode {
    
            private TaskNode(Task task) {
                super(Children.LEAF, Lookups.singleton(task));
                setDisplayName(task.getName());
            }
    
        }
        
    }

  4. Create the JScrollPane with Visual Library Widgets. A lot of the JScrollPane below is based on Toni Epple's blog entry Creating An Explorer View with the Visual Library (3). The additions below relate to updating the Task when the name is changed or the X and Y values are modified:
    public class VisLibScrollPane extends JScrollPane {
    
        ExplorerManager em;
        ObjectScene scene;
        WidgetAction selectAction;
        private final AbcEditorSupport support;
    
        public VisLibScrollPane(AbcEditorSupport support) {
            selectAction = ActionFactory.createSelectAction(new SelectProviderImpl());
            this.support = support;
        }
    
        @Override
        public void addNotify() {
            super.addNotify();
            ExplorerManager m = ExplorerManager.find(this);
            if (m == em) {
                // do nothing
            } else {
                em = m;
                scene = new ObjectScene();
                Node root = em.getRootContext();
                addWidgetsForNodes(root.getChildren().getNodes(), scene);
                setViewportView(scene.createView());
            }
        }
    
        //Send the current state of the scene together with the file to be serialized,
        //call this after a name is changed or an X or Y is updated:
        private void serialize() {
            SceneSerializer.serialize(scene, FileUtil.toFile(support.getDataObject().getPrimaryFile()));
        }
    
        private void addWidgetsForNodes(Node[] nodes, final ObjectScene scene) {
            for (int i = 0; i < nodes.length; i++) {
                Node node = nodes[i];
                //Get the Task object from the Node Lookup:
                Task task = node.getLookup().lookup(Task.class);
                IconNodeWidget w = new IconNodeWidget(scene, IconNodeWidget.TextOrientation.RIGHT_CENTER);
                w.setLabel(task.getName());
                w.setImage(node.getIcon(BeanInfo.ICON_COLOR_32x32));
                w.getActions().addAction(ActionFactory.createMoveAction(new TaskMoveProvider(task), new TaskMoveProvider(task)));
                w.setPreferredLocation(new Point(task.getX(), task.getY()));
                w.setOpaque(true);
                scene.addObject(node, w);
                scene.addChild(w);
                w.getLabelWidget().getActions().addAction(ActionFactory.createInplaceEditorAction(new LabelTextFieldEditor(task)));
                w.getActions().addAction(selectAction);
            }
        }
    
        private class TaskMoveProvider implements MoveProvider, MoveStrategy {
    
            private Point original;
            private final Task task;
    
            private TaskMoveProvider(Task task) {
                this.task = task;
            }
    
            public void movementStarted(Widget widget) {
            }
    
            public void movementFinished(Widget widget) {
                original = null;
            }
    
            public Point getOriginalLocation(Widget widget) {
                original = widget.getPreferredLocation();
                return original;
            }
    
            public void setNewLocation(Widget widget, Point location) {
                widget.setPreferredLocation(location);
                //Set the X and Y of the Task object:
                task.setX(location.x);
                task.setY(location.y);
                serialize();
            }
    
            public Point locationSuggested(Widget widget, Point originalPoint, Point suggested) {
                return suggested;
            }
        }
    
        private class LabelTextFieldEditor implements TextFieldInplaceEditor {
    
            private final Task task;
    
            private LabelTextFieldEditor(Task task) {
                this.task = task;
            }
    
            public boolean isEnabled(Widget widget) {
                return true;
            }
    
            public String getText(Widget widget) {
                return ((LabelWidget) widget).getLabel();
            }
    
            public void setText(Widget widget, String text) {
                ((LabelWidget) widget).setLabel(text);
                //Set the Name of the Task object:
                task.setName(text);
                serialize();
            }
        }
    
        private class SelectProviderImpl implements SelectProvider {
    
            public boolean isAimingAllowed(Widget widget, Point localLocation, boolean invertSelection) {
                return false;
            }
    
            public boolean isSelectionAllowed(Widget widget, Point localLocation, boolean invertSelection) {
                return true;
            }
    
            public void select(Widget widget, Point localLocation, boolean invertSelection) {
                try {
                    ObjectScene scene = ((ObjectScene) widget.getScene());
                    Object object = scene.findObject(widget);
                    scene.setFocusedObject(object);
                    if (object != null) {
                        if (!invertSelection && scene.getSelectedObjects().contains(object)) {
                            return;
                        }
                        scene.userSelectionSuggested(Collections.singleton(object), invertSelection);
                    } else {
                        scene.userSelectionSuggested(Collections.emptySet(), invertSelection);
                    }
                    if (object instanceof Node) {
                        Set selected = scene.getSelectedObjects();
                        ArrayList selectedNodes = new ArrayList();
                        for (Object object1 : selected) {
                            if (object1 instanceof Node) {
                                selectedNodes.add((Node) object1);
                            }
                        }
                        em.setSelectedNodes((Node[]) selectedNodes.toArray(new Node[0]));
                    }
                } catch (PropertyVetoException ex) {
                    Exceptions.printStackTrace(ex);
                }
            }
        }
        
    }

  5. Serialize the Scene. When "serialize()" is called above, reference is made to a "SceneSerializer". Here it is:
    public class SceneSerializer {
    
        private static final String SCENE_ELEMENT = "activity"; // NOI18N
        private static final String NODE_ELEMENT = "task"; // NOI18N
        private static final String NODE_NAME_ATTR = "name"; // NOI18N
        private static final String NODE_X_ATTR = "x"; // NOI18N
        private static final String NODE_Y_ATTR = "y"; // NOI18N
    
        public static void serialize(ObjectScene scene, File file) {
            Document document = XMLUtil.createDocument(SCENE_ELEMENT, null, null, null);
            Node sceneElement = document.getFirstChild();
            for (Object o : scene.getObjects()) {
                Element nodeElement = document.createElement(NODE_ELEMENT);
                AbstractNode taskNode = (AbstractNode) scene.findStoredObject(o);
                Task task = taskNode.getLookup().lookup(Task.class);
                setAttribute(document, nodeElement, NODE_NAME_ATTR, task.getName());
                setAttribute(document, nodeElement, NODE_X_ATTR, Integer.toString(task.getX()));
                setAttribute(document, nodeElement, NODE_Y_ATTR, Integer.toString(task.getY()));
                sceneElement.appendChild(nodeElement);
            }
            FileOutputStream fos = null;
            try {
                fos = new FileOutputStream(file);
                XMLUtil.write(document, fos, "UTF-8"); // NOI18N
            } catch (Exception e) {
                Exceptions.printStackTrace(e);
            } finally {
                try {
                    if (fos != null) {
                        fos.close();
                    }
                } catch (Exception e) {
                    Exceptions.printStackTrace(e);
                }
            }
        }
    
        private static void setAttribute(Document xml, Node node, String name, String value) {
            NamedNodeMap map = node.getAttributes();
            Attr attribute = xml.createAttribute(name);
            attribute.setValue(value);
            map.setNamedItem(attribute);
        }
        
    }

That's it. Mostly standard code that has been blogged about or used in various tutorials, but now collected together into a single scenario.

The next step is to set a DocumentListener on the file and then update the visual view whenever the user decides to tweak the XML file rather than use the widgets in the visual view. Also the "x" and the "y" would probably not be in the XML file, instead, you're more likely to have an attribute named something like "priority" which is set depending on the x/y value retrieved from the scene. That will also be handled in a next blog entry.

Comments:

Post a Comment:
  • HTML Syntax: NOT allowed
About

Geertjan Wielenga (@geertjanw) is a Principal Product Manager in the Oracle Developer Tools group living & working in Amsterdam. He is a Java technology enthusiast, evangelist, trainer, speaker, and writer. He blogs here daily.

The focus of this blog is mostly on NetBeans (a development tool primarily for Java programmers), with an occasional reference to NetBeans, and sometimes diverging to topics relating to NetBeans. And then there are days when NetBeans is mentioned, just for a change.

Search

Archives
« April 2014
SunMonTueWedThuFriSat
  
12
13
14
17
18
19
20
21
22
23
24
25
26
27
28
29
30
   
       
Today