1/*
2 * Copyright (c) 2008, 2015, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.
8 *
9 * This code is distributed in the hope that it will be useful, but WITHOUT
10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12 * version 2 for more details (a copy is included in the LICENSE file that
13 * accompanied this code).
14 *
15 * You should have received a copy of the GNU General Public License version
16 * 2 along with this work; if not, write to the Free Software Foundation,
17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18 *
19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20 * or visit www.oracle.com if you need additional information or have any
21 * questions.
22 *
23 */
24package com.sun.hotspot.igv.view;
25
26import com.sun.hotspot.igv.data.ChangedListener;
27import com.sun.hotspot.igv.data.ControllableChangedListener;
28import com.sun.hotspot.igv.data.InputBlock;
29import com.sun.hotspot.igv.data.InputNode;
30import com.sun.hotspot.igv.data.Pair;
31import com.sun.hotspot.igv.data.Properties;
32import com.sun.hotspot.igv.data.services.Scheduler;
33import com.sun.hotspot.igv.graph.*;
34import com.sun.hotspot.igv.hierarchicallayout.HierarchicalClusterLayoutManager;
35import com.sun.hotspot.igv.hierarchicallayout.HierarchicalLayoutManager;
36import com.sun.hotspot.igv.layout.LayoutGraph;
37import com.sun.hotspot.igv.selectioncoordinator.SelectionCoordinator;
38import com.sun.hotspot.igv.util.ColorIcon;
39import com.sun.hotspot.igv.util.DoubleClickAction;
40import com.sun.hotspot.igv.util.PropertiesSheet;
41import com.sun.hotspot.igv.view.actions.CustomizablePanAction;
42import com.sun.hotspot.igv.view.widgets.*;
43import java.awt.*;
44import java.awt.event.*;
45import java.util.List;
46import java.util.*;
47import javax.swing.*;
48import javax.swing.event.UndoableEditEvent;
49import javax.swing.undo.AbstractUndoableEdit;
50import javax.swing.undo.CannotRedoException;
51import javax.swing.undo.CannotUndoException;
52import org.netbeans.api.visual.action.*;
53import org.netbeans.api.visual.animator.SceneAnimator;
54import org.netbeans.api.visual.layout.LayoutFactory;
55import org.netbeans.api.visual.model.*;
56import org.netbeans.api.visual.widget.LayerWidget;
57import org.netbeans.api.visual.widget.Widget;
58import org.openide.awt.UndoRedo;
59import org.openide.nodes.AbstractNode;
60import org.openide.nodes.Children;
61import org.openide.nodes.Sheet;
62import org.openide.util.Lookup;
63import org.openide.util.lookup.AbstractLookup;
64import org.openide.util.lookup.InstanceContent;
65
66/**
67 *
68 * @author Thomas Wuerthinger
69 */
70public class DiagramScene extends ObjectScene implements DiagramViewer {
71
72    private CustomizablePanAction panAction;
73    private WidgetAction hoverAction;
74    private WidgetAction selectAction;
75    private Lookup lookup;
76    private InstanceContent content;
77    private Action[] actions;
78    private Action[] actionsWithSelection;
79    private LayerWidget connectionLayer;
80    private JScrollPane scrollPane;
81    private UndoRedo.Manager undoRedoManager;
82    private LayerWidget mainLayer;
83    private LayerWidget blockLayer;
84    private Widget topLeft;
85    private Widget bottomRight;
86    private DiagramViewModel model;
87    private DiagramViewModel modelCopy;
88    private WidgetAction zoomAction;
89    private boolean rebuilding;
90
91    /**
92     * The alpha level of partially visible figures.
93     */
94    public static final float ALPHA = 0.4f;
95
96    /**
97     * The offset of the graph to the border of the window showing it.
98     */
99    public static final int BORDER_SIZE = 20;
100
101
102    public static final int UNDOREDO_LIMIT = 100;
103    public static final int SCROLL_UNIT_INCREMENT = 80;
104    public static final int SCROLL_BLOCK_INCREMENT = 400;
105    public static final float ZOOM_MAX_FACTOR = 3.0f;
106    public static final float ZOOM_MIN_FACTOR = 0.0f;//0.15f;
107    public static final float ZOOM_INCREMENT = 1.5f;
108    public static final int SLOT_OFFSET = 8;
109    public static final int ANIMATION_LIMIT = 40;
110
111    private PopupMenuProvider popupMenuProvider = new PopupMenuProvider() {
112
113        @Override
114        public JPopupMenu getPopupMenu(Widget widget, Point localLocation) {
115            return DiagramScene.this.createPopupMenu();
116        }
117    };
118
119    private RectangularSelectDecorator rectangularSelectDecorator = new RectangularSelectDecorator() {
120
121        @Override
122        public Widget createSelectionWidget() {
123            Widget widget = new Widget(DiagramScene.this);
124            widget.setBorder(BorderFactory.createLineBorder(Color.black, 2));
125            widget.setForeground(Color.red);
126            return widget;
127        }
128    };
129
130    @SuppressWarnings("unchecked")
131    public <T> T getWidget(Object o) {
132        Widget w = this.findWidget(o);
133        return (T) w;
134    }
135
136    @SuppressWarnings("unchecked")
137    public <T> T getWidget(Object o, Class<T> klass) {
138        Widget w = this.findWidget(o);
139        return (T) w;
140    }
141
142    private static boolean intersects(Set<? extends Object> s1, Set<? extends Object> s2) {
143        for (Object o : s1) {
144            if (s2.contains(o)) {
145                return true;
146            }
147        }
148        return false;
149    }
150
151    @Override
152    public void zoomOut() {
153        double zoom = getZoomFactor();
154        Point viewPosition = getScrollPane().getViewport().getViewPosition();
155        double newZoom = zoom / DiagramScene.ZOOM_INCREMENT;
156        if (newZoom > DiagramScene.ZOOM_MIN_FACTOR) {
157            setZoomFactor(newZoom);
158            validate();
159            getScrollPane().getViewport().setViewPosition(new Point((int) (viewPosition.x / DiagramScene.ZOOM_INCREMENT), (int) (viewPosition.y / DiagramScene.ZOOM_INCREMENT)));
160        }
161    }
162
163    @Override
164    public void zoomIn() {
165
166        double zoom = getZoomFactor();
167        Point viewPosition = getScrollPane().getViewport().getViewPosition();
168        double newZoom = zoom * DiagramScene.ZOOM_INCREMENT;
169        if (newZoom < DiagramScene.ZOOM_MAX_FACTOR) {
170            setZoomFactor(newZoom);
171            validate();
172            getScrollPane().getViewport().setViewPosition(new Point((int) (viewPosition.x * DiagramScene.ZOOM_INCREMENT), (int) (viewPosition.y * DiagramScene.ZOOM_INCREMENT)));
173        }
174    }
175
176
177    @Override
178    public void centerFigures(List<Figure> list) {
179
180        boolean b = getUndoRedoEnabled();
181        setUndoRedoEnabled(false);
182        gotoFigures(list);
183        setUndoRedoEnabled(b);
184    }
185
186    private Set<Object> getObjectsFromIdSet(Set<Object> set) {
187        Set<Object> selectedObjects = new HashSet<>();
188        for (Figure f : getModel().getDiagramToView().getFigures()) {
189            if (intersects(f.getSource().getSourceNodesAsSet(), set)) {
190                selectedObjects.add(f);
191            }
192
193            for (Slot s : f.getSlots()) {
194                if (intersects(s.getSource().getSourceNodesAsSet(), set)) {
195                    selectedObjects.add(s);
196                }
197            }
198        }
199        return selectedObjects;
200    }
201    private ControllableChangedListener<SelectionCoordinator> highlightedCoordinatorListener = new ControllableChangedListener<SelectionCoordinator>() {
202
203        @Override
204        public void filteredChanged(SelectionCoordinator source) {
205            DiagramScene.this.setHighlightedObjects(getObjectsFromIdSet(source.getHighlightedObjects()));
206            DiagramScene.this.validate();
207        }
208    };
209    private ControllableChangedListener<SelectionCoordinator> selectedCoordinatorListener = new ControllableChangedListener<SelectionCoordinator>() {
210
211        @Override
212        public void filteredChanged(SelectionCoordinator source) {
213            DiagramScene.this.gotoSelection(source.getSelectedObjects());
214            DiagramScene.this.validate();
215        }
216    };
217
218    private RectangularSelectProvider rectangularSelectProvider = new RectangularSelectProvider() {
219
220        @Override
221        public void performSelection(Rectangle rectangle) {
222            if (rectangle.width < 0) {
223                rectangle.x += rectangle.width;
224                rectangle.width *= -1;
225            }
226
227            if (rectangle.height < 0) {
228                rectangle.y += rectangle.height;
229                rectangle.height *= -1;
230            }
231
232            Set<Object> selectedObjects = new HashSet<>();
233            for (Figure f : getModel().getDiagramToView().getFigures()) {
234                FigureWidget w = getWidget(f);
235                if (w != null) {
236                    Rectangle r = new Rectangle(w.getBounds());
237                    r.setLocation(w.getLocation());
238
239                    if (r.intersects(rectangle)) {
240                        selectedObjects.add(f);
241                    }
242
243                    for (Slot s : f.getSlots()) {
244                        SlotWidget sw = getWidget(s);
245                        Rectangle r2 = new Rectangle(sw.getBounds());
246                        r2.setLocation(sw.convertLocalToScene(new Point(0, 0)));
247
248                        if (r2.intersects(rectangle)) {
249                            selectedObjects.add(s);
250                        }
251                    }
252                } else {
253                    assert false : "w should not be null here!";
254                }
255            }
256
257            setSelectedObjects(selectedObjects);
258        }
259    };
260
261    private MouseWheelListener mouseWheelListener = new MouseWheelListener() {
262
263        @Override
264        public void mouseWheelMoved(MouseWheelEvent e) {
265            if (e.isControlDown()) {
266                DiagramScene.this.relayoutWithoutLayout(null);
267            }
268        }
269    };
270
271    public Point getScrollPosition() {
272        return getScrollPane().getViewport().getViewPosition();
273    }
274
275    public void setScrollPosition(Point p) {
276        getScrollPane().getViewport().setViewPosition(p);
277    }
278
279    private JScrollPane createScrollPane() {
280        JComponent comp = this.createView();
281        comp.setDoubleBuffered(true);
282        comp.setBackground(Color.WHITE);
283        comp.setOpaque(true);
284        this.setBackground(Color.WHITE);
285        this.setOpaque(true);
286        JScrollPane result = new JScrollPane(comp);
287        result.setBackground(Color.WHITE);
288        result.getVerticalScrollBar().setUnitIncrement(SCROLL_UNIT_INCREMENT);
289        result.getVerticalScrollBar().setBlockIncrement(SCROLL_BLOCK_INCREMENT);
290        result.getHorizontalScrollBar().setUnitIncrement(SCROLL_UNIT_INCREMENT);
291        result.getHorizontalScrollBar().setBlockIncrement(SCROLL_BLOCK_INCREMENT);
292        return result;
293    }
294    private ObjectSceneListener selectionChangedListener = new ObjectSceneListener() {
295
296        @Override
297        public void objectAdded(ObjectSceneEvent arg0, Object arg1) {
298        }
299
300        @Override
301        public void objectRemoved(ObjectSceneEvent arg0, Object arg1) {
302        }
303
304        @Override
305        public void objectStateChanged(ObjectSceneEvent e, Object o, ObjectState oldState, ObjectState newState) {
306        }
307
308        @Override
309        public void selectionChanged(ObjectSceneEvent e, Set<Object> oldSet, Set<Object> newSet) {
310            DiagramScene scene = (DiagramScene) e.getObjectScene();
311            if (scene.isRebuilding()) {
312                return;
313            }
314
315            content.set(newSet, null);
316
317            Set<Integer> nodeSelection = new HashSet<>();
318            for (Object o : newSet) {
319                if (o instanceof Properties.Provider) {
320                    final Properties.Provider provider = (Properties.Provider) o;
321                    AbstractNode node = new AbstractNode(Children.LEAF) {
322
323                        @Override
324                        protected Sheet createSheet() {
325                            Sheet s = super.createSheet();
326                            PropertiesSheet.initializeSheet(provider.getProperties(), s);
327                            return s;
328                        }
329                    };
330                    node.setDisplayName(provider.getProperties().get("name"));
331                    content.add(node);
332                }
333
334
335                if (o instanceof Figure) {
336                    nodeSelection.addAll(((Figure) o).getSource().getSourceNodesAsSet());
337                } else if (o instanceof Slot) {
338                    nodeSelection.addAll(((Slot) o).getSource().getSourceNodesAsSet());
339                }
340            }
341            getModel().setSelectedNodes(nodeSelection);
342
343            boolean b = selectedCoordinatorListener.isEnabled();
344            selectedCoordinatorListener.setEnabled(false);
345            SelectionCoordinator.getInstance().setSelectedObjects(nodeSelection);
346            selectedCoordinatorListener.setEnabled(b);
347
348        }
349
350        @Override
351        public void highlightingChanged(ObjectSceneEvent e, Set<Object> oldSet, Set<Object> newSet) {
352            Set<Integer> nodeHighlighting = new HashSet<>();
353            for (Object o : newSet) {
354                if (o instanceof Figure) {
355                    nodeHighlighting.addAll(((Figure) o).getSource().getSourceNodesAsSet());
356                } else if (o instanceof Slot) {
357                    nodeHighlighting.addAll(((Slot) o).getSource().getSourceNodesAsSet());
358                }
359            }
360            boolean b = highlightedCoordinatorListener.isEnabled();
361            highlightedCoordinatorListener.setEnabled(false);
362            SelectionCoordinator.getInstance().setHighlightedObjects(nodeHighlighting);
363            highlightedCoordinatorListener.setEnabled(true);
364        }
365
366        @Override
367        public void hoverChanged(ObjectSceneEvent e, Object oldObject, Object newObject) {
368            Set<Object> newHighlightedObjects = new HashSet<>(DiagramScene.this.getHighlightedObjects());
369            if (oldObject != null) {
370                newHighlightedObjects.remove(oldObject);
371            }
372            if (newObject != null) {
373                newHighlightedObjects.add(newObject);
374            }
375            DiagramScene.this.setHighlightedObjects(newHighlightedObjects);
376        }
377
378        @Override
379        public void focusChanged(ObjectSceneEvent arg0, Object arg1, Object arg2) {
380        }
381    };
382
383    public DiagramScene(Action[] actions, Action[] actionsWithSelection, DiagramViewModel model) {
384
385        this.actions = actions;
386        this.actionsWithSelection = actionsWithSelection;
387
388        content = new InstanceContent();
389        lookup = new AbstractLookup(content);
390
391        this.setCheckClipping(true);
392
393        scrollPane = createScrollPane();
394
395        hoverAction = createObjectHoverAction();
396
397        // This panAction handles the event only when the left mouse button is
398        // pressed without any modifier keys, otherwise it will not consume it
399        // and the selection action (below) will handle the event
400        panAction = new CustomizablePanAction(~0, MouseEvent.BUTTON1_DOWN_MASK);
401        this.getActions().addAction(panAction);
402
403        selectAction = createSelectAction();
404        this.getActions().addAction(selectAction);
405
406        blockLayer = new LayerWidget(this);
407        this.addChild(blockLayer);
408
409        connectionLayer = new LayerWidget(this);
410        this.addChild(connectionLayer);
411
412        mainLayer = new LayerWidget(this);
413        this.addChild(mainLayer);
414
415        topLeft = new Widget(this);
416        topLeft.setPreferredLocation(new Point(-BORDER_SIZE, -BORDER_SIZE));
417        this.addChild(topLeft);
418
419        bottomRight = new Widget(this);
420        bottomRight.setPreferredLocation(new Point(-BORDER_SIZE, -BORDER_SIZE));
421        this.addChild(bottomRight);
422
423        LayerWidget selectionLayer = new LayerWidget(this);
424        this.addChild(selectionLayer);
425
426        this.setLayout(LayoutFactory.createAbsoluteLayout());
427
428        this.getInputBindings().setZoomActionModifiers(KeyEvent.CTRL_MASK);
429        zoomAction = ActionFactory.createMouseCenteredZoomAction(1.2);
430        this.getActions().addAction(zoomAction);
431        this.getView().addMouseWheelListener(mouseWheelListener);
432        this.getActions().addAction(ActionFactory.createPopupMenuAction(popupMenuProvider));
433
434        this.getActions().addAction(ActionFactory.createWheelPanAction());
435
436        LayerWidget selectLayer = new LayerWidget(this);
437        this.addChild(selectLayer);
438        this.getActions().addAction(ActionFactory.createRectangularSelectAction(rectangularSelectDecorator, selectLayer, rectangularSelectProvider));
439
440        boolean b = this.getUndoRedoEnabled();
441        this.setUndoRedoEnabled(false);
442        this.setNewModel(model);
443        this.setUndoRedoEnabled(b);
444        this.addObjectSceneListener(selectionChangedListener, ObjectSceneEventType.OBJECT_SELECTION_CHANGED, ObjectSceneEventType.OBJECT_HIGHLIGHTING_CHANGED, ObjectSceneEventType.OBJECT_HOVER_CHANGED);
445    }
446
447    public DiagramViewModel getModel() {
448        return model;
449    }
450
451    public JScrollPane getScrollPane() {
452        return scrollPane;
453    }
454
455    @Override
456    public Component getComponent() {
457        return scrollPane;
458    }
459
460    public boolean isAllVisible() {
461        return getModel().getHiddenNodes().isEmpty();
462    }
463
464    public Action createGotoAction(final Figure f) {
465        final DiagramScene diagramScene = this;
466        String name = f.getLines()[0];
467
468        name += " (";
469
470        if (f.getCluster() != null) {
471            name += "B" + f.getCluster().toString();
472        }
473        final boolean hidden = !this.getWidget(f, FigureWidget.class).isVisible();
474        if (hidden) {
475            if (f.getCluster() != null) {
476                name += ", ";
477            }
478            name += "hidden";
479        }
480        name += ")";
481        Action a = new AbstractAction(name, new ColorIcon(f.getColor())) {
482
483            @Override
484            public void actionPerformed(ActionEvent e) {
485                diagramScene.gotoFigure(f);
486            }
487        };
488
489        a.setEnabled(true);
490        return a;
491    }
492
493    public void setNewModel(DiagramViewModel model) {
494        assert this.model == null : "can set model only once!";
495        this.model = model;
496        this.modelCopy = null;
497
498        model.getDiagramChangedEvent().addListener(fullChange);
499        model.getViewPropertiesChangedEvent().addListener(fullChange);
500        model.getViewChangedEvent().addListener(selectionChange);
501        model.getHiddenNodesChangedEvent().addListener(hiddenNodesChange);
502        update();
503    }
504
505    private void update() {
506        mainLayer.removeChildren();
507        blockLayer.removeChildren();
508
509        rebuilding = true;
510
511        Collection<Object> objects = new ArrayList<>(this.getObjects());
512        for (Object o : objects) {
513            this.removeObject(o);
514        }
515
516        Diagram d = getModel().getDiagramToView();
517
518        if (d.getGraph().getBlocks().isEmpty()) {
519            Scheduler s = Lookup.getDefault().lookup(Scheduler.class);
520            d.getGraph().clearBlocks();
521            s.schedule(d.getGraph());
522            d.getGraph().ensureNodesInBlocks();
523            d.updateBlocks();
524        }
525
526        for (Figure f : d.getFigures()) {
527            FigureWidget w = new FigureWidget(f, hoverAction, selectAction, this, mainLayer);
528            w.getActions().addAction(ActionFactory.createPopupMenuAction(w));
529            w.getActions().addAction(selectAction);
530            w.getActions().addAction(hoverAction);
531            w.setVisible(false);
532
533            this.addObject(f, w);
534
535            for (InputSlot s : f.getInputSlots()) {
536                SlotWidget sw = new InputSlotWidget(s, this, w, w);
537                addObject(s, sw);
538                sw.getActions().addAction(new DoubleClickAction(sw));
539                sw.getActions().addAction(hoverAction);
540                sw.getActions().addAction(selectAction);
541            }
542
543            for (OutputSlot s : f.getOutputSlots()) {
544                SlotWidget sw = new OutputSlotWidget(s, this, w, w);
545                addObject(s, sw);
546                sw.getActions().addAction(new DoubleClickAction(sw));
547                sw.getActions().addAction(hoverAction);
548                sw.getActions().addAction(selectAction);
549            }
550        }
551
552        if (getModel().getShowBlocks()) {
553            for (InputBlock bn : d.getGraph().getBlocks()) {
554                BlockWidget w = new BlockWidget(this, d, bn);
555                w.setVisible(false);
556                this.addObject(bn, w);
557                blockLayer.addChild(w);
558            }
559        }
560
561        rebuilding = false;
562        this.smallUpdate(true);
563    }
564
565    public boolean isRebuilding() {
566        return rebuilding;
567    }
568
569    private void smallUpdate(boolean relayout) {
570        this.updateHiddenNodes(model.getHiddenNodes(), relayout);
571        boolean b = this.getUndoRedoEnabled();
572        this.setUndoRedoEnabled(false);
573        this.setUndoRedoEnabled(b);
574        this.validate();
575    }
576
577    private boolean isVisible(Connection c) {
578        FigureWidget w1 = getWidget(c.getInputSlot().getFigure());
579        FigureWidget w2 = getWidget(c.getOutputSlot().getFigure());
580
581        if (w1.isVisible() && w2.isVisible()) {
582            return true;
583        }
584
585        return false;
586    }
587
588    private void relayout(Set<Widget> oldVisibleWidgets) {
589        Diagram diagram = getModel().getDiagramToView();
590
591        HashSet<Figure> figures = new HashSet<>();
592
593        for (Figure f : diagram.getFigures()) {
594            FigureWidget w = getWidget(f);
595            if (w.isVisible()) {
596                figures.add(f);
597            }
598        }
599
600        HashSet<Connection> edges = new HashSet<>();
601
602        for (Connection c : diagram.getConnections()) {
603            if (isVisible(c)) {
604                edges.add(c);
605            }
606        }
607
608        if (getModel().getShowBlocks()) {
609            HierarchicalClusterLayoutManager m = new HierarchicalClusterLayoutManager(HierarchicalLayoutManager.Combine.SAME_OUTPUTS);
610            HierarchicalLayoutManager manager = new HierarchicalLayoutManager(HierarchicalLayoutManager.Combine.SAME_OUTPUTS);
611            manager.setMaxLayerLength(9);
612            manager.setMinLayerDifference(3);
613            m.setManager(manager);
614            m.setSubManager(new HierarchicalLayoutManager(HierarchicalLayoutManager.Combine.SAME_OUTPUTS));
615            m.doLayout(new LayoutGraph(edges, figures));
616        } else {
617            HierarchicalLayoutManager manager = new HierarchicalLayoutManager(HierarchicalLayoutManager.Combine.SAME_OUTPUTS);
618            manager.setMaxLayerLength(10);
619            manager.doLayout(new LayoutGraph(edges, figures));
620        }
621
622        relayoutWithoutLayout(oldVisibleWidgets);
623    }
624    private Set<Pair<Point, Point>> lineCache = new HashSet<>();
625
626    private void relayoutWithoutLayout(Set<Widget> oldVisibleWidgets) {
627
628        Diagram diagram = getModel().getDiagramToView();
629
630        int maxX = -BORDER_SIZE;
631        int maxY = -BORDER_SIZE;
632        for (Figure f : diagram.getFigures()) {
633            FigureWidget w = getWidget(f);
634            if (w.isVisible()) {
635                Point p = f.getPosition();
636                Dimension d = f.getSize();
637                maxX = Math.max(maxX, p.x + d.width);
638                maxY = Math.max(maxY, p.y + d.height);
639            }
640        }
641
642        for (Connection c : diagram.getConnections()) {
643            List<Point> points = c.getControlPoints();
644            FigureWidget w1 = getWidget((Figure) c.getTo().getVertex());
645            FigureWidget w2 = getWidget((Figure) c.getFrom().getVertex());
646            if (w1.isVisible() && w2.isVisible()) {
647                for (Point p : points) {
648                    if (p != null) {
649                        maxX = Math.max(maxX, p.x);
650                        maxY = Math.max(maxY, p.y);
651                    }
652                }
653            }
654        }
655
656        if (getModel().getShowBlocks()) {
657            for (Block b : diagram.getBlocks()) {
658                BlockWidget w = getWidget(b.getInputBlock());
659                if (w != null && w.isVisible()) {
660                    Rectangle r = b.getBounds();
661                    maxX = Math.max(maxX, r.x + r.width);
662                    maxY = Math.max(maxY, r.y + r.height);
663                }
664            }
665        }
666
667        bottomRight.setPreferredLocation(new Point(maxX + BORDER_SIZE, maxY + BORDER_SIZE));
668        int offx = 0;
669        int offy = 0;
670        int curWidth = maxX + 2 * BORDER_SIZE;
671        int curHeight = maxY + 2 * BORDER_SIZE;
672
673        Rectangle bounds = this.getScrollPane().getBounds();
674        bounds.width /= getZoomFactor();
675        bounds.height /= getZoomFactor();
676        if (curWidth < bounds.width) {
677            offx = (bounds.width - curWidth) / 2;
678        }
679
680        if (curHeight < bounds.height) {
681            offy = (bounds.height - curHeight) / 2;
682        }
683
684        final int offx2 = offx;
685        final int offy2 = offy;
686
687        SceneAnimator animator = this.getSceneAnimator();
688        connectionLayer.removeChildren();
689        int visibleFigureCount = 0;
690        for (Figure f : diagram.getFigures()) {
691            if (getWidget(f, FigureWidget.class).isVisible()) {
692                visibleFigureCount++;
693            }
694        }
695
696
697        Set<Pair<Point, Point>> lastLineCache = lineCache;
698        lineCache = new HashSet<>();
699        for (Figure f : diagram.getFigures()) {
700            for (OutputSlot s : f.getOutputSlots()) {
701                SceneAnimator anim = animator;
702                if (visibleFigureCount > ANIMATION_LIMIT || oldVisibleWidgets == null) {
703                    anim = null;
704                }
705                processOutputSlot(lastLineCache, s, s.getConnections(), 0, null, null, offx2, offy2, anim);
706            }
707        }
708
709        for (Figure f : diagram.getFigures()) {
710            FigureWidget w = getWidget(f);
711            if (w.isVisible()) {
712                Point p = f.getPosition();
713                Point p2 = new Point(p.x + offx2, p.y + offy2);
714                if ((visibleFigureCount <= ANIMATION_LIMIT && oldVisibleWidgets != null && oldVisibleWidgets.contains(w))) {
715                    animator.animatePreferredLocation(w, p2);
716                } else {
717                    w.setPreferredLocation(p2);
718                    animator.animatePreferredLocation(w, p2);
719                }
720            }
721        }
722
723        if (getModel().getShowBlocks()) {
724            for (Block b : diagram.getBlocks()) {
725                BlockWidget w = getWidget(b.getInputBlock());
726                if (w != null && w.isVisible()) {
727                    Point location = new Point(b.getBounds().x + offx2, b.getBounds().y + offy2);
728                    Rectangle r = new Rectangle(location.x, location.y, b.getBounds().width, b.getBounds().height);
729
730                    if ((visibleFigureCount <= ANIMATION_LIMIT && oldVisibleWidgets != null && oldVisibleWidgets.contains(w))) {
731                        animator.animatePreferredBounds(w, r);
732                    } else {
733                        w.setPreferredBounds(r);
734                        animator.animatePreferredBounds(w, r);
735                    }
736                }
737            }
738        }
739
740        this.validate();
741    }
742    private final Point specialNullPoint = new Point(Integer.MAX_VALUE, Integer.MAX_VALUE);
743
744    private void processOutputSlot(Set<Pair<Point, Point>> lastLineCache, OutputSlot s, List<Connection> connections, int controlPointIndex, Point lastPoint, LineWidget predecessor, int offx, int offy, SceneAnimator animator) {
745        Map<Point, List<Connection>> pointMap = new HashMap<>(connections.size());
746
747        for (Connection c : connections) {
748
749            if (!isVisible(c)) {
750                continue;
751            }
752
753            List<Point> controlPoints = c.getControlPoints();
754            if (controlPointIndex >= controlPoints.size()) {
755                continue;
756            }
757
758            Point cur = controlPoints.get(controlPointIndex);
759            if (cur == null) {
760                cur = specialNullPoint;
761            } else if (controlPointIndex == 0 && !s.shouldShowName()) {
762                cur = new Point(cur.x, cur.y - SLOT_OFFSET);
763            } else if (controlPointIndex == controlPoints.size() - 1 && !c.getInputSlot().shouldShowName()) {
764                cur = new Point(cur.x, cur.y + SLOT_OFFSET);
765            }
766
767            if (pointMap.containsKey(cur)) {
768                pointMap.get(cur).add(c);
769            } else {
770                List<Connection> newList = new ArrayList<>(2);
771                newList.add(c);
772                pointMap.put(cur, newList);
773            }
774
775        }
776
777        for (Point p : pointMap.keySet()) {
778            List<Connection> connectionList = pointMap.get(p);
779
780            boolean isBold = false;
781            boolean isDashed = true;
782
783            for (Connection c : connectionList) {
784
785                if (c.getStyle() == Connection.ConnectionStyle.BOLD) {
786                    isBold = true;
787                }
788
789                if (c.getStyle() != Connection.ConnectionStyle.DASHED) {
790                    isDashed = false;
791                }
792            }
793
794            LineWidget newPredecessor = predecessor;
795            if (p == specialNullPoint) {
796            } else if (lastPoint == specialNullPoint) {
797            } else if (lastPoint != null) {
798                Point p1 = new Point(lastPoint.x + offx, lastPoint.y + offy);
799                Point p2 = new Point(p.x + offx, p.y + offy);
800
801                Pair<Point, Point> curPair = new Pair<>(p1, p2);
802                SceneAnimator curAnimator = animator;
803                if (lastLineCache.contains(curPair)) {
804                    curAnimator = null;
805                }
806                LineWidget w = new LineWidget(this, s, connectionList, p1, p2, predecessor, curAnimator, isBold, isDashed);
807                lineCache.add(curPair);
808
809                newPredecessor = w;
810                connectionLayer.addChild(w);
811                this.addObject(new ConnectionSet(connectionList), w);
812                w.getActions().addAction(hoverAction);
813            }
814
815            processOutputSlot(lastLineCache, s, connectionList, controlPointIndex + 1, p, newPredecessor, offx, offy, animator);
816        }
817    }
818
819    @Override
820    public void setInteractionMode(InteractionMode mode) {
821        panAction.setEnabled(mode == InteractionMode.PANNING);
822        // When panAction is not enabled, it does not consume the event
823        // and the selection action handles it instead
824    }
825
826    private class ConnectionSet {
827
828        private Set<Connection> connections;
829
830        public ConnectionSet(Collection<Connection> connections) {
831            connections = new HashSet<>(connections);
832        }
833
834        public Set<Connection> getConnectionSet() {
835            return Collections.unmodifiableSet(connections);
836        }
837    }
838
839    @Override
840    public Lookup getLookup() {
841        return lookup;
842    }
843
844    @Override
845    public void initialize() {
846        Figure f = getModel().getDiagramToView().getRootFigure();
847        if (f != null) {
848            setUndoRedoEnabled(false);
849            gotoFigure(f);
850            setUndoRedoEnabled(true);
851        }
852    }
853
854    public void gotoFigures(final List<Figure> figures) {
855        Rectangle overall = null;
856        getModel().showFigures(figures);
857        for (Figure f : figures) {
858
859            FigureWidget fw = getWidget(f);
860            if (fw != null) {
861                Rectangle r = fw.getBounds();
862                Point p = fw.getLocation();
863                Rectangle r2 = new Rectangle(p.x, p.y, r.width, r.height);
864
865                if (overall == null) {
866                    overall = r2;
867                } else {
868                    overall = overall.union(r2);
869                }
870            }
871        }
872        if (overall != null) {
873            centerRectangle(overall);
874        }
875    }
876
877    private Set<Object> idSetToObjectSet(Set<Object> ids) {
878
879        Set<Object> result = new HashSet<>();
880        for (Figure f : getModel().getDiagramToView().getFigures()) {
881            if (DiagramScene.doesIntersect(f.getSource().getSourceNodesAsSet(), ids)) {
882                result.add(f);
883            }
884
885            for (Slot s : f.getSlots()) {
886                if (DiagramScene.doesIntersect(s.getSource().getSourceNodesAsSet(), ids)) {
887                    result.add(s);
888                }
889            }
890        }
891        return result;
892    }
893
894    public void gotoSelection(Set<Object> ids) {
895
896        Rectangle overall = null;
897        Set<Integer> hiddenNodes = new HashSet<>(this.getModel().getHiddenNodes());
898        hiddenNodes.removeAll(ids);
899        this.getModel().showNot(hiddenNodes);
900
901        Set<Object> objects = idSetToObjectSet(ids);
902        for (Object o : objects) {
903
904            Widget w = getWidget(o);
905            if (w != null) {
906                Rectangle r = w.getBounds();
907                Point p = w.convertLocalToScene(new Point(0, 0));
908
909                Rectangle r2 = new Rectangle(p.x, p.y, r.width, r.height);
910
911                if (overall == null) {
912                    overall = r2;
913                } else {
914                    overall = overall.union(r2);
915                }
916            }
917        }
918        if (overall != null) {
919            centerRectangle(overall);
920        }
921
922        setSelectedObjects(objects);
923    }
924
925    private Point calcCenter(Rectangle r) {
926
927        Point center = new Point((int) r.getCenterX(), (int) r.getCenterY());
928        center.x -= getScrollPane().getViewport().getViewRect().width / 2;
929        center.y -= getScrollPane().getViewport().getViewRect().height / 2;
930
931        // Ensure to be within area
932        center.x = Math.max(0, center.x);
933        center.x = Math.min(getScrollPane().getViewport().getViewSize().width - getScrollPane().getViewport().getViewRect().width, center.x);
934        center.y = Math.max(0, center.y);
935        center.y = Math.min(getScrollPane().getViewport().getViewSize().height - getScrollPane().getViewport().getViewRect().height, center.y);
936
937        return center;
938    }
939
940    private void centerRectangle(Rectangle r) {
941
942        if (getScrollPane().getViewport().getViewRect().width == 0 || getScrollPane().getViewport().getViewRect().height == 0) {
943            return;
944        }
945
946        Rectangle r2 = new Rectangle(r.x, r.y, r.width, r.height);
947        r2 = convertSceneToView(r2);
948
949        double factorX = (double) r2.width / (double) getScrollPane().getViewport().getViewRect().width;
950        double factorY = (double) r2.height / (double) getScrollPane().getViewport().getViewRect().height;
951        double factor = Math.max(factorX, factorY);
952        if (factor >= 1.0) {
953            Point p = getScrollPane().getViewport().getViewPosition();
954            setZoomFactor(getZoomFactor() / factor);
955            r2.x /= factor;
956            r2.y /= factor;
957            r2.width /= factor;
958            r2.height /= factor;
959            getScrollPane().getViewport().setViewPosition(calcCenter(r2));
960        } else {
961            getScrollPane().getViewport().setViewPosition(calcCenter(r2));
962        }
963    }
964
965    @Override
966    public void setSelection(Collection<Figure> list) {
967        super.setSelectedObjects(new HashSet<>(list));
968    }
969
970    private UndoRedo.Manager getUndoRedoManager() {
971        if (undoRedoManager == null) {
972            undoRedoManager = new UndoRedo.Manager();
973            undoRedoManager.setLimit(UNDOREDO_LIMIT);
974        }
975
976        return undoRedoManager;
977    }
978
979    @Override
980    public UndoRedo getUndoRedo() {
981        return getUndoRedoManager();
982    }
983
984    private boolean isVisible(Figure f) {
985        for (Integer n : f.getSource().getSourceNodesAsSet()) {
986            if (getModel().getHiddenNodes().contains(n)) {
987                return false;
988            }
989        }
990        return true;
991    }
992
993    public static boolean doesIntersect(Set<?> s1, Set<?> s2) {
994        if (s1.size() > s2.size()) {
995            Set<?> tmp = s1;
996            s1 = s2;
997            s2 = tmp;
998        }
999
1000        for (Object o : s1) {
1001            if (s2.contains(o)) {
1002                return true;
1003            }
1004        }
1005
1006        return false;
1007    }
1008
1009    @Override
1010    public void componentHidden() {
1011        SelectionCoordinator.getInstance().getHighlightedChangedEvent().removeListener(highlightedCoordinatorListener);
1012        SelectionCoordinator.getInstance().getSelectedChangedEvent().removeListener(selectedCoordinatorListener);
1013    }
1014
1015    @Override
1016    public void componentShowing() {
1017        SelectionCoordinator.getInstance().getHighlightedChangedEvent().addListener(highlightedCoordinatorListener);
1018        SelectionCoordinator.getInstance().getSelectedChangedEvent().addListener(selectedCoordinatorListener);
1019    }
1020
1021    private void updateHiddenNodes(Set<Integer> newHiddenNodes, boolean doRelayout) {
1022
1023        Diagram diagram = getModel().getDiagramToView();
1024        assert diagram != null;
1025
1026        Set<InputBlock> visibleBlocks = new HashSet<InputBlock>();
1027        Set<Widget> oldVisibleWidgets = new HashSet<>();
1028
1029        for (Figure f : diagram.getFigures()) {
1030            FigureWidget w = getWidget(f);
1031            if (w != null && w.isVisible()) {
1032                oldVisibleWidgets.add(w);
1033            }
1034        }
1035
1036        if (getModel().getShowBlocks()) {
1037            for (InputBlock b : diagram.getGraph().getBlocks()) {
1038                BlockWidget w = getWidget(b);
1039                if (w.isVisible()) {
1040                    oldVisibleWidgets.add(w);
1041                }
1042            }
1043        }
1044
1045        for (Figure f : diagram.getFigures()) {
1046            boolean hiddenAfter = doesIntersect(f.getSource().getSourceNodesAsSet(), newHiddenNodes);
1047
1048            FigureWidget w = getWidget(f);
1049            w.setBoundary(false);
1050            if (!hiddenAfter) {
1051                // Figure is shown
1052                w.setVisible(true);
1053                for (InputNode n : f.getSource().getSourceNodes()) {
1054                    visibleBlocks.add(diagram.getGraph().getBlock(n));
1055                }
1056            } else {
1057                // Figure is hidden
1058                w.setVisible(false);
1059            }
1060        }
1061
1062        if (getModel().getShowNodeHull()) {
1063            List<FigureWidget> boundaries = new ArrayList<>();
1064            for (Figure f : diagram.getFigures()) {
1065                FigureWidget w = getWidget(f);
1066                if (!w.isVisible()) {
1067                    Set<Figure> set = new HashSet<>(f.getPredecessorSet());
1068                    set.addAll(f.getSuccessorSet());
1069
1070                    boolean b = false;
1071                    for (Figure neighbor : set) {
1072                        FigureWidget neighborWidget = getWidget(neighbor);
1073                        if (neighborWidget.isVisible()) {
1074                            b = true;
1075                            break;
1076                        }
1077                    }
1078
1079                    if (b) {
1080                        w.setBoundary(true);
1081                        for (InputNode n : f.getSource().getSourceNodes()) {
1082                            visibleBlocks.add(diagram.getGraph().getBlock(n));
1083                        }
1084                        boundaries.add(w);
1085                    }
1086                }
1087            }
1088
1089            for (FigureWidget w : boundaries) {
1090                if (w.isBoundary()) {
1091                    w.setVisible(true);
1092                }
1093            }
1094        }
1095
1096        if (getModel().getShowBlocks()) {
1097            for (InputBlock b : diagram.getGraph().getBlocks()) {
1098
1099                boolean visibleAfter = visibleBlocks.contains(b);
1100
1101                BlockWidget w = getWidget(b);
1102                if (visibleAfter) {
1103                    // Block must be shown
1104                    w.setVisible(true);
1105                } else {
1106                    // Block must be hidden
1107                    w.setVisible(false);
1108                }
1109            }
1110        }
1111
1112        if (doRelayout) {
1113            relayout(oldVisibleWidgets);
1114        }
1115        this.validate();
1116        addUndo();
1117    }
1118
1119    private void showFigure(Figure f) {
1120        HashSet<Integer> newHiddenNodes = new HashSet<>(getModel().getHiddenNodes());
1121        newHiddenNodes.removeAll(f.getSource().getSourceNodesAsSet());
1122        this.model.setHiddenNodes(newHiddenNodes);
1123    }
1124
1125    public void show(final Figure f) {
1126        showFigure(f);
1127    }
1128
1129    public void setSelectedObjects(Object... args) {
1130        Set<Object> set = new HashSet<>();
1131        for (Object o : args) {
1132            set.add(o);
1133        }
1134        super.setSelectedObjects(set);
1135    }
1136
1137    private void centerWidget(Widget w) {
1138        Rectangle r = w.getBounds();
1139        Point p = w.getLocation();
1140        centerRectangle(new Rectangle(p.x, p.y, r.width, r.height));
1141    }
1142
1143    public void gotoFigure(final Figure f) {
1144        if (!isVisible(f)) {
1145            showFigure(f);
1146        }
1147
1148        FigureWidget fw = getWidget(f);
1149        if (fw != null) {
1150            centerWidget(fw);
1151            setSelection(Arrays.asList(f));
1152        }
1153    }
1154
1155    public JPopupMenu createPopupMenu() {
1156        JPopupMenu menu = new JPopupMenu();
1157
1158        Action[] currentActions = actionsWithSelection;
1159        if (this.getSelectedObjects().isEmpty()) {
1160            currentActions = actions;
1161        }
1162        for (Action a : currentActions) {
1163            if (a == null) {
1164                menu.addSeparator();
1165            } else {
1166                menu.add(a);
1167            }
1168        }
1169        return menu;
1170    }
1171
1172    private static class DiagramUndoRedo extends AbstractUndoableEdit implements ChangedListener<DiagramViewModel> {
1173
1174        private DiagramViewModel oldModel;
1175        private DiagramViewModel newModel;
1176        private Point oldScrollPosition;
1177        private DiagramScene scene;
1178
1179        public DiagramUndoRedo(DiagramScene scene, Point oldScrollPosition, DiagramViewModel oldModel, DiagramViewModel newModel) {
1180            assert oldModel != null;
1181            assert newModel != null;
1182            this.oldModel = oldModel;
1183            this.newModel = newModel;
1184            this.scene = scene;
1185            this.oldScrollPosition = oldScrollPosition;
1186        }
1187
1188        @Override
1189        public void redo() throws CannotRedoException {
1190            super.redo();
1191            boolean b = scene.getUndoRedoEnabled();
1192            scene.setUndoRedoEnabled(false);
1193            scene.getModel().getViewChangedEvent().addListener(this);
1194            scene.getModel().setData(newModel);
1195            scene.getModel().getViewChangedEvent().removeListener(this);
1196            scene.setUndoRedoEnabled(b);
1197        }
1198
1199        @Override
1200        public void undo() throws CannotUndoException {
1201            super.undo();
1202            boolean b = scene.getUndoRedoEnabled();
1203            scene.setUndoRedoEnabled(false);
1204            scene.getModel().getViewChangedEvent().addListener(this);
1205            scene.getModel().setData(oldModel);
1206            scene.getModel().getViewChangedEvent().removeListener(this);
1207
1208            SwingUtilities.invokeLater(new Runnable() {
1209
1210                @Override
1211                public void run() {
1212                    scene.setScrollPosition(oldScrollPosition);
1213                }
1214            });
1215
1216            scene.setUndoRedoEnabled(b);
1217        }
1218
1219        @Override
1220        public void changed(DiagramViewModel source) {
1221            scene.getModel().getViewChangedEvent().removeListener(this);
1222            if (oldModel.getHiddenNodes().equals(newModel.getHiddenNodes())) {
1223                scene.smallUpdate(false);
1224            } else {
1225                scene.smallUpdate(true);
1226            }
1227        }
1228    }
1229    private boolean undoRedoEnabled = true;
1230
1231    public void setUndoRedoEnabled(boolean b) {
1232        this.undoRedoEnabled = b;
1233    }
1234
1235    public boolean getUndoRedoEnabled() {
1236        return undoRedoEnabled;
1237    }
1238
1239    private final ChangedListener<DiagramViewModel> fullChange = new ChangedListener<DiagramViewModel>() {
1240        @Override
1241        public void changed(DiagramViewModel source) {
1242            assert source == model : "Receive only changed event from current model!";
1243            assert source != null;
1244            update();
1245        }
1246    };
1247
1248    private final ChangedListener<DiagramViewModel> hiddenNodesChange = new ChangedListener<DiagramViewModel>() {
1249        @Override
1250        public void changed(DiagramViewModel source) {
1251            assert source == model : "Receive only changed event from current model!";
1252            assert source != null;
1253            smallUpdate(true);
1254        }
1255    };
1256
1257    private final ChangedListener<DiagramViewModel> selectionChange = new ChangedListener<DiagramViewModel>() {
1258        @Override
1259        public void changed(DiagramViewModel source) {
1260            assert source == model : "Receive only changed event from current model!";
1261            assert source != null;
1262            smallUpdate(false);
1263        }
1264    };
1265
1266
1267    private void addUndo() {
1268
1269        DiagramViewModel newModelCopy = model.copy();
1270
1271        if (undoRedoEnabled) {
1272            this.getUndoRedoManager().undoableEditHappened(new UndoableEditEvent(this, new DiagramUndoRedo(this, this.getScrollPosition(), modelCopy, newModelCopy)));
1273        }
1274
1275        this.modelCopy = newModelCopy;
1276    }
1277}
1278