source: trunk/quest-core-events/src/main/java/de/ugoe/cs/quest/eventcore/guimodel/GUIModel.java @ 846

Last change on this file since 846 was 846, checked in by sherbold, 12 years ago
  • added interface to the GUIModel to merge nodes from the outside
  • added functionality to the SWT GUI to merge nodes of the GUI model manually
File size: 20.5 KB
RevLine 
[820]1
[545]2package de.ugoe.cs.quest.eventcore.guimodel;
3
[643]4import java.io.OutputStream;
5import java.io.PrintStream;
[746]6import java.io.UnsupportedEncodingException;
[545]7import java.util.ArrayList;
[603]8import java.util.LinkedList;
[545]9import java.util.List;
[821]10import java.util.logging.Level;
[545]11
[821]12import de.ugoe.cs.util.console.Console;
13
[545]14/**
[576]15 * <p>
[746]16 * A GUI model is a tree of {@link IGUIElements} and represents a complete GUI of a software. It is
17 * platform independent. It may have several root nodes, as some GUIs are made up of several Frames
18 * being independent from each other. The GUI model is filled using the
[820]19 * {@link #integratePath(List, IGUIElementFactory)} method.
[576]20 * </p>
[545]21 *
[821]22 * @version 1.0
[820]23 * @author Patrick Harms, Steffen Herbold
[545]24 */
[576]25public class GUIModel {
[820]26
[545]27    /**
[746]28     * <p>
[831]29     * The root node of the tree not provided externally.
[746]30     * </p>
[545]31     */
[576]32    private TreeNode root = new TreeNode();
[820]33
[545]34    /**
[746]35     * <p>
[831]36     * A list with all nodes currently known
[746]37     * </p>
[545]38     */
[576]39    private List<TreeNode> allNodes = new ArrayList<TreeNode>();
[545]40
41    /**
[746]42     * <p>
43     * Integrates a path of GUI elements into the GUI model. The GUI model itself is a tree and
44     * therefore a set of different paths through the tree that start with a root node and end with
45     * a leaf node. Such a path can be added to the tree. The method checks, if any of the GUI
46     * elements denoted by the path already exists. If so, it reuses it. It may therefore also
[820]47     * return an existing GUI element being the leaf node of the provided path. If a GUI element of
48     * the path does not exist yet, it creates a new one using the provided GUI element factory.
[746]49     * </p>
50     * <p>
[820]51     * If a GUI element specification describes an existing GUI element or not is determined through
52     * comparing the GUI element specifications of the existing GUI elements with the ones provided
53     * in the path. The comparison is done using the
54     * {@link IGUIElementSpec#getSimilarity(IGUIElementSpec)} method. The comparison is only done on
55     * the correct levels. I.e. the currently known root elements of the tree are only compared to
56     * the first element in the path. If the correct one is found or created, its children are
[746]57     * compared only to the second specification in the path, and so on.
58     * </p>
59     * <p>
60     * The returned GUI elements are singletons. I.e. it is tried to return always the identical
[820]61     * object for the same denoted element. However, while creating the GUI model, the similarity of
62     * GUI elements may change. Therefore, the method might determine, that two formerly different
63     * nodes are now similar. (This may happen, e.g. if GUI elements do not have initial names which
64     * are set afterwards. Therefore, first they are handled differently and later they can be
65     * identified as being the same.) In such a case, there are already several GUI element objects
66     * instantiated for the same GUI element. The singleton paradigm gets broken. Therefore, such
67     * GUI element objects are registered with each other, so that their equal method can determine
68     * equality again correctly, although the objects are no singletons anymore.
[746]69     * </p>
[820]70     *
71     * @param guiElementPath
72     *            the path to integrate into the model
73     * @param guiElementFactory
74     *            the GUI element factory to be used for instantiating GUI element objects
75     *
[746]76     * @return The GUI element object representing the GUI element denoted by the provided path
77     *
[820]78     * @throws GUIModelException
79     *             thrown in cases such as the GUI element object could not be instantiated
80     * @throws IllegalArgumentException
81     *             if the provided path is invalid.
[545]82     */
[576]83    public IGUIElement integratePath(List<? extends IGUIElementSpec> guiElementPath,
[820]84                                     IGUIElementFactory guiElementFactory)
[746]85        throws GUIModelException, IllegalArgumentException
[576]86    {
[611]87        if ((guiElementPath == null) || (guiElementPath.size() <= 0)) {
[820]88            throw new IllegalArgumentException("GUI element path must contain at least one element");
[611]89        }
[820]90
[603]91        List<IGUIElementSpec> remainingPath = new LinkedList<IGUIElementSpec>();
[820]92
93        for (IGUIElementSpec spec : guiElementPath) {
[576]94            remainingPath.add(spec);
[545]95        }
[820]96
[576]97        return integratePath(root, remainingPath, guiElementFactory);
[545]98    }
99
100    /**
[746]101     * <p>
[820]102     * Returns all children of the provided GUI element or null, if it does not have any or the node
103     * is unknown.
[746]104     * </p>
105     *
[820]106     * @param guiElement
107     *            the GUI element of which the children shall be returned
108     *
[746]109     * @return As described
[545]110     */
[576]111    public List<IGUIElement> getChildren(IGUIElement guiElement) {
[821]112        List<IGUIElement> result = null;
[576]113        for (TreeNode node : allNodes) {
114            if (node.guiElement.equals(guiElement)) {
[821]115                if (result == null) {
116                    result = new ArrayList<IGUIElement>();
[820]117
[821]118                    if (node.children != null) {
119                        for (TreeNode child : node.children) {
120                            result.add(child.guiElement);
121                        }
[576]122                    }
123                }
[821]124                else {
125                    Console
126                        .traceln(Level.SEVERE,
127                                 "Multiple nodes in the internal GUI model match the same GUI element. "
128                                     + "This should not be the case and the GUI model is probably invalid.");
129                }
[576]130            }
131        }
[820]132
[821]133        return result;
[545]134    }
135
136    /**
[593]137     * <p>
[746]138     * Returns the parent GUI element of the provided GUI element or null, if it does not have a
139     * parent (i.e. if it is a root node) or if the node is unknown.
[593]140     * </p>
[746]141     *
[820]142     * @param guiElement
143     *            the GUI element of which the parent shall be returned
144     *
[746]145     * @return As described
[545]146     */
[593]147    public IGUIElement getParent(IGUIElement guiElement) {
[821]148        IGUIElement parent = null;
149
[593]150        for (TreeNode node : allNodes) {
151            for (TreeNode child : node.children) {
152                if (child.guiElement.equals(guiElement)) {
[821]153                    if (parent != null) {
154                        parent = node.guiElement;
155                    }
156                    else {
157                        Console
158                            .traceln(Level.SEVERE,
159                                     "Multiple nodes in the internal GUI model match the same GUI element. "
160                                         + "This should not be the case and the GUI model is probably invalid.");
161                    }
[593]162                }
163            }
164        }
[820]165
[821]166        return parent;
[593]167    }
168
169    /**
[746]170     * <p>
171     * Returns all root GUI elements of the model or an empty list, if the model is empty
172     * </p>
[820]173     *
[746]174     * @return As described
[593]175     */
[576]176    public List<IGUIElement> getRootElements() {
177        List<IGUIElement> roots = new ArrayList<IGUIElement>();
[820]178
[611]179        if (root.children != null) {
180            for (TreeNode rootChild : root.children) {
181                roots.add(rootChild.guiElement);
182            }
[545]183        }
[820]184
[545]185        return roots;
186    }
187
188    /**
[746]189     * <p>
190     * dumps the GUI model to the provided stream. Each node is represented through its toString()
191     * method. If a node has children, those are dumped indented and surrounded by braces.
192     * </p>
193     *
[820]194     * @param out
195     *            The stream to dump the textual representation of the model to
196     * @param encoding
197     *            The encoding to be used while dumping
[643]198     */
[746]199    public void dump(OutputStream out, String encoding) {
[643]200        PrintStream stream;
[820]201
[643]202        if (out instanceof PrintStream) {
203            stream = (PrintStream) out;
204        }
205        else {
[746]206            String enc = encoding == null ? "UTF-8" : encoding;
207            try {
208                stream = new PrintStream(out, true, enc);
209            }
210            catch (UnsupportedEncodingException e) {
[747]211                throw new IllegalArgumentException("encodind " + enc + " not supported");
[746]212            }
[643]213        }
[820]214
[643]215        for (IGUIElement root : getRootElements()) {
216            dumpGUIElement(stream, root, "");
217        }
218    }
219
220    /**
[576]221     * <p>
[821]222     * By calling this method, the GUIModel is traversed and similar nodes are merged.
[576]223     * </p>
[820]224     *
225     */
226    public void condenseModel() {
[821]227        mergeSubTree(root);
[820]228    }
[846]229   
230    /**
231     * <p>
232     * Merges the tree nodes of two GUI elements. The GUI elements need to have the same parent.
233     * </p>
234     *
235     * @param guiElement1
236     *            the first merge GUI element
237     * @param guiElement2
238     *            the second merge GUI element
239     * @throws IllegalArgumentException
240     *             thrown if the two GUI elements do not have the same parent
241     */
242    public void mergeGUIElements(IGUIElement guiElement1, IGUIElement guiElement2)
243        throws IllegalArgumentException
244    {
245        // check if both nodes have the same parent
246        IGUIElement parentElement = guiElement1.getParent();
247        if (parentElement != null && !parentElement.equals(guiElement2.getParent())) {
248            throw new IllegalArgumentException("can only merge nodes with the same parent");
249        }
[820]250
[846]251        // get the TreeNode of the parent of the GUI elements
252        TreeNode parent = findNode(parentElement);
253
254        // get the TreeNodes for both GUI elements
255        TreeNode node1 = findNode(guiElement1);
256        TreeNode node2 = findNode(guiElement2);
257
258        if (node1 == null || node2 == null) {
259            throw new IllegalArgumentException(
260                                               "Error while merging nodes: one element is not part of the GUI model!");
261        }
262
263        TreeNode replacement = mergeTreeNodes(node1, node2);
264
265        if (parent != null) {
266            // remove node1 and node2 from the parent's children and add the replacement instead
267            // assumes that there are no duplicates of node1 and node2
268            if (parent.children != null) {
269                parent.children.set(parent.children.indexOf(node1), replacement);
270                parent.children.remove(node2);
271            }
272        }
273
274    }
275
[820]276    /**
277     * <p>
278     * internally integrates a path as the children of the provided parent node. This method is
279     * recursive and calls itself, for the child of the parent node, that matches the first element
280     * in the remaining path.
281     * </p>
282     *
283     * @param parentNode
284     *            the parent node to add children for
285     * @param guiElementPath
286     *            the path of children to be created starting with the parent node
287     * @param guiElementFactory
288     *            the GUI element factory to be used for instantiating GUI element objects
289     *
[746]290     * @return The GUI element object representing the GUI element denoted by the provided path
291     *
[820]292     * @throws GUIModelException
293     *             thrown in cases such as the GUI element object could not be instantiated
[545]294     */
[820]295    private IGUIElement integratePath(TreeNode parentNode,
[576]296                                      List<? extends IGUIElementSpec> remainingPath,
[820]297                                      IGUIElementFactory guiElementFactory)
[576]298        throws GUIModelException
299    {
300        IGUIElementSpec specToIntegrateElementFor = remainingPath.remove(0);
[611]301
[820]302        TreeNode child = findEqualChild(parentNode, specToIntegrateElementFor);
[821]303        if (child == null) {
[820]304            IGUIElement newElement =
305                guiElementFactory.instantiateGUIElement(specToIntegrateElementFor,
306                                                        parentNode.guiElement);
[611]307
[820]308            child = parentNode.addChild(newElement);
[545]309        }
[820]310
[576]311        if (remainingPath.size() > 0) {
[820]312            return integratePath(child, remainingPath, guiElementFactory);
[576]313        }
314        else {
[820]315            return child.guiElement;
[576]316        }
[545]317    }
318
[576]319    /**
320     * <p>
[821]321     * Searches the children of a tree node to see if the {@link IGUIElementSpec} of equals the
322     * specification of the {@link TreeNode#guiElement} of the child. If a match is found, the child
323     * is returned.
[611]324     * </p>
[831]325     *
326     * @param parentNode
327     *            parent node whose children are searched
328     * @param specToMatch
329     *            specification that is searched for
330     * @return matching child node or null if no child matches
[611]331     */
[820]332    private TreeNode findEqualChild(TreeNode parentNode, IGUIElementSpec specToMatch) {
[611]333        if (parentNode.children != null) {
334            for (TreeNode child : parentNode.children) {
[820]335                if (specToMatch.equals(child.guiElement.getSpecification())) {
336                    return child;
[611]337                }
338            }
339        }
[820]340        return null;
[611]341    }
342
343    /**
344     * <p>
[821]345     * Merges all similar nodes in the sub-tree of the GUI model defined by the subTreeRoot.
[611]346     * </p>
[821]347     * <p>
348     * The merging order is a bottom-up. This means, that we first call mergeSubTree recursively for
349     * the grand children of the subTreeRoot, before we merge subTreeRoot.
350     * </p>
351     * <p>
352     * The merging strategy is top-down. This means, that every time we merge two child nodes, we
353     * call mergeSubTree recursively for all children of the merged nodes in order to check if we
354     * can merge the children, too.
355     * </p>
[831]356     *
357     * @param subTreeRoot
358     *            root node of the sub-tree that is merged
[611]359     */
[821]360    private void mergeSubTree(TreeNode subTreeRoot) {
361        if (subTreeRoot.children == null || subTreeRoot.children.isEmpty()) {
[820]362            return;
[611]363        }
[820]364
[821]365        // lets first merge the grand children of the parentNode
366        for (TreeNode child : subTreeRoot.children) {
367            mergeSubTree(child);
[611]368        }
369
370        boolean performedMerge;
[820]371
[611]372        do {
373            performedMerge = false;
[821]374            for (int i = 0; !performedMerge && i < subTreeRoot.children.size(); i++) {
[820]375                IGUIElementSpec elemSpec1 =
[821]376                    subTreeRoot.children.get(i).guiElement.getSpecification();
377                for (int j = i + 1; !performedMerge && j < subTreeRoot.children.size(); j++) {
[820]378                    IGUIElementSpec elemSpec2 =
[821]379                        subTreeRoot.children.get(j).guiElement.getSpecification();
[820]380                    if (elemSpec1.getSimilarity(elemSpec2)) {
381                        TreeNode replacement =
[821]382                            mergeTreeNodes(subTreeRoot.children.get(i), subTreeRoot.children.get(j));
[820]383
[821]384                        subTreeRoot.children.set(i, replacement);
385                        subTreeRoot.children.remove(j);
[820]386                        performedMerge = true;
387                        i--;
388                        break;
[611]389                    }
390                }
391            }
392        }
393        while (performedMerge);
394    }
395
396    /**
397     * <p>
[820]398     * merges two nodes with each other. Merging means registering the GUI element objects with each
399     * other for equality checks. Further it add all children of both nodes to a new replacing node.
400     * Afterwards, all similar nodes of the replacement node are merged as well.
[611]401     * </p>
[820]402     *
[831]403     * @param treeNode1
404     *            the first of the two nodes to be merged
405     * @param treeNode2
406     *            the second of the two nodes to be merged
[746]407     * @return a tree node being the merge of the two provided nodes.
[611]408     */
409    private TreeNode mergeTreeNodes(TreeNode treeNode1, TreeNode treeNode2) {
410        // the following two lines are needed to preserve the references to the existing GUI
411        // elements. If two elements are the same, one should be deleted to make the elements
412        // singletons again. However, there may exist references to both objects. To preserve
413        // these, we simply register the equal GUI elements with each other so that an equals
414        // check can return true.
[820]415        treeNode1.guiElement.addEqualGUIElement(treeNode2.guiElement);
416        treeNode2.guiElement.addEqualGUIElement(treeNode1.guiElement);
417
418        // and now a replacement node that is the merge of treeNode1 and treeNode2 is created
419        TreeNode replacement = new TreeNode();
420        replacement.guiElement = treeNode1.guiElement;
[611]421        if (treeNode1.children != null) {
422            for (TreeNode child : treeNode1.children) {
[820]423                replacement.addChildNode(child);
[611]424            }
425        }
426        if (treeNode2.children != null) {
427            for (TreeNode child : treeNode2.children) {
[820]428                replacement.addChildNode(child);
[611]429            }
430        }
[820]431
[821]432        mergeSubTree(replacement);
[820]433
[611]434        replacement.guiElement.updateSpecification(treeNode2.guiElement.getSpecification());
[820]435
436        // finally, update the known nodes list
437        // if you don't do this getChildren will return wrong things and very bad things happen!
438        allNodes.remove(treeNode1);
439        allNodes.remove(treeNode2);
440        allNodes.add(replacement);
441
[611]442        return replacement;
443    }
444
445    /**
[746]446     * <p>
[820]447     * dumps a GUI element to the stream. A dump contains the toString() representation of the GUI
448     * element as well as a indented list of its children surrounded by braces.
[746]449     * </p>
[831]450     *
451     * @param out
452     *            {@link PrintStream} where the guiElement is dumped to
453     * @param guiElement
454     *            the guiElement whos string represenation is dumped
455     * @param indent
456     *            indent string of the dumping
[643]457     */
458    private void dumpGUIElement(PrintStream out, IGUIElement guiElement, String indent) {
459        out.print(indent);
460        out.print(guiElement);
461
462        List<IGUIElement> children = getChildren(guiElement);
463
464        if ((children != null) && (children.size() > 0)) {
465            out.println(" {");
466
467            for (IGUIElement child : children) {
468                dumpGUIElement(out, child, indent + "  ");
469            }
470
471            out.print(indent);
472            out.print("}");
473        }
474
475        out.println();
476    }
[846]477   
478    /**
479     * <p>
480     * Retrieves the TreeNode associated with a GUI element. Returns null if no such TreeNode is
481     * found.
482     * </p>
483     *
484     * @param element
485     *            the GUI element
486     * @return associated TreeNode; null if no such node exists
487     */
488    private TreeNode findNode(IGUIElement element) {
489        if (element == null) {
490            return null;
491        }
[643]492
[846]493        TreeNode result = null;
494        for (TreeNode node : allNodes) {
495            if (node.guiElement.equals(element)) {
496                if (result == null) {
497                    result = node;
498                }
499                else {
500                    Console
501                        .traceln(Level.SEVERE,
502                                 "Multiple nodes in the internal GUI model match the same GUI element. "
503                                     + "This should not be the case and the GUI model is probably invalid.");
504                }
505            }
506        }
507        return result;
508    }
509
[643]510    /**
[611]511     * <p>
[831]512     * Used internally for building up the tree of GUI elements.
[576]513     * </p>
[831]514     *
515     * @version 1.0
516     * @author Patrick Harms, Steffen Herbold
[576]517     */
[820]518    private class TreeNode {
[831]519
520        /**
521         * <p>
522         * GUI element associated with the TreeNode.
523         * </p>
524         */
[576]525        private IGUIElement guiElement;
[820]526
[831]527        /**
528         * <p>
529         * Children of the TreeNode.
530         * </p>
531         */
[576]532        private List<TreeNode> children;
[820]533
[576]534        /**
535         * <p>
[831]536         * Adds a child to the current node while keeping all lists of nodes up to date
[576]537         * </p>
[831]538         *
539         * @param guiElement
540         *            GUI element that will be associated with the new child
541         * @return the added child
[576]542         */
[820]543        private TreeNode addChild(IGUIElement guiElement) {
544            if (children == null) {
[576]545                children = new ArrayList<TreeNode>();
546            }
[820]547
[576]548            TreeNode child = new TreeNode();
549            child.guiElement = guiElement;
550            children.add(child);
[820]551
[576]552            allNodes.add(child);
[820]553
[576]554            return child;
555        }
[709]556
[820]557        /**
558         *
559         * <p>
[821]560         * Adds a TreeNode as child to the current node. This way, the whole sub-tree is added.
[820]561         * </p>
[831]562         *
563         * @param node
564         *            child node that is added
565         * @return node that has been added
[820]566         */
567        private TreeNode addChildNode(TreeNode node) {
568            if (children == null) {
569                children = new ArrayList<TreeNode>();
570            }
571            children.add(node);
572            return node;
573        }
574
575        /*
576         * (non-Javadoc)
577         *
[709]578         * @see java.lang.Object#toString()
579         */
580        @Override
581        public String toString() {
582            return guiElement.toString();
583        }
[576]584    }
[545]585}
Note: See TracBrowser for help on using the repository browser.