Xcos: use dynamic palette for "Commonly used blocks"
[scilab.git] / scilab / modules / xcos / src / java / org / scilab / modules / xcos / palette / view / PaletteManagerPanel.java
1 /*
2  * Scilab ( http://www.scilab.org/ ) - This file is part of Scilab
3  * Copyright (C) 2010 - DIGITEO - Clement DAVID
4  * Copyright (C) 2011-2015 - Scilab Enterprises - Clement DAVID
5  * Copyright (C) 2015 - Marcos CARDINOT
6  *
7  * Copyright (C) 2012 - 2016 - Scilab Enterprises
8  *
9  * This file is hereby licensed under the terms of the GNU GPL v2.0,
10  * pursuant to article 5.3.4 of the CeCILL v.2.1.
11  * This file was originally licensed under the terms of the CeCILL v2.1,
12  * and continues to be available under such terms.
13  * For more information, see the COPYING file which you should have received
14  * along with this program.
15  *
16  */
17
18 package org.scilab.modules.xcos.palette.view;
19
20 import java.awt.Color;
21 import java.awt.event.ActionEvent;
22 import java.awt.event.ActionListener;
23 import java.awt.Component;
24 import java.awt.Dimension;
25 import java.awt.Toolkit;
26 import java.awt.event.MouseWheelEvent;
27 import java.awt.event.MouseWheelListener;
28 import java.util.ArrayList;
29 import java.util.List;
30
31 import javax.swing.DropMode;
32 import javax.swing.JPanel;
33 import javax.swing.JScrollBar;
34 import javax.swing.JScrollPane;
35 import javax.swing.JSplitPane;
36 import javax.swing.JTree;
37 import javax.swing.Timer;
38 import javax.swing.tree.TreeNode;
39 import javax.swing.tree.TreePath;
40 import javax.swing.tree.TreeSelectionModel;
41
42 import org.scilab.modules.xcos.JavaController;
43 import org.scilab.modules.xcos.Kind;
44 import org.scilab.modules.xcos.graph.PaletteDiagram;
45 import org.scilab.modules.xcos.palette.PaletteCtrl;
46 import org.scilab.modules.xcos.palette.PaletteBlockCtrl;
47 import org.scilab.modules.xcos.palette.PaletteManager;
48 import org.scilab.modules.xcos.palette.actions.NavigationAction;
49 import org.scilab.modules.xcos.palette.actions.ZoomAction;
50 import org.scilab.modules.xcos.palette.listener.PaletteManagerMouseListener;
51 import org.scilab.modules.xcos.palette.listener.PaletteManagerTreeSelectionListener;
52 import org.scilab.modules.xcos.palette.listener.PaletteTreeTransferHandler;
53 import org.scilab.modules.xcos.palette.model.Category;
54 import org.scilab.modules.xcos.palette.model.Custom;
55 import org.scilab.modules.xcos.palette.model.PaletteBlock;
56 import org.scilab.modules.xcos.palette.model.PaletteNode;
57 import org.scilab.modules.xcos.palette.model.PreLoaded;
58 import org.scilab.modules.xcos.utils.Stack;
59 import org.scilab.modules.xcos.utils.XcosConstants;
60 import org.scilab.modules.xcos.utils.XcosConstants.PaletteBlockSize;
61
62 /**
63  * The content pane for the block view
64  */
65 @SuppressWarnings(value = { "serial" })
66 public class PaletteManagerPanel extends JSplitPane {
67
68     /** Name of tree node for recently used blocks **/
69     public static final String RECENTLY_USED_BLOCKS = "Recently Used Blocks";
70
71     private static final int NUMBER_OF_DUMMY_BLOCKS = 10; // 10 blocks loaded,
72     private static final int LOAD_DUMMY_BLOCK_DELAY = 200; // one each 200 milliseconds
73
74     private static final class LoadBlock implements ActionListener {
75         private final PaletteManager controller;
76         private int loadedBlocks;
77
78         private Stack<TreeNode> nodeStack;
79         private Stack<PaletteBlock> blocksStack;
80
81         LoadBlock(final PaletteManager controller, final int loadedBlocks) {
82             this.controller = controller;
83             this.loadedBlocks = loadedBlocks;
84
85             nodeStack = new Stack<>();
86             nodeStack.push(controller.getRoot());
87             blocksStack = new Stack<>();
88         }
89
90         @Override
91         public void actionPerformed(ActionEvent e) {
92
93             // load only a limited set of blocks
94             if (loadedBlocks <= 0) {
95                 Timer timer = (Timer) e.getSource();
96                 timer.stop();
97             }
98
99             // consume a block
100             if (blocksStack.size() != 0) {
101                 loadTransferable(blocksStack.pop());
102                 return;
103             }
104
105             // no block is available, look for a Preloaded palette
106             while (nodeStack.size() != 0) {
107                 TreeNode o = nodeStack.pop();
108
109                 if (o instanceof PreLoaded) {
110                     // found it a palette, create a dummy transferable from the first block and store the others for later use.
111                     blocksStack.addAll(((PreLoaded) o).getBlock());
112
113                     if (blocksStack.size() != 0) {
114                         loadTransferable(blocksStack.pop());
115                         return;
116                     }
117                 } else {
118                     for (int i = 0; i < o.getChildCount(); i++) {
119                         nodeStack.push(o.getChildAt(i));
120                     }
121                 }
122             }
123         }
124
125         private void loadTransferable(final PaletteBlock blk) {
126             new PaletteBlockCtrl(new PaletteCtrl(), blk).getPaletteCtrl().getTransferable();
127             loadedBlocks -= 1;
128         }
129     }
130
131     private static XcosConstants.PaletteBlockSize currentSize;
132     private final PaletteManager controller;
133     private CustomMouseWheelListener mouseWheelListener;
134     private JTree tree;
135     private PaletteDiagram diagramInstance;
136     private List<TreePath> historyNext;
137     private List<TreePath> historyPrev;
138     private TreePath currentPath;
139
140     /**
141      * Default constructor
142      *
143      * @param controller
144      *            the {@link PaletteManager} instance
145      */
146     public PaletteManagerPanel(PaletteManager controller) {
147         super(JSplitPane.HORIZONTAL_SPLIT);
148         this.controller = controller;
149         this.mouseWheelListener = new CustomMouseWheelListener();
150         this.historyNext = new ArrayList<TreePath>();
151         this.historyPrev = new ArrayList<TreePath>();
152         this.currentPath = null;
153         currentSize = XcosConstants.PaletteBlockSize.NORMAL;
154         fillUpContentPane();
155     }
156
157     /**
158      * Fill up the content pane
159      */
160     private void fillUpContentPane() {
161         // Default instances
162         JScrollPane panel = new JScrollPane();
163         panel.setBackground(Color.WHITE);
164         setUpScrollBar(panel, currentSize);
165
166         // Set default left component
167         JPanel rootPalette = new JPanel();
168
169         TreeNode root = controller.getRoot();
170         tree = new JTree(new PaletteTreeModel(root));
171         JScrollPane treeScrollPane = new JScrollPane(tree);
172
173         // Setup tree
174         tree.getSelectionModel().setSelectionMode(
175             TreeSelectionModel.SINGLE_TREE_SELECTION);
176         tree.addMouseListener(new PaletteManagerMouseListener());
177         tree.addTreeSelectionListener(new PaletteManagerTreeSelectionListener(
178                                           this, panel));
179
180         tree.setEditable(false);
181         tree.setDragEnabled(true);
182         tree.setExpandsSelectedPaths(true);
183         tree.setDropMode(DropMode.INSERT);
184         tree.setTransferHandler(new PaletteTreeTransferHandler());
185
186         setLeftComponent(treeScrollPane);
187         panel.setViewportView(rootPalette);
188         setRightComponent(panel);
189     }
190
191     /**
192      * Get the current PaletteBlockSize
193      * @return current PaletteBlockSize
194      */
195     public static PaletteBlockSize getCurrentSize() {
196         return currentSize;
197     }
198
199     /**
200      * Adds a PaletteBlock to the list of recently used blocks.
201      * @param block PaletteBlock
202      */
203     public void addRecentltyUsedBlock(PaletteBlock block) {
204         PaletteNode currentNode = (PaletteNode) tree.getLastSelectedPathComponent();
205         if (currentNode != null && currentNode.getName().equals(RECENTLY_USED_BLOCKS)) {
206             // do not refresh when the dynamic palette is selected
207             return;
208         }
209
210         // look for the dynamic palette
211         final Category root = PaletteManager.getInstance().getRoot();
212         ArrayList<PaletteNode> nodes = new ArrayList<>(root.getNode());
213         PreLoaded p = null;
214         while (p == null && !nodes.isEmpty()) {
215             // pop()
216             PaletteNode n = nodes.get(nodes.size() - 1);
217             nodes.remove(nodes.size() - 1);
218
219             // terminate if found
220             if (n instanceof PreLoaded && n.getName().equals(RECENTLY_USED_BLOCKS)) {
221                 p = (PreLoaded) n;
222                 break;
223             }
224
225             // add all the sub palettes
226             if (n instanceof Category) {
227                 nodes.addAll(((Category) n).getNode());
228             }
229         }
230         if (p == null) {
231             return;
232         }
233
234         // do not resort if already present
235         List<PaletteBlock> blocks = p.getBlock();
236         for (PaletteBlock b : blocks) {
237             if (b.getName().equals(block.getName())) {
238                 return;
239             }
240         }
241
242         // add the block but keep the same number of elements (with a custom palette.xml the user can change the number of recent blocks)
243         blocks.remove(blocks.size() - 1);
244         blocks.add(0, block);
245     }
246
247     /**
248      * Zoom
249      * @param newSize new paletteblocksize enum
250      */
251     // CSOFF: CyclomaticComplexity
252     private void zoom(PaletteBlockSize newSize) {
253         if (newSize == null || newSize == currentSize) {
254             return;
255         }
256
257         currentSize = newSize;
258
259         try {
260             JScrollPane jspR = (JScrollPane) this.getRightComponent();
261             final Dimension dimension = jspR.getPreferredSize();
262             setUpScrollBar(jspR, newSize);
263
264             // check what's being displayed on the right panel
265             PaletteNode node = (PaletteNode) tree.getLastSelectedPathComponent();
266             if (node instanceof PreLoaded || node == null) {
267                 Component c = jspR.getViewport().getComponent(0);
268                 String cName = c.getName();
269                 PaletteView pview;
270                 if (cName.equals("PaletteView")) {
271                     pview = (PaletteView) c;
272                 } else if (cName.equals("PaletteSearchView")) {
273                     PaletteSearchView sview = (PaletteSearchView) c;
274                     pview = (PaletteView) sview.getComponent(1);
275                 } else {
276                     return;
277                 }
278
279                 Component[] blockViews = pview.getComponents();
280                 for (Component component : blockViews) {
281                     PaletteBlockView bview = (PaletteBlockView) component;
282                     bview.initComponents();
283                 }
284                 pview.revalidate();
285             } else if (node instanceof Custom) {
286                 jspR = openDiagramAsPal(node);
287                 jspR.setPreferredSize(dimension);
288                 this.setRightComponent(jspR);
289             } else {
290                 return;
291             }
292
293             // update the status of both zoom buttons
294             if (newSize.next() == null) {
295                 // is it the last zoom-in level?
296                 ZoomAction.setEnabledZoomIn(false);
297                 ZoomAction.setEnabledZoomOut(true);
298             } else if (newSize.previous() == null) {
299                 // is it the last zoom-out level?
300                 ZoomAction.setEnabledZoomIn(true);
301                 ZoomAction.setEnabledZoomOut(false);
302             } else {
303                 ZoomAction.setEnabledZoomIn(true);
304                 ZoomAction.setEnabledZoomOut(true);
305             }
306
307             jspR.revalidate();
308         } catch (NullPointerException e) {
309             e.printStackTrace();
310         }
311     }
312
313     /** zoom in **/
314     public void zoomIn() {
315         zoom(currentSize.next());
316     }
317
318     /** zoom out **/
319     public void zoomOut() {
320         zoom(currentSize.previous());
321     }
322
323     /**
324      * Open a diagram as a palette.
325      * @param node PaletteNode
326      * @return a JScrollPane with the diagram
327      */
328     public JScrollPane openDiagramAsPal(PaletteNode node) {
329         String path = ((Custom) node).getPath().getEvaluatedPath();
330         JavaController ctroller = new JavaController();
331         this.diagramInstance = new PaletteDiagram(ctroller.createObject(Kind.DIAGRAM));
332         this.diagramInstance.openDiagramAsPal(path);
333         return this.diagramInstance.getAsComponent();
334     }
335
336     /**
337      * Setup the ScrollPane component
338      * @param jsp The component
339      * @param pbs PaletteBlockSize
340      */
341     private void setUpScrollBar(JScrollPane jsp, PaletteBlockSize pbs) {
342         // vertical
343         jsp.getVerticalScrollBar().setBlockIncrement(
344             pbs.getBlockDimension().height
345             + XcosConstants.PALETTE_VMARGIN);
346         jsp.getVerticalScrollBar().setUnitIncrement(
347             pbs.getBlockDimension().height
348             + XcosConstants.PALETTE_VMARGIN);
349         // horizontal
350         jsp.getHorizontalScrollBar().setBlockIncrement(
351             pbs.getBlockDimension().width
352             + XcosConstants.PALETTE_HMARGIN);
353         jsp.getHorizontalScrollBar().setUnitIncrement(
354             pbs.getBlockDimension().width
355             + XcosConstants.PALETTE_HMARGIN);
356
357         mouseWheelListener.setVerticalScrollBar(jsp.getVerticalScrollBar());
358
359         // removes the mouse wheel listeners
360         MouseWheelListener[] mwls = jsp.getMouseWheelListeners();
361         for (MouseWheelListener mwl : mwls) {
362             jsp.removeMouseWheelListener(mwl);
363         }
364         // adds the CustomMouseWheelListener
365         jsp.addMouseWheelListener(mouseWheelListener);
366     }
367
368     /**
369      * Setup the default layout
370      */
371     public void performStartUpLayout() {
372         /* Tree layout */
373         final Object root = tree.getModel().getRoot();
374         final Object firstChild = tree.getModel().getChild(root, 0);
375         final Object secondChild = tree.getModel().getChild(firstChild, 0);
376         tree.setSelectionPath(new TreePath(new Object[] {root, firstChild, secondChild}));
377
378         tree.setRootVisible(false);
379         tree.setScrollsOnExpand(true);
380
381         /* Global layout */
382         setContinuousLayout(true);
383
384         // Delay-load some blocks to pre-load jars used on the rendering.
385         // A timer is used to avoid busying EDT and add a small delay between blocks
386         Timer timer = new Timer(LOAD_DUMMY_BLOCK_DELAY, new LoadBlock(controller, NUMBER_OF_DUMMY_BLOCKS));
387         timer.start();
388     }
389
390     /**
391      * Updates the history.
392      */
393     public void updateHistory() {
394         TreePath lastPath = currentPath;
395         currentPath = tree.getSelectionPath();
396
397         if (lastPath != null && !currentPath.equals(lastPath)) {
398
399             if (historyPrev.isEmpty()) {
400                 historyPrev.add(lastPath);
401             } else {
402                 int prevPathIdx = historyPrev.size() - 1;
403                 // going backwards?
404                 if (currentPath.equals(historyPrev.get(prevPathIdx))) {
405                     historyNext.add(lastPath);
406                     historyPrev.remove(prevPathIdx);
407                     updateHistorySettings();
408                     return;
409                 } else {
410                     // it seems to be a new path, just store it!
411                     // Soon we'll check if we are just going forward
412                     // or if it's really a new path!
413                     historyPrev.add(lastPath);
414                 }
415             }
416
417             if (!historyNext.isEmpty()) {
418                 int nextPathIdx = historyNext.size() - 1;
419                 // going forward?
420                 if (currentPath.equals(historyNext.get(nextPathIdx))) {
421                     historyNext.remove(nextPathIdx);
422                 } else {
423                     // it is really a new path!
424                     // make sure it's on the top of the history!
425                     historyNext.clear();
426                 }
427             }
428             updateHistorySettings();
429         }
430     }
431
432     /**
433      * Update the status of the buttons and the history length.
434      */
435     private void updateHistorySettings() {
436         if (historyNext.size() + historyPrev.size() > XcosConstants.HISTORY_LENGTH) {
437             historyPrev.remove(0);
438         }
439         NavigationAction.setEnabledNext(historyNext.size() > 0);
440         NavigationAction.setEnabledPrev(historyPrev.size() > 0);
441     }
442
443     /**
444      * Go to the next path (history).
445      */
446     public void goNext() {
447         try {
448             tree.setSelectionPath(historyNext.get(historyNext.size() - 1));
449         } catch (ArrayIndexOutOfBoundsException err) {
450             NavigationAction.setEnabledNext(false);
451         }
452     }
453
454     /**
455      * Go to the previous path (history).
456      */
457     public void goPrevious() {
458         try {
459             tree.setSelectionPath(historyPrev.get(historyPrev.size() - 1));
460         } catch (ArrayIndexOutOfBoundsException err) {
461             NavigationAction.setEnabledPrev(false);
462         }
463     }
464
465     /**
466      * Implement custom mouse handling for the zoom
467      */
468     private static final class CustomMouseWheelListener implements MouseWheelListener {
469         private static final int ACCELERATOR_KEY = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
470         private JScrollBar verticalScrollBar;
471
472         /**
473          * Default constructor
474          */
475         public CustomMouseWheelListener() {
476         }
477
478         /**
479          * Set the vertical scrollbar
480          * It's important to update the unit increment
481          * @param verticalScrollBar JScrollBar
482          */
483         public void setVerticalScrollBar(JScrollBar verticalScrollBar) {
484             this.verticalScrollBar = verticalScrollBar;
485         }
486
487         /**
488          * When the wheel is used
489          * @param e The parameters
490          * @see java.awt.event.MouseWheelListener#mouseWheelMoved(java.awt.event.MouseWheelEvent)
491          */
492         @Override
493         public void mouseWheelMoved(MouseWheelEvent e) {
494             if (verticalScrollBar == null) {
495                 // nothing to do!
496                 return;
497             }
498
499             if (e.getModifiers() == ACCELERATOR_KEY) {
500                 if (e.getWheelRotation() < 0) {
501                     PaletteManagerView.get().getPanel().zoomIn();
502                 } else if (e.getWheelRotation() > 0) {
503                     PaletteManagerView.get().getPanel().zoomOut();
504                 }
505             } else {
506                 int i = verticalScrollBar.getValue();
507                 if (e.getWheelRotation() < 0) {
508                     i -= verticalScrollBar.getUnitIncrement();
509                 } else {
510                     i += verticalScrollBar.getUnitIncrement();
511                 }
512                 verticalScrollBar.setValue(i);
513             }
514         }
515     }
516 }