[ui_data] prevent spurious FileBrowser ScrollBar reset after 93c93ccf
[scilab.git] / scilab / modules / ui_data / src / java / org / scilab / modules / ui_data / filebrowser / SwingScilabTreeTable.java
1 /*
2  * Scilab ( http://www.scilab.org/ ) - This file is part of Scilab
3  * Copyright (C) 2011 - DIGITEO - Calixte DENIZET
4  *
5  * Copyright (C) 2012 - 2016 - Scilab Enterprises
6  *
7  * This file is hereby licensed under the terms of the GNU GPL v2.0,
8  * pursuant to article 5.3.4 of the CeCILL v.2.1.
9  * This file was originally licensed under the terms of the CeCILL v2.1,
10  * and continues to be available under such terms.
11  * For more information, see the COPYING file which you should have received
12  * along with this program.
13  *
14  */
15
16 package org.scilab.modules.ui_data.filebrowser;
17
18 import java.awt.Color;
19 import java.awt.Component;
20 import java.awt.Dimension;
21 import java.awt.Graphics;
22 import java.awt.Insets;
23 import java.awt.Point;
24 import java.awt.Rectangle;
25 import java.awt.event.KeyAdapter;
26 import java.awt.event.KeyEvent;
27 import java.awt.event.MouseAdapter;
28 import java.awt.event.MouseEvent;
29 import java.io.File;
30 import java.lang.reflect.Method;
31 import java.text.DateFormat;
32 import java.util.Date;
33 import java.util.regex.Pattern;
34
35 import javax.swing.ActionMap;
36 import javax.swing.ImageIcon;
37 import javax.swing.InputMap;
38 import javax.swing.JButton;
39 import javax.swing.JLabel;
40 import javax.swing.JMenuItem;
41 import javax.swing.JPopupMenu;
42 import javax.swing.JScrollPane;
43 import javax.swing.JTable;
44 import javax.swing.KeyStroke;
45 import javax.swing.SwingUtilities;
46 import javax.swing.border.AbstractBorder;
47 import javax.swing.border.Border;
48 import javax.swing.plaf.basic.BasicTreeUI;
49 import javax.swing.table.DefaultTableCellRenderer;
50 import javax.swing.SwingWorker;
51 import javax.swing.tree.TreePath;
52
53 import org.scilab.modules.commons.gui.FindIconHelper;
54 import org.scilab.modules.gui.events.callback.CommonCallBack;
55 import org.scilab.modules.ui_data.filebrowser.actions.ChangeCWDAction;
56 import org.scilab.modules.ui_data.filebrowser.actions.EditFileWithDefaultAppAction;
57 import org.scilab.modules.ui_data.filebrowser.actions.ExecuteFileInConsoleAction;
58 import org.scilab.modules.ui_data.filebrowser.actions.ExecuteFileInXcosAction;
59 import org.scilab.modules.ui_data.filebrowser.actions.ExecuteMatFileAction;
60 import org.scilab.modules.ui_data.filebrowser.actions.LoadFileAsGraphAction;
61 import org.scilab.modules.ui_data.filebrowser.actions.LoadFileInScilabAction;
62 import org.scilab.modules.ui_data.filebrowser.actions.OpenFileInSciNotesAction;
63 import org.scilab.modules.ui_data.filebrowser.actions.OpenFileWithDefaultAppAction;
64 import org.scilab.modules.ui_data.filebrowser.actions.ValidateAction;
65 import org.scilab.modules.ui_data.utils.UiDataMessages;
66
67 /**
68  * The tree table model abstract implementation
69  * @author Calixte DENIZET
70  */
71 @SuppressWarnings(value = { "serial" })
72 public class SwingScilabTreeTable extends JTable {
73
74     private static final Insets INSETS = new Insets(0, 2, 0, 0);
75     private static final DateFormat DATEFORMAT = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM);
76
77     private static final Border BORDER = new AbstractBorder() {
78         public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) {
79             g.setColor(Color.LIGHT_GRAY);
80             g.drawLine(x, y, x, y + height);
81         }
82
83         public Insets getBorderInsets(Component c) {
84             return INSETS;
85         }
86
87         public Insets getBorderInsets(Component c, Insets insets) {
88             return INSETS;
89         }
90     };
91
92     private SwingWorker dirRefresher;
93     private ScilabFileBrowserModel model;
94     private boolean resetScrollBar = true;
95
96     private Method isLocationInExpandControl;
97
98     protected ScilabTreeTableCellRenderer tree;
99     protected ScilabFileSelectorComboBox combobox;
100     protected ScilabFileBrowserHistory history;
101     
102     /**
103      * Default Constructor
104      * @param treeTableModel the tree table model
105      * @param combobox the combox used to set the path
106      */
107     public SwingScilabTreeTable(ScilabTreeTableModel treeTableModel, ScilabFileSelectorComboBox combobox) {
108         super();
109         this.combobox = combobox;
110         combobox.setTreeTable(this);
111         history = new ScilabFileBrowserHistory(this);
112         tree = new ScilabTreeTableCellRenderer(this, treeTableModel);
113         super.setModel(new ScilabTreeTableModelAdapter(treeTableModel, tree));
114
115         // Install the tree editor renderer and editor.
116         setDefaultRenderer(ScilabTreeTableModel.class, tree);
117         setDefaultRenderer(Date.class, new DefaultTableCellRenderer() {
118             {
119                 setHorizontalTextPosition(DefaultTableCellRenderer.LEFT);
120             }
121
122             public Component getTableCellRendererComponent(JTable table, Object value, boolean selected, boolean focus, int row, int col) {
123                 JLabel label = (JLabel) super.getTableCellRendererComponent(table, value, selected, focus, row, col);
124                 label.setText(DATEFORMAT.format((Date) value));
125                 if (col == 1) {
126                     label.setBorder(BORDER);
127                 }
128                 return label;
129             }
130         });
131         setDefaultRenderer(ScilabFileBrowserModel.FileSize.class, new DefaultTableCellRenderer() {
132             {
133                 setHorizontalTextPosition(DefaultTableCellRenderer.LEFT);
134             }
135
136             public Component getTableCellRendererComponent(JTable table, Object value, boolean selected, boolean focus, int row, int col) {
137                 Component c = super.getTableCellRendererComponent(table, value, selected, focus, row, col);
138                 if (col == 1) {
139                     JLabel jl = (JLabel) c;
140                     jl.setBorder(BORDER);
141                 }
142                 return c;
143             }
144         });
145         setDefaultRenderer(String.class, new DefaultTableCellRenderer() {
146             {
147                 setHorizontalTextPosition(DefaultTableCellRenderer.LEFT);
148             }
149
150             public Component getTableCellRendererComponent(JTable table, Object value, boolean selected, boolean focus, int row, int col) {
151                 Component c = super.getTableCellRendererComponent(table, value, selected, focus, row, col);
152                 if (col == 1) {
153                     JLabel jl = (JLabel) c;
154                     jl.setBorder(BORDER);
155                 }
156                 return c;
157             }
158         });
159
160         setShowGrid(false);
161         setFillsViewportHeight(true);
162         setIntercellSpacing(new Dimension(0, 0));
163         setRowSorter(new FileBrowserRowSorter(tree, this));
164         setAutoResizeMode(AUTO_RESIZE_NEXT_COLUMN);
165
166         try {
167             isLocationInExpandControl = BasicTreeUI.class.getDeclaredMethod("isLocationInExpandControl", new Class[] {TreePath.class, int.class, int.class});
168             isLocationInExpandControl.setAccessible(true);
169         } catch (NoSuchMethodException e) { }
170
171         addMouseListener(new MouseAdapter() {
172             public void mousePressed(MouseEvent e) {
173                 Point p = e.getPoint();
174                 int col = columnAtPoint(p);
175                 if (getColumnClass(col) == ScilabTreeTableModel.class && SwingUtilities.isLeftMouseButton(e)) {
176                     MouseEvent me = e;
177                     if (isLocationInExpandControl != null) {
178                         try {
179                             int row = rowAtPoint(p);
180                             TreePath path = tree.getPathForRow(row);
181                             boolean isOnExpander = ((Boolean) isLocationInExpandControl.invoke(tree.getUI(), path, e.getX(), e.getY())).booleanValue();
182                             Rectangle r = tree.getRowBounds(row);
183                             if (!isOnExpander && !r.contains(p)) {
184                                 me = new MouseEvent((Component) e.getSource(), e.getID(), e.getWhen(), e.getModifiers(), r.x, r.y, e.getClickCount(), e.isPopupTrigger());
185                             }
186                         } catch (Exception ex) { }
187                     }
188                     tree.dispatchEvent(me);
189                 }
190             }
191         });
192
193         addKeyListener(new KeyAdapter() {
194             public void keyTyped(KeyEvent e) {
195                 char c = e.getKeyChar();
196                 if (Character.isLetter(c)) {
197                     int step = 1;
198                     if (Character.isUpperCase(c)) {
199                         step = -1;
200                     }
201                     c = Character.toLowerCase(c);
202                     int[] rows = getSelectedRows();
203                     int count = getRowCount();
204                     int start = 0;
205                     if (rows != null && rows.length != 0) {
206                         start = modulo(rows[0] + step, count);
207                     }
208                     for (int i = start; i != start - step; i = modulo(i + step, count)) {
209                         char first = ((FileNode) tree.getPathForRow(i).getLastPathComponent()).toString().charAt(0);
210                         first = Character.toLowerCase(first);
211                         if (first == c) {
212                             scrollRectToVisible(tree.getRowBounds(i));
213                             setRowSelectionInterval(i, i);
214                             break;
215                         }
216                     }
217                 }
218             }
219         });
220
221
222         initActions();
223         setComponentPopupMenu(createPopup());
224     }
225
226     /**
227      * @return the Next button used in history
228      */
229     public JButton getNextButton() {
230         return history.getNextButton();
231     }
232
233     /**
234      * @return the Previous button used in history
235      */
236     public JButton getPreviousButton() {
237         return history.getPreviousButton();
238     }
239
240     /**
241      * @return the combobox used to set the path
242      */
243     public ScilabFileSelectorComboBox getComboBox() {
244         return combobox;
245     }
246
247     /**
248      * Get the selected rows as file path
249      * @return the paths
250      */
251     public String[] getSelectedPaths() {
252         int[] rows = getSelectedRows();
253         String[] paths = new String[rows.length];
254         for (int i = 0; i < rows.length; i++) {
255             TreePath path = tree.getPathForRow(rows[i]);
256             FileNode fn = (FileNode) path.getLastPathComponent();
257             paths[i] = fn.getFile().getAbsolutePath();
258         }
259
260         return paths;
261     }
262
263     /**
264      * Get the selected rows as file
265      * @return the paths
266      */
267     public File[] getSelectedFiles() {
268         int[] rows = getSelectedRows();
269         File[] files = new File[rows.length];
270         for (int i = 0; i < rows.length; i++) {
271             TreePath path = tree.getPathForRow(rows[i]);
272             FileNode fn = (FileNode) path.getLastPathComponent();
273             files[i] = fn.getFile();
274         }
275
276         return files;
277     }
278
279     /**
280      * {@inheritDoc}
281      */
282     public int getRowHeight(int row) {
283         return getRowHeight();
284     }
285
286     /**
287      * {@inheritDoc}
288      */
289     public boolean isOpaque() {
290         return false;
291     }
292
293     /**
294      * Set the base directory
295      * @param baseDir the base directory
296      */
297     public void setBaseDir(String baseDir) {
298         setBaseDir(baseDir, true);
299     }
300
301     /**
302      * Set the base directory
303      * @param baseDir the base directory
304      * @param addInHistory if true the dir is add in the history
305      */
306     public synchronized void setBaseDir(String baseDir, boolean addInHistory) {
307         boolean cancelled = false;
308         ScilabFileBrowserModel model;
309         if (dirRefresher != null) {
310             dirRefresher.cancel(true);
311             dirRefresher = null;
312             model = this.model;
313             this.model = null;
314             cancelled = true;
315         } else {
316             model = (ScilabFileBrowserModel) tree.getModel();
317         }
318         combobox.setBaseDir(baseDir);
319         if (model != null) {
320             File f = new File(baseDir);
321             if (cancelled || (f.exists() && f.isDirectory() && f.canRead())) {
322                 boolean sameDir = baseDir.equals(model.getBaseDir());
323                 tree.setModel(null);
324                 if (!sameDir && addInHistory) {
325                     history.addPathInHistory(baseDir);
326                 }
327                 resetScrollBar = !sameDir;
328                 model.setBaseDir(baseDir, this);
329             }
330         }
331     }
332
333     /**
334      * Set the file filter to use in table
335      * @param pat the pattern to use
336      */
337     public void setFilter(Pattern pat) {
338         ScilabFileBrowserModel model = (ScilabFileBrowserModel) tree.getModel();
339         TreePath rootPath = new TreePath(model.getRoot());
340         tree.setModel(null);
341         model.setFilter(pat);
342         reload(model);
343     }
344
345     /**
346      * Reload the table
347      */
348     public void reload(ScilabFileBrowserModel model) {
349         tree.setModel(model);
350         tree.setRowHeight(getRowHeight());
351         tree.setLargeModel(true);
352         TreePath path = new TreePath(model.getRoot());
353         tree.collapsePath(path);
354         if (resetScrollBar) {
355             ((JScrollPane) SwingUtilities.getAncestorOfClass(JScrollPane.class, this)).getVerticalScrollBar().setValue(0);
356         }
357         resetScrollBar = true;
358         tree.expandPath(path);
359         if (getRowCount() >= 1) {
360             repaint(tree.getRowBounds(0));
361         }
362         editingRow = 0;
363     }
364
365     /* Workaround for BasicTableUI anomaly. Make sure the UI never tries to
366      * paint the editor. The UI currently uses different techniques to
367      * paint the renderers and editors and overriding setBounds() below
368      * is not the right thing to do for an editor. Returning -1 for the
369      * editing row in this case, ensures the editor is never painted.
370      */
371     public int getEditingRow() {
372         if (getColumnClass(editingColumn) == ScilabTreeTableModel.class) {
373             return -1;
374         } else {
375             return editingRow;
376         }
377     }
378
379     /**
380      * Init the actions
381      */
382     private void initActions() {
383         final ActionMap actions = getActionMap();
384         actions.put("scinotes", new OpenFileInSciNotesAction(this));
385         actions.put("xcos", new ExecuteFileInXcosAction(this));
386         actions.put("mat", new ExecuteMatFileAction(this));
387         actions.put("console", new ExecuteFileInConsoleAction(this));
388         actions.put("load", new LoadFileInScilabAction(this));
389         actions.put("graph", new LoadFileAsGraphAction(this));
390         actions.put("cwd", new ChangeCWDAction(this));
391         if (EditFileWithDefaultAppAction.isSupported()) {
392             actions.put("edit", new EditFileWithDefaultAppAction(this));
393         }
394         if (OpenFileWithDefaultAppAction.isSupported()) {
395             actions.put("open", new OpenFileWithDefaultAppAction(this));
396         }
397         actions.put("validate", new ValidateAction(this));
398         actions.put("validateorexpand", new CommonCallBack(null) {
399             public void callBack() {
400                 int[] rows = getSelectedRows();
401                 if (rows != null && rows.length != 0) {
402                     TreePath path = tree.getPathForRow(rows[0]);
403                     FileNode fn = (FileNode) path.getLastPathComponent();
404                     if (fn.isLeaf()) {
405                         ((CommonCallBack) actions.get("validate")).callBack();
406                     } else {
407                         if (tree.isExpanded(path)) {
408                             tree.collapsePath(path);
409                         } else {
410                             tree.expandPath(path);
411                         }
412                         setRowSelectionInterval(rows[0], rows[0]);
413                     }
414                 }
415             }
416         });
417
418         combobox.setAction((CommonCallBack) actions.get("cwd"));
419         InputMap map = getInputMap();
420         map.put(KeyStroke.getKeyStroke("ENTER"), "validateorexpand");
421     }
422
423     /**
424      * Create the popup menu
425      */
426     private JPopupMenu createPopup() {
427         ActionMap actions = getActionMap();
428         JPopupMenu popup = new JPopupMenu();
429         JMenuItem item = new JMenuItem(UiDataMessages.OPENINSCINOTES);
430         item.addActionListener(actions.get("scinotes"));
431         item.setIcon(new ImageIcon(FindIconHelper.findIcon("accessories-text-editor")));
432         popup.add(item);
433
434         item = new JMenuItem(UiDataMessages.EXECINCONSOLE);
435         item.addActionListener(actions.get("console"));
436         item.setIcon(new ImageIcon(FindIconHelper.findIcon("media-playback-start")));
437         popup.add(item);
438
439         item = new JMenuItem(UiDataMessages.OPENINXCOS);
440         item.addActionListener(actions.get("xcos"));
441         item.setIcon(new ImageIcon(FindIconHelper.findIcon("utilities-system-monitor")));
442         popup.add(item);
443
444         item = new JMenuItem(UiDataMessages.LOADINSCILAB);
445         item.addActionListener(actions.get("load"));
446         item.setIcon(new ImageIcon(FindIconHelper.findIcon("scilab")));
447         popup.add(item);
448
449         if (actions.get("edit") != null || actions.get("open") != null) {
450             popup.addSeparator();
451         }
452
453         if (actions.get("edit") != null) {
454             item = new JMenuItem(UiDataMessages.EDITWITHDEFAULT);
455             item.addActionListener(actions.get("edit"));
456             popup.add(item);
457         }
458
459         if (actions.get("open") != null) {
460             item = new JMenuItem(UiDataMessages.OPENWITHDEFAULT);
461             item.addActionListener(actions.get("open"));
462             popup.add(item);
463         }
464
465         popup.pack();
466
467         return popup;
468     }
469
470
471     public synchronized void setDirRefresher(SwingWorker refresher, ScilabFileBrowserModel model) {
472         dirRefresher = refresher;
473         this.model = model;
474     }
475
476     /**
477      * A modulo for negative numbers
478      * @param n an int
479      * @param p another int
480      * @return n modulo p
481      */
482     private static final int modulo(int n, int p) {
483         if (n >= 0) {
484             return n % p;
485         }
486         return p - (-n % p);
487     }
488
489 }