From ea605e3b82f5dfce473137425aefe7821cd01d77 Mon Sep 17 00:00:00 2001 From: tferr Date: Sat, 19 Feb 2022 15:13:54 -0500 Subject: [PATCH 01/21] Add option for Ctrl+Space triggering auto-completion As per https://github.com/scijava/script-editor-jython/issues/8 --- .../org/scijava/ui/swing/script/EditorPane.java | 8 ++++++++ .../org/scijava/ui/swing/script/TextEditor.java | 15 ++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/scijava/ui/swing/script/EditorPane.java b/src/main/java/org/scijava/ui/swing/script/EditorPane.java index 7397b2bd..19264749 100644 --- a/src/main/java/org/scijava/ui/swing/script/EditorPane.java +++ b/src/main/java/org/scijava/ui/swing/script/EditorPane.java @@ -523,6 +523,14 @@ public void setAutoCompletionEnabled(boolean value) { setLanguage(currentLanguage); } + private boolean autoCompletionKeyRequired = false; + void setAutoCompletionKeyRequired(boolean value) { + autoCompletionKeyRequired = value; + } + public boolean getAutoCompletionKeyRequired() { + return autoCompletionKeyRequired; + } + /** * Get file currently open in this {@link EditorPane}. * diff --git a/src/main/java/org/scijava/ui/swing/script/TextEditor.java b/src/main/java/org/scijava/ui/swing/script/TextEditor.java index d3063746..d1b8d71b 100644 --- a/src/main/java/org/scijava/ui/swing/script/TextEditor.java +++ b/src/main/java/org/scijava/ui/swing/script/TextEditor.java @@ -218,7 +218,8 @@ public class TextEditor extends JFrame implements ActionListener, openMacroFunctions, decreaseFontSize, increaseFontSize, chooseFontSize, chooseTabSize, gitGrep, replaceTabsWithSpaces, replaceSpacesWithTabs, toggleWhiteSpaceLabeling, zapGremlins, - savePreferences, toggleAutoCompletionMenu, openClassOrPackageHelp; + savePreferences, toggleAutoCompletionMenu, toggleAutoCompletionKeyRequired, + openClassOrPackageHelp; private RecentFilesMenuItem openRecent; private JMenu gitMenu, tabsMenu, fontSizeMenu, tabSizeMenu, toolsMenu, runMenu, whiteSpaceMenu; @@ -605,6 +606,10 @@ public TextEditor(final Context context) { toggleAutoCompletionMenu.setSelected(prefService.getBoolean(TextEditor.class, "autoComplete", true)); toggleAutoCompletionMenu.addChangeListener(e -> toggleAutoCompletion()); options.add(toggleAutoCompletionMenu); + toggleAutoCompletionKeyRequired = new JCheckBoxMenuItem("Auto-completion Requires Ctrl+Space"); + toggleAutoCompletionKeyRequired.setSelected(prefService.getBoolean(TextEditor.class, "autoCompleteKeyRequired", true)); + toggleAutoCompletionKeyRequired.addChangeListener(e -> toggleAutoCompletionKeyRequired()); + options.add(toggleAutoCompletionKeyRequired); options.addSeparator(); savePreferences = addToMenu(options, "Save Preferences", 0, 0); @@ -1468,6 +1473,14 @@ private void toggleAutoCompletion() { prefService.put(TextEditor.class, "autoComplete", toggleAutoCompletionMenu.isSelected()); } + private void toggleAutoCompletionKeyRequired() { + for (int i = 0; i < tabbed.getTabCount(); i++) { + final EditorPane editorPane = getEditorPane(i); + editorPane.setAutoCompletionEnabled(toggleAutoCompletionKeyRequired.isSelected()); + } + prefService.put(TextEditor.class, "autoCompleteKeyRequired", toggleAutoCompletionKeyRequired.isSelected()); + } + protected boolean handleTabsMenu(final Object source) { if (!(source instanceof JMenuItem)) return false; final JMenuItem item = (JMenuItem) source; From f2e4afd6fde08bf5393592bde03d4c8063596b14 Mon Sep 17 00:00:00 2001 From: tferr Date: Sun, 20 Feb 2022 22:54:03 -0500 Subject: [PATCH 02/21] GUI: Minor fixes - Improve Consistency of Title Case - Add titled separators to larger menus - errorScreen: Adopt pane font size, not hardwired value - Bookmarks: Info message when list is empty - Disable 'clear' menu entry, redundant with JButton of 'errorScreen' --- .../scijava/ui/swing/script/TextEditor.java | 81 ++++++++++++------- .../ui/swing/script/TextEditorTab.java | 4 +- 2 files changed, 55 insertions(+), 30 deletions(-) diff --git a/src/main/java/org/scijava/ui/swing/script/TextEditor.java b/src/main/java/org/scijava/ui/swing/script/TextEditor.java index d1b8d71b..177bc875 100644 --- a/src/main/java/org/scijava/ui/swing/script/TextEditor.java +++ b/src/main/java/org/scijava/ui/swing/script/TextEditor.java @@ -32,7 +32,6 @@ import java.awt.Color; import java.awt.Cursor; import java.awt.Dimension; -import java.awt.Font; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Toolkit; @@ -123,6 +122,7 @@ import javax.swing.JTextField; import javax.swing.KeyStroke; import javax.swing.SwingUtilities; +import javax.swing.UIManager; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.DocumentEvent; @@ -212,7 +212,8 @@ public class TextEditor extends JFrame implements ActionListener, close, undo, redo, cut, copy, paste, find, replace, selectAll, kill, gotoLine, makeJar, makeJarWithSource, removeUnusedImports, sortImports, removeTrailingWhitespace, findNext, findPrevious, openHelp, addImport, - clearScreen, nextError, previousError, openHelpWithoutFrames, nextTab, + //clearScreen, redundant with "Clear" JButton + nextError, previousError, openHelpWithoutFrames, nextTab, previousTab, runSelection, extractSourceJar, toggleBookmark, listBookmarks, openSourceForClass, openSourceForMenuItem, openMacroFunctions, decreaseFontSize, increaseFontSize, chooseFontSize, @@ -305,14 +306,15 @@ public TextEditor(final Context context) { openRecent = new RecentFilesMenuItem(prefService, this); openRecent.setMnemonic(KeyEvent.VK_R); file.add(openRecent); + file.addSeparator(); save = addToMenu(file, "Save", KeyEvent.VK_S, ctrl); save.setMnemonic(KeyEvent.VK_S); - saveas = addToMenu(file, "Save as...", 0, 0); + saveas = addToMenu(file, "Save As...", 0, 0); saveas.setMnemonic(KeyEvent.VK_A); file.addSeparator(); - makeJar = addToMenu(file, "Export as .jar", 0, 0); + makeJar = addToMenu(file, "Export as JAR", 0, 0); makeJar.setMnemonic(KeyEvent.VK_E); - makeJarWithSource = addToMenu(file, "Export as .jar (with source)", 0, 0); + makeJarWithSource = addToMenu(file, "Export as JAR (With Source)", 0, 0); makeJarWithSource.setMnemonic(KeyEvent.VK_X); file.addSeparator(); close = addToMenu(file, "Close", KeyEvent.VK_W, ctrl); @@ -330,7 +332,7 @@ public TextEditor(final Context context) { cut = addToMenu(edit, "Cut", KeyEvent.VK_X, ctrl); copy = addToMenu(edit, "Copy", KeyEvent.VK_C, ctrl); paste = addToMenu(edit, "Paste", KeyEvent.VK_V, ctrl); - edit.addSeparator(); + addSeparator(edit, "Find:"); find = addToMenu(edit, "Find...", KeyEvent.VK_F, ctrl); find.setMnemonic(KeyEvent.VK_F); findNext = addToMenu(edit, "Find Next", KeyEvent.VK_F3, 0); @@ -338,16 +340,18 @@ public TextEditor(final Context context) { findPrevious = addToMenu(edit, "Find Previous", KeyEvent.VK_F3, shift); findPrevious.setMnemonic(KeyEvent.VK_P); replace = addToMenu(edit, "Find and Replace...", KeyEvent.VK_H, ctrl); - gotoLine = addToMenu(edit, "Goto line...", KeyEvent.VK_G, ctrl); + + addSeparator(edit, "Goto:"); + gotoLine = addToMenu(edit, "Goto Line...", KeyEvent.VK_G, ctrl); gotoLine.setMnemonic(KeyEvent.VK_G); toggleBookmark = addToMenu(edit, "Toggle Bookmark", KeyEvent.VK_B, ctrl); toggleBookmark.setMnemonic(KeyEvent.VK_B); - listBookmarks = addToMenu(edit, "List Bookmarks", 0, 0); + listBookmarks = addToMenu(edit, "List Bookmarks...", 0, 0); listBookmarks.setMnemonic(KeyEvent.VK_O); - edit.addSeparator(); - clearScreen = addToMenu(edit, "Clear output panel", 0, 0); - clearScreen.setMnemonic(KeyEvent.VK_L); + // This is redundant with 'Clear' JButton +// clearScreen = addToMenu(edit, "Clear output panel", 0, 0); +// clearScreen.setMnemonic(KeyEvent.VK_L); zapGremlins = addToMenu(edit, "Zap Gremlins", 0, 0); @@ -456,12 +460,12 @@ public TextEditor(final Context context) { compileAndRun.setMnemonic(KeyEvent.VK_R); runSelection = - addToMenu(runMenu, "Run selected code", KeyEvent.VK_R, ctrl | shift); + addToMenu(runMenu, "Run Selected Code", KeyEvent.VK_R, ctrl | shift); runSelection.setMnemonic(KeyEvent.VK_S); compile = addToMenu(runMenu, "Compile", KeyEvent.VK_C, ctrl | shift); compile.setMnemonic(KeyEvent.VK_C); - autoSave = new JCheckBoxMenuItem("Auto-save before compiling"); + autoSave = new JCheckBoxMenuItem("Auto-save Before Compiling"); runMenu.add(autoSave); runMenu.addSeparator(); @@ -472,7 +476,7 @@ public TextEditor(final Context context) { runMenu.addSeparator(); - kill = addToMenu(runMenu, "Kill running script...", 0, 0); + kill = addToMenu(runMenu, "Kill Running Script...", 0, 0); kill.setMnemonic(KeyEvent.VK_K); kill.setEnabled(false); @@ -539,14 +543,15 @@ public TextEditor(final Context context) { options.setMnemonic(KeyEvent.VK_O); // Font adjustments + addSeparator(options, "Font:"); decreaseFontSize = - addToMenu(options, "Decrease font size", KeyEvent.VK_MINUS, ctrl); + addToMenu(options, "Decrease Font Size", KeyEvent.VK_MINUS, ctrl); decreaseFontSize.setMnemonic(KeyEvent.VK_D); increaseFontSize = - addToMenu(options, "Increase font size", KeyEvent.VK_PLUS, ctrl); + addToMenu(options, "Increase Font Size", KeyEvent.VK_PLUS, ctrl); increaseFontSize.setMnemonic(KeyEvent.VK_C); - fontSizeMenu = new JMenu("Font sizes"); + fontSizeMenu = new JMenu("Font Size"); fontSizeMenu.setMnemonic(KeyEvent.VK_Z); final boolean[] fontSizeShortcutUsed = new boolean[10]; final ButtonGroup buttonGroup = new ButtonGroup(); @@ -828,8 +833,7 @@ public void windowGainedFocus(final WindowEvent e) { } }); - final Font font = new Font("Courier", Font.PLAIN, 12); - errorScreen.setFont(font); + errorScreen.setFont(getEditorPane().getFont()); errorScreen.setEditable(false); errorScreen.setLineWrap(true); @@ -1397,13 +1401,13 @@ else if (source == replaceTabsWithSpaces) getTextArea() .convertTabsToSpaces(); else if (source == replaceSpacesWithTabs) getTextArea() .convertSpacesToTabs(); - else if (source == clearScreen) { - getTab().getScreen().setText(""); - } +// else if (source == clearScreen) { +// getTab().getScreen().setText(""); +// } else if (source == zapGremlins) zapGremlins(); - else if (source == toggleAutoCompletionMenu) { - toggleAutoCompletion(); - } +// else if (source == toggleAutoCompletionMenu) { +// toggleAutoCompletion(); +// } else if (source == savePreferences) { getEditorPane().savePreferences(tree.getTopLevelFoldersString()); } @@ -1558,9 +1562,11 @@ public void listBookmarks() { final TextEditorTab tab = (TextEditorTab) tabbed.getComponentAt(i); tab.editorPane.getBookmarks(tab, bookmarks); } - - final BookmarkDialog dialog = new BookmarkDialog(this, bookmarks); - dialog.setVisible(true); + if (bookmarks.isEmpty()) { + JOptionPane.showMessageDialog(this, "No Bookmarks currently exist."); + } else { + new BookmarkDialog(this, bookmarks).setVisible(true); + } } public boolean reload() { @@ -3134,4 +3140,23 @@ public void setFontSize(final float size) { private void changeFontSize(final JTextArea a, final float size) { a.setFont(a.getFont().deriveFont(size)); } + + private static void addSeparator(final JMenu menu, final String header) { + final JLabel label = new JLabel(header); + // label.setHorizontalAlignment(SwingConstants.LEFT); + label.setEnabled(false); + label.setForeground(getDisabledComponentColor()); + if (menu.getMenuComponentCount() > 1) { + menu.addSeparator(); + } + menu.add(label); + } + + private static Color getDisabledComponentColor() { + try { + return UIManager.getColor("MenuItem.disabledForeground"); + } catch (final Exception ignored) { + return Color.GRAY; + } + } } diff --git a/src/main/java/org/scijava/ui/swing/script/TextEditorTab.java b/src/main/java/org/scijava/ui/swing/script/TextEditorTab.java index 5fdfe345..7a1d31d4 100644 --- a/src/main/java/org/scijava/ui/swing/script/TextEditorTab.java +++ b/src/main/java/org/scijava/ui/swing/script/TextEditorTab.java @@ -188,7 +188,7 @@ public void actionPerformed(final ActionEvent ae) { bottom.add(killit, bc); bc.gridx = 3; - incremental = new JCheckBox("persistent"); + incremental = new JCheckBox("Persistent"); incremental.setEnabled(true); incremental.setSelected(false); bottom.add(incremental, bc); @@ -291,7 +291,7 @@ public void actionPerformed(ActionEvent a) { + "and shift+up/down arrow work like arrow keys before for caret movement\n" + "within a multi-line prompt." ; - JOptionPane.showMessageDialog(textEditor, msg, "REPL help", JOptionPane.INFORMATION_MESSAGE); + JOptionPane.showMessageDialog(textEditor, msg, "REPL Help", JOptionPane.INFORMATION_MESSAGE); } }); prompt_panel.add(prompt_help, bc); From 59c408c06d5a4f3c3e985c5a5db338a7efdd8f9b Mon Sep 17 00:00:00 2001 From: tferr Date: Sun, 20 Feb 2022 23:05:36 -0500 Subject: [PATCH 03/21] Improvements to File Tree - Contextual Menu: (Collapse, Expand, Reveal Selection in OS, Reset) - Add button: prompt remembers last folder and accepts drag & drop - Drag & drop support from the OS' File Explorer - Fix hardwired colors for compatibility with dark L&F - Minor Improvements to dialogs - Do not collapse nodes when reloading - File filtering: - Help message in tooltip - Case insensitive by default - Clearing field resets tree --- .../org/scijava/ui/swing/script/FileDrop.java | 992 ++++++++++++++++++ .../ui/swing/script/FileSystemTree.java | 29 +- .../scijava/ui/swing/script/TextEditor.java | 124 ++- 3 files changed, 1117 insertions(+), 28 deletions(-) create mode 100644 src/main/java/org/scijava/ui/swing/script/FileDrop.java diff --git a/src/main/java/org/scijava/ui/swing/script/FileDrop.java b/src/main/java/org/scijava/ui/swing/script/FileDrop.java new file mode 100644 index 00000000..8448660e --- /dev/null +++ b/src/main/java/org/scijava/ui/swing/script/FileDrop.java @@ -0,0 +1,992 @@ +/* + * #%L + * Script Editor and Interpreter for SciJava script languages. + * %% + * Copyright (C) 2009 - 2022 SciJava developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +package org.scijava.ui.swing.script; + +import java.awt.datatransfer.DataFlavor; +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; +import java.io.Reader; + +import javax.swing.UIManager; + + +/** + * This class makes it easy to drag and drop files from the operating system to + * a Java program. Any java.awt.Component can be dropped onto, but only + * javax.swing.JComponents will indicate the drop event with a changed + * border. + *

+ * To use this class, construct a new FileDrop by passing it the target + * component and a Listener to receive notification when file(s) have + * been dropped. Here is an example: + *

+ * + *
+ *      JPanel myPanel = new JPanel();
+ *      new FileDrop( myPanel, new FileDrop.Listener()
+ *      {   public void filesDropped( java.io.File[] files )
+ *          {   
+ *              // handle file drop
+ *              ...
+ *          }   // end filesDropped
+ *      }); // end FileDrop.Listener
+ * 
+ *

+ * You can specify the border that will appear when files are being dragged by + * calling the constructor with a javax.swing.border.Border. Only + * JComponents will show any indication with a border. + *

+ *

+ * You can turn on some debugging features by passing a PrintStream + * object (such as System.out) into the full constructor. A + * null value will result in no extra debugging information being + * output. + *

+ * + *

+ * I'm releasing this code into the Public Domain. Enjoy. + *

+ *

+ * Original author: Robert Harder, rob@iharder.net + *

+ *

+ * Additional support: + *

+ *
    + *
  • September 2007, Nathan Blomquist -- Linux (KDE/Gnome) support added.
  • + *
  • December 2010, Joshua Gerth
  • + *
  • June 2019, TF, Adjust defaultBorderColor. Code cleanup. Added the ability + * to abort drop operation using Esc + *
+ * + * @author Robert Harder + * @version 1.1.1 + */ +public class FileDrop { + private transient javax.swing.border.Border normalBorder; + private transient java.awt.dnd.DropTargetListener dropListener; + + /** Discover if the running JVM is modern enough to have drag and drop. */ + private static Boolean supportsDnD; + + // Default border color + private static java.awt.Color defaultBorderColor = UIManager.getColor("Tree.selectionBackground"); + static { + if (defaultBorderColor == null) defaultBorderColor = new java.awt.Color(0f,0f, 1f, 0.25f); + } + + /** + * Constructs a {@link FileDrop} with a default light-blue border and, if + * c is a {@link java.awt.Container}, recursively sets all + * elements contained within as drop targets, though only the top level + * container will change borders. + * + * @param c + * Component on which files will be dropped. + * @param listener + * Listens for filesDropped. + * @since 1.0 + */ + public FileDrop(final java.awt.Component c, final Listener listener) { + this(null, // Logging stream + c, // Drop target + javax.swing.BorderFactory.createMatteBorder(2, 2, 2, 2, + defaultBorderColor), // Drag border + true, // Recursive + listener); + } // end constructor + + /** + * Constructor with a default border and the option to recursively set drop + * targets. If your component is a java.awt.Container, then each of + * its children components will also listen for drops, though only the + * parent will change borders. + * + * @param c + * Component on which files will be dropped. + * @param recursive + * Recursively set children as drop targets. + * @param listener + * Listens for filesDropped. + * @since 1.0 + */ + protected FileDrop(final java.awt.Component c, final boolean recursive, + final Listener listener) { + this(null, // Logging stream + c, // Drop target + javax.swing.BorderFactory.createMatteBorder(2, 2, 2, 2, + defaultBorderColor), // Drag border + recursive, // Recursive + listener); + } // end constructor + + /** + * Constructor with a default border and debugging optionally turned on. + * With Debugging turned on, more status messages will be displayed to + * out. A common way to use this constructor is with + * System.out or System.err. A null value for the + * parameter out will result in no debugging output. + * + * @param out + * PrintStream to record debugging info or null for no debugging. + * @param c + * Component on which files will be dropped. + * @param listener + * Listens for filesDropped. + * @since 1.0 + */ + protected FileDrop(final java.io.PrintStream out, final java.awt.Component c, + final Listener listener) { + this(out, // Logging stream + c, // Drop target + javax.swing.BorderFactory.createMatteBorder(2, 2, 2, 2, + defaultBorderColor), false, // Recursive + listener); + } // end constructor + + /** + * Constructor with a default border, debugging optionally turned on and the + * option to recursively set drop targets. If your component is a + * java.awt.Container, then each of its children components will + * also listen for drops, though only the parent will change borders. With + * Debugging turned on, more status messages will be displayed to + * out. A common way to use this constructor is with + * System.out or System.err. A null value for the + * parameter out will result in no debugging output. + * + * @param out + * PrintStream to record debugging info or null for no debugging. + * @param c + * Component on which files will be dropped. + * @param recursive + * Recursively set children as drop targets. + * @param listener + * Listens for filesDropped. + * @since 1.0 + */ + protected FileDrop(final java.io.PrintStream out, final java.awt.Component c, + final boolean recursive, final Listener listener) { + this(out, // Logging stream + c, // Drop target + javax.swing.BorderFactory.createMatteBorder(2, 2, 2, 2, + defaultBorderColor), // Drag border + recursive, // Recursive + listener); + } // end constructor + + /** + * Constructor with a specified border + * + * @param c + * Component on which files will be dropped. + * @param dragBorder + * Border to use on JComponent when dragging occurs. + * @param listener + * Listens for filesDropped. + * @since 1.0 + */ + protected FileDrop(final java.awt.Component c, + final javax.swing.border.Border dragBorder, final Listener listener) { + this(null, // Logging stream + c, // Drop target + dragBorder, // Drag border + false, // Recursive + listener); + } // end constructor + + /** + * Constructor with a specified border and the option to recursively set + * drop targets. If your component is a java.awt.Container, then + * each of its children components will also listen for drops, though only + * the parent will change borders. + * + * @param c + * Component on which files will be dropped. + * @param dragBorder + * Border to use on JComponent when dragging occurs. + * @param recursive + * Recursively set children as drop targets. + * @param listener + * Listens for filesDropped. + * @since 1.0 + */ + protected FileDrop(final java.awt.Component c, + final javax.swing.border.Border dragBorder, + final boolean recursive, final Listener listener) { + this(null, c, dragBorder, recursive, listener); + } // end constructor + + /** + * Constructor with a specified border and debugging optionally turned on. + * With Debugging turned on, more status messages will be displayed to + * out. A common way to use this constructor is with + * System.out or System.err. A null value for the + * parameter out will result in no debugging output. + * + * @param out + * PrintStream to record debugging info or null for no debugging. + * @param c + * Component on which files will be dropped. + * @param dragBorder + * Border to use on JComponent when dragging occurs. + * @param listener + * Listens for filesDropped. + * @since 1.0 + */ + protected FileDrop(final java.io.PrintStream out, final java.awt.Component c, + final javax.swing.border.Border dragBorder, final Listener listener) { + this(out, // Logging stream + c, // Drop target + dragBorder, // Drag border + false, // Recursive + listener); + } // end constructor + + /** + * Full constructor with a specified border and debugging optionally turned + * on. With Debugging turned on, more status messages will be displayed to + * out. A common way to use this constructor is with + * System.out or System.err. A null value for the + * parameter out will result in no debugging output. + * + * @param out + * PrintStream to record debugging info or null for no debugging. + * @param c + * Component on which files will be dropped. + * @param dragBorder + * Border to use on JComponent when dragging occurs. + * @param recursive + * Recursively set children as drop targets. + * @param listener + * Listens for filesDropped. + * @since 1.0 + */ + protected FileDrop(final java.io.PrintStream out, final java.awt.Component c, + final javax.swing.border.Border dragBorder, + final boolean recursive, final Listener listener) { + + if (supportsDnD()) { // Make a drop listener + dropListener = new java.awt.dnd.DropTargetListener() { + @Override + public void dragEnter(final java.awt.dnd.DropTargetDragEvent evt) { + log(out, "FileDrop: dragEnter event."); + + // Is this an acceptable drag event? + if (isDragOk(out, evt) && c.isEnabled()) { + // If it's a Swing component, set its border + if (c instanceof javax.swing.JComponent) { + final javax.swing.JComponent jc = (javax.swing.JComponent) c; + if (normalBorder == null) { + normalBorder = jc.getBorder(); + } // end if: border not yet saved + log(out, "FileDrop: normal border saved."); + jc.setBorder(dragBorder); + log(out, "FileDrop: drag border set."); + } // end if: JComponent + + // Acknowledge that it's okay to enter + // evt.acceptDrag( + // java.awt.dnd.DnDConstants.ACTION_COPY_OR_MOVE ); + evt.acceptDrag(java.awt.dnd.DnDConstants.ACTION_COPY); + log(out, "FileDrop: event accepted."); + } // end if: drag ok + else { // Reject the drag event + evt.rejectDrag(); + log(out, "FileDrop: event rejected."); + } // end else: drag not ok + } // end dragEnter + + @Override + public void dragOver(final java.awt.dnd.DropTargetDragEvent evt) { // This + // is + // called + // continually + // as + // long + // as + // the + // mouse + // is + // over + // the + // drag + // target. + } // end dragOver + + @Override + public void drop(final java.awt.dnd.DropTargetDropEvent evt) { + log(out, "FileDrop: drop event."); + try { // Get whatever was dropped + final java.awt.datatransfer.Transferable tr = evt + .getTransferable(); + + // Is it a file list? + if (tr.isDataFlavorSupported(java.awt.datatransfer.DataFlavor.javaFileListFlavor)) { + // Say we'll take it. + // evt.acceptDrop ( + // java.awt.dnd.DnDConstants.ACTION_COPY_OR_MOVE ); + evt.acceptDrop(java.awt.dnd.DnDConstants.ACTION_COPY); + log(out, "FileDrop: file list accepted."); + + // Get a useful list + final java.util.List fileList = (java.util.List) tr + .getTransferData(java.awt.datatransfer.DataFlavor.javaFileListFlavor); + //final java.util.Iterator iterator = fileList.iterator(); + + // Convert list to array + final java.io.File[] filesTemp = new java.io.File[fileList + .size()]; + fileList.toArray(filesTemp); + final java.io.File[] files = filesTemp; + + // Alert listener to drop. + if (listener != null) + listener.filesDropped(files); + + // Mark that drop is completed. + evt.getDropTargetContext().dropComplete(true); + log(out, "FileDrop: drop complete."); + } // end if: file list + else // this section will check for a reader flavor. + { + // Thanks, Nathan! + // BEGIN 2007-09-12 Nathan Blomquist -- Linux + // (KDE/Gnome) support added. + final DataFlavor[] flavors = tr.getTransferDataFlavors(); + boolean handled = false; + for (int zz = 0; zz < flavors.length; zz++) { + if (flavors[zz].isRepresentationClassReader()) { + // Say we'll take it. + // evt.acceptDrop ( + // java.awt.dnd.DnDConstants.ACTION_COPY_OR_MOVE + // ); + evt.acceptDrop(java.awt.dnd.DnDConstants.ACTION_COPY); + log(out, "FileDrop: reader accepted."); + + final Reader reader = flavors[zz] + .getReaderForText(tr); + + final BufferedReader br = new BufferedReader( + reader); + + if (listener != null) + listener.filesDropped(createFileArray( + br, out)); + + // Mark that drop is completed. + evt.getDropTargetContext().dropComplete( + true); + log(out, "FileDrop: drop complete."); + handled = true; + break; + } + } + if (!handled) { + log(out, + "FileDrop: not a file list or reader - abort."); + evt.rejectDrop(); + } + // END 2007-09-12 Nathan Blomquist -- Linux + // (KDE/Gnome) support added. + } // end else: not a file list + } // end try + catch (final java.io.IOException io) { + log(out, "FileDrop: IOException - abort:"); + io.printStackTrace(out); + evt.rejectDrop(); + } // end catch IOException + catch (final java.awt.datatransfer.UnsupportedFlavorException ufe) { + log(out, + "FileDrop: UnsupportedFlavorException - abort:"); + ufe.printStackTrace(out); + evt.rejectDrop(); + } // end catch: UnsupportedFlavorException + finally { + // If it's a Swing component, reset its border + if (c instanceof javax.swing.JComponent) { + final javax.swing.JComponent jc = (javax.swing.JComponent) c; + jc.setBorder(normalBorder); + log(out, "FileDrop: normal border restored."); + } // end if: JComponent + } // end finally + } // end drop + + @Override + public void dragExit(final java.awt.dnd.DropTargetEvent evt) { + log(out, "FileDrop: dragExit event."); + // If it's a Swing component, reset its border + if (c instanceof javax.swing.JComponent) { + final javax.swing.JComponent jc = (javax.swing.JComponent) c; + jc.setBorder(normalBorder); + log(out, "FileDrop: normal border restored."); + } // end if: JComponent + } // end dragExit + + @Override + public void dropActionChanged( + final java.awt.dnd.DropTargetDragEvent evt) { + log(out, "FileDrop: dropActionChanged event."); + // Is this an acceptable drag event? + if (isDragOk(out, evt)) { // evt.acceptDrag( + // java.awt.dnd.DnDConstants.ACTION_COPY_OR_MOVE + // ); + evt.acceptDrag(java.awt.dnd.DnDConstants.ACTION_COPY); + log(out, "FileDrop: event accepted."); + } // end if: drag ok + else { + evt.rejectDrag(); + log(out, "FileDrop: event rejected."); + } // end else: drag not ok + } // end dropActionChanged + }; // end DropTargetListener + + // Make the component (and possibly children) drop targets + makeDropTarget(out, c, recursive); + } // end if: supports dnd + else { + log(out, "FileDrop: Drag and drop is not supported with this JVM"); + } // end else: does not support DnD + } // end constructor + + private static boolean supportsDnD() { // Static Boolean + if (supportsDnD == null) { + boolean support = false; + try { + Class.forName("java.awt.dnd.DnDConstants"); + support = true; + } // end try + catch (final Exception e) { + support = false; + } // end catch + supportsDnD = Boolean.valueOf(support); + } // end if: first time through + return supportsDnD.booleanValue(); + } // end supportsDnD + + // BEGIN 2007-09-12 Nathan Blomquist -- Linux (KDE/Gnome) support added. + private static String ZERO_CHAR_STRING = "" + (char) 0; + + private static File[] createFileArray(final BufferedReader bReader, + final PrintStream out) { + try { + final java.util.List list = new java.util.ArrayList(); + java.lang.String line = null; + while ((line = bReader.readLine()) != null) { + try { + // kde seems to append a 0 char to the end of the reader + if (ZERO_CHAR_STRING.equals(line)) + continue; + + final java.io.File file = new java.io.File(new java.net.URI(line)); + list.add(file); + } catch (final Exception ex) { + log(out, "Error with " + line + ": " + ex.getMessage()); + } + } + + return list.toArray(new File[list.size()]); + } catch (final IOException ex) { + log(out, "FileDrop: IOException"); + } + return new File[0]; + } + + // END 2007-09-12 Nathan Blomquist -- Linux (KDE/Gnome) support added. + + private void makeDropTarget(final java.io.PrintStream out, + final java.awt.Component c, final boolean recursive) { + // Make drop target + final java.awt.dnd.DropTarget dt = new java.awt.dnd.DropTarget(); + try { + dt.addDropTargetListener(dropListener); + } // end try + catch (final java.util.TooManyListenersException e) { + e.printStackTrace(); + log(out, + "FileDrop: Drop will not work due to previous error. Do you have another listener attached?"); + } // end catch + + // Listen for hierarchy changes and remove the drop target when the + // parent gets cleared out. + c.addHierarchyListener(new java.awt.event.HierarchyListener() { + @Override + public void hierarchyChanged(final java.awt.event.HierarchyEvent evt) { + log(out, "FileDrop: Hierarchy changed."); + final java.awt.Component parent = c.getParent(); + if (parent == null) { + c.setDropTarget(null); + log(out, "FileDrop: Drop target cleared from component."); + } // end if: null parent + else { + new java.awt.dnd.DropTarget(c, dropListener); + log(out, "FileDrop: Drop target added to component."); + } // end else: parent not null + } // end hierarchyChanged + }); // end hierarchy listener + if (c.getParent() != null) + new java.awt.dnd.DropTarget(c, dropListener); + + if (recursive && (c instanceof java.awt.Container)) { + // Get the container + final java.awt.Container cont = (java.awt.Container) c; + + // Get it's components + final java.awt.Component[] comps = cont.getComponents(); + + // Set it's components as listeners also + for (int i = 0; i < comps.length; i++) + makeDropTarget(out, comps[i], recursive); + } // end if: recursively set components as listener + } // end dropListener + + /** Determine if the dragged data is a file list. */ + private boolean isDragOk(final java.io.PrintStream out, + final java.awt.dnd.DropTargetDragEvent evt) { + boolean ok = false; + + // Get data flavors being dragged + final java.awt.datatransfer.DataFlavor[] flavors = evt + .getCurrentDataFlavors(); + + // See if any of the flavors are a file list + int i = 0; + while (!ok && i < flavors.length) { + // BEGIN 2007-09-12 Nathan Blomquist -- Linux (KDE/Gnome) support + // added. + // Is the flavor a file list? + final DataFlavor curFlavor = flavors[i]; + if (curFlavor + .equals(java.awt.datatransfer.DataFlavor.javaFileListFlavor) + || curFlavor.isRepresentationClassReader()) { + ok = true; + } + // END 2007-09-12 Nathan Blomquist -- Linux (KDE/Gnome) support + // added. + i++; + } // end while: through flavors + + // If logging is enabled, show data flavors + if (out != null) { + if (flavors.length == 0) + log(out, "FileDrop: no data flavors."); + for (i = 0; i < flavors.length; i++) + log(out, flavors[i].toString()); + } // end if: logging enabled + + return ok; + } // end isDragOk + + /** Outputs message to out if it's not null. */ + private static void log(final java.io.PrintStream out, final String message) { // Log + // message + // if + // requested + if (out != null) + out.println(message); + } // end log + + /** + * Removes the drag-and-drop hooks from the component and optionally from + * the all children. You should call this if you add and remove components + * after you've set up the drag-and-drop. This will recursively unregister + * all components contained within c if c is a + * {@link java.awt.Container}. + * + * @param c + * The component to unregister as a drop target + * @since 1.0 + */ + public static boolean remove(final java.awt.Component c) { + return remove(null, c, true); + } // end remove + + /** + * Removes the drag-and-drop hooks from the component and optionally from + * the all children. You should call this if you add and remove components + * after you've set up the drag-and-drop. + * + * @param out + * Optional {@link java.io.PrintStream} for logging drag and drop + * messages + * @param c + * The component to unregister + * @param recursive + * Recursively unregister components within a container + * @since 1.0 + */ + protected static boolean remove(final java.io.PrintStream out, final java.awt.Component c, + final boolean recursive) { // Make sure we support dnd. + if (supportsDnD()) { + log(out, "FileDrop: Removing drag-and-drop hooks."); + c.setDropTarget(null); + if (recursive && (c instanceof java.awt.Container)) { + final java.awt.Component[] comps = ((java.awt.Container) c) + .getComponents(); + for (int i = 0; i < comps.length; i++) + remove(out, comps[i], recursive); + return true; + } // end if: recursive + else + return false; + } // end if: supports DnD + else + return false; + } // end remove + + /* ******** I N N E R I N T E R F A C E L I S T E N E R ******** */ + + /** + * Implement this inner interface to listen for when files are dropped. For + * example your class declaration may begin like this: + *
+	 *      public class MyClass implements FileDrop.Listener
+	 *      ...
+	 *      public void filesDropped( java.io.File[] files )
+	 *      {
+	 *          ...
+	 *      }   // end filesDropped
+	 *      ...
+	 * 
+ * + * @since 1.1 + */ + public static interface Listener { + + /** + * This method is called when files have been successfully dropped. + * + * @param files + * An array of Files that were dropped. + * @since 1.0 + */ + public abstract void filesDropped(java.io.File[] files); + + } // end inner-interface Listener + + /* ******** I N N E R C L A S S ******** */ + + /** + * This is the event that is passed to the + * {@link FileDrop.Listener#filesDropped filesDropped(...)} method in your + * {@link FileDrop.Listener} when files are dropped onto a registered drop + * target. + * + *

+ * I'm releasing this code into the Public Domain. Enjoy. + *

+ * + * @author Robert Harder + * @author rob@iharder.net + * @version 1.2 + */ + @SuppressWarnings("serial") + static class Event extends java.util.EventObject { + + private final java.io.File[] files; + + /** + * Constructs an {@link Event} with the array of files that were dropped + * and the {@link FileDrop} that initiated the event. + * + * @param files + * The array of files that were dropped + * @param source + * The event source + * @since 1.1 + */ + public Event(final java.io.File[] files, final Object source) { + super(source); + this.files = files; + } // end constructor + + /** + * Returns an array of files that were dropped on a registered drop + * target. + * + * @return array of files that were dropped + * @since 1.1 + */ + public java.io.File[] getFiles() { + return files; + } // end getFiles + + } // end inner class Event + + /* ******** I N N E R C L A S S ******** */ + + /** + * At last an easy way to encapsulate your custom objects for dragging and + * dropping in your Java programs! When you need to create a + * {@link java.awt.datatransfer.Transferable} object, use this class to wrap + * your object. For example: + * + *
+	 *      ...
+	 *      MyCoolClass myObj = new MyCoolClass();
+	 *      Transferable xfer = new TransferableObject( myObj );
+	 *      ...
+	 * 
+ * + * Or if you need to know when the data was actually dropped, like when + * you're moving data out of a list, say, you can use the + * {@link TransferableObject.Fetcher} inner class to return your object Just + * in Time. For example: + * + *
+	 *      ...
+	 *      final MyCoolClass myObj = new MyCoolClass();
+	 * 
+	 *      TransferableObject.Fetcher fetcher = new TransferableObject.Fetcher()
+	 *      {   public Object getObject(){ return myObj; }
+	 *      }; // end fetcher
+	 * 
+	 *      Transferable xfer = new TransferableObject( fetcher );
+	 *      ...
+	 * 
+ * + * The {@link java.awt.datatransfer.DataFlavor} associated with + * {@link TransferableObject} has the representation class + * net.iharder.dnd.TransferableObject.class and MIME type + * application/x-net.iharder.dnd.TransferableObject. This data + * flavor is accessible via the static {@link #DATA_FLAVOR} property. + * + * + *

+ * I'm releasing this code into the Public Domain. Enjoy. + *

+ * + * @author Robert Harder + * @author rob@iharder.net + * @version 1.2 + */ + static class TransferableObject implements + java.awt.datatransfer.Transferable { + /** + * The MIME type for {@link #DATA_FLAVOR} is + * application/x-net.iharder.dnd.TransferableObject. + * + * @since 1.1 + */ + public final static String MIME_TYPE = "application/x-net.iharder.dnd.TransferableObject"; + + /** + * The default {@link java.awt.datatransfer.DataFlavor} for + * {@link TransferableObject} has the representation class + * net.iharder.dnd.TransferableObject.class and the MIME type + * application/x-net.iharder.dnd.TransferableObject. + * + * @since 1.1 + */ + public final static java.awt.datatransfer.DataFlavor DATA_FLAVOR = new java.awt.datatransfer.DataFlavor( + FileDrop.TransferableObject.class, MIME_TYPE); + + private Fetcher fetcher; + private Object data; + + private java.awt.datatransfer.DataFlavor customFlavor; + + /** + * Creates a new {@link TransferableObject} that wraps data. + * Along with the {@link #DATA_FLAVOR} associated with this class, this + * creates a custom data flavor with a representation class determined + * from data.getClass() and the MIME type + * application/x-net.iharder.dnd.TransferableObject. + * + * @param data + * The data to transfer + * @since 1.1 + */ + public TransferableObject(final Object data) { + this.data = data; + this.customFlavor = new java.awt.datatransfer.DataFlavor( + data.getClass(), MIME_TYPE); + } // end constructor + + /** + * Creates a new {@link TransferableObject} that will return the object + * that is returned by fetcher. No custom data flavor is set + * other than the default {@link #DATA_FLAVOR}. + * + * @see Fetcher + * @param fetcher + * The {@link Fetcher} that will return the data object + * @since 1.1 + */ + public TransferableObject(final Fetcher fetcher) { + this.fetcher = fetcher; + } // end constructor + + /** + * Creates a new {@link TransferableObject} that will return the object + * that is returned by fetcher. Along with the + * {@link #DATA_FLAVOR} associated with this class, this creates a + * custom data flavor with a representation class dataClass + * and the MIME type + * application/x-net.iharder.dnd.TransferableObject. + * + * @see Fetcher + * @param dataClass + * The {@link java.lang.Class} to use in the custom data + * flavor + * @param fetcher + * The {@link Fetcher} that will return the data object + * @since 1.1 + */ + public TransferableObject(final Class dataClass, final Fetcher fetcher) { + this.fetcher = fetcher; + this.customFlavor = new java.awt.datatransfer.DataFlavor(dataClass, + MIME_TYPE); + } // end constructor + + /** + * Returns the custom {@link java.awt.datatransfer.DataFlavor} + * associated with the encapsulated object or null if the + * {@link Fetcher} constructor was used without passing a + * {@link java.lang.Class}. + * + * @return The custom data flavor for the encapsulated object + * @since 1.1 + */ + public java.awt.datatransfer.DataFlavor getCustomDataFlavor() { + return customFlavor; + } // end getCustomDataFlavor + + /* ******** T R A N S F E R A B L E M E T H O D S ******** */ + + /** + * Returns a two- or three-element array containing first the custom + * data flavor, if one was created in the constructors, second the + * default {@link #DATA_FLAVOR} associated with + * {@link TransferableObject}, and third the + * {@link java.awt.datatransfer.DataFlavor#stringFlavor}. + * + * @return An array of supported data flavors + * @since 1.1 + */ + @Override + public java.awt.datatransfer.DataFlavor[] getTransferDataFlavors() { + if (customFlavor != null) + return new java.awt.datatransfer.DataFlavor[] { customFlavor, + DATA_FLAVOR, + java.awt.datatransfer.DataFlavor.stringFlavor }; // end + // flavors + // array + else + return new java.awt.datatransfer.DataFlavor[] { DATA_FLAVOR, + java.awt.datatransfer.DataFlavor.stringFlavor }; // end + // flavors + // array + } // end getTransferDataFlavors + + /** + * Returns the data encapsulated in this {@link TransferableObject}. If + * the {@link Fetcher} constructor was used, then this is when the + * {@link Fetcher#getObject getObject()} method will be called. If the + * requested data flavor is not supported, then the + * {@link Fetcher#getObject getObject()} method will not be called. + * + * @param flavor + * The data flavor for the data to return + * @return The dropped data + * @since 1.1 + */ + @Override + public Object getTransferData(final java.awt.datatransfer.DataFlavor flavor) + throws java.awt.datatransfer.UnsupportedFlavorException, + java.io.IOException { + // Native object + if (flavor.equals(DATA_FLAVOR)) + return fetcher == null ? data : fetcher.getObject(); + + // String + if (flavor.equals(java.awt.datatransfer.DataFlavor.stringFlavor)) + return fetcher == null ? data.toString() : fetcher.getObject() + .toString(); + + // We can't do anything else + throw new java.awt.datatransfer.UnsupportedFlavorException(flavor); + } // end getTransferData + + /** + * Returns true if flavor is one of the supported + * flavors. Flavors are supported using the equals(...) + * method. + * + * @param flavor + * The data flavor to check + * @return Whether or not the flavor is supported + * @since 1.1 + */ + @Override + public boolean isDataFlavorSupported( + final java.awt.datatransfer.DataFlavor flavor) { + // Native object + if (flavor.equals(DATA_FLAVOR)) + return true; + + // String + if (flavor.equals(java.awt.datatransfer.DataFlavor.stringFlavor)) + return true; + + // We can't do anything else + return false; + } // end isDataFlavorSupported + + /* ******** I N N E R I N T E R F A C E F E T C H E R ******** */ + + /** + * Instead of passing your data directly to the + * {@link TransferableObject} constructor, you may want to know exactly + * when your data was received in case you need to remove it from its + * source (or do anyting else to it). When the {@link #getTransferData + * getTransferData(...)} method is called on the + * {@link TransferableObject}, the {@link Fetcher}'s {@link #getObject + * getObject()} method will be called. + * + * @author Robert Harder + * @version 1.1 + * @since 1.1 + */ + public static interface Fetcher { + /** + * Return the object being encapsulated in the + * {@link TransferableObject}. + * + * @return The dropped object + * @since 1.1 + */ + public abstract Object getObject(); + } // end inner interface Fetcher + + } // end class TransferableObject + +} // end class FileDrop diff --git a/src/main/java/org/scijava/ui/swing/script/FileSystemTree.java b/src/main/java/org/scijava/ui/swing/script/FileSystemTree.java index 25111a68..20e9983d 100644 --- a/src/main/java/org/scijava/ui/swing/script/FileSystemTree.java +++ b/src/main/java/org/scijava/ui/swing/script/FileSystemTree.java @@ -49,11 +49,15 @@ import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; +import java.util.List; import java.util.Map; import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.swing.Icon; import javax.swing.ImageIcon; +import javax.swing.JOptionPane; import javax.swing.JTree; import javax.swing.SwingUtilities; import javax.swing.event.TreeExpansionEvent; @@ -270,6 +274,22 @@ public FileSystemTree(final Logger log) getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); setAutoscrolls(true); setScrollsOnExpand(true); + setExpandsSelectedPaths(true); + new FileDrop(this, files -> { + final List dirs = Arrays.asList(files).stream().filter(f -> f.isDirectory()) + .collect(Collectors.toList()); + if (dirs.isEmpty()) { + JOptionPane.showMessageDialog(FileSystemTree.this, "Only folders can be dropped into the file tree.", + "Invalid Drop", JOptionPane.WARNING_MESSAGE); + return; + } + final boolean confirm = dirs.size() < 4 || (JOptionPane.showConfirmDialog(FileSystemTree.this, + "Confirm loading of " + dirs.size() + " folders?", "Confirm?", + JOptionPane.OK_CANCEL_OPTION) == JOptionPane.OK_OPTION); + if (confirm) { + dirs.forEach(dir -> addRootDirectory(dir.getAbsolutePath(), true)); + } + }); addTreeWillExpandListener(new TreeWillExpandListener() { @Override public void treeWillExpand(TreeExpansionEvent event) throws ExpandVetoException { @@ -439,9 +459,10 @@ public void addRootDirectory(final String dir, final boolean checkIfChild) { final TreePath[] p = new TreePath[1]; node.expandTo(dirPath, p); if (null != p[0]) { - getModel().reload(); + //getModel().reload(); // this will collapse all nodes expandPath(p[0]); - scrollPathToVisible(p[0]); + setSelectionPath(p[0]); + scrollPathToVisible(p[0]); //spurious!? return; } } @@ -449,7 +470,8 @@ public void addRootDirectory(final String dir, final boolean checkIfChild) { } // Else, append it as a new root getModel().insertNodeInto(new Node(dirPath), root, root.getChildCount()); - getModel().reload(); + //getModel().reload(); // this will collapse all nodes + getModel().nodesWereInserted(root, new int[] { root.getChildCount() - 1 }); } @Override @@ -491,6 +513,7 @@ public void addTopLevelFoldersFrom(final String folders) { public void destroy() { dir_watcher.interrupt(); + FileDrop.remove(this); } private class DirectoryWatcher extends Thread { diff --git a/src/main/java/org/scijava/ui/swing/script/TextEditor.java b/src/main/java/org/scijava/ui/swing/script/TextEditor.java index 177bc875..affdeb3f 100644 --- a/src/main/java/org/scijava/ui/swing/script/TextEditor.java +++ b/src/main/java/org/scijava/ui/swing/script/TextEditor.java @@ -31,6 +31,7 @@ import java.awt.Color; import java.awt.Cursor; +import java.awt.Desktop; import java.awt.Dimension; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; @@ -114,6 +115,7 @@ import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPanel; +import javax.swing.JPopupMenu; import javax.swing.JRadioButtonMenuItem; import javax.swing.JScrollPane; import javax.swing.JSplitPane; @@ -129,6 +131,7 @@ import javax.swing.event.DocumentListener; import javax.swing.text.BadLocationException; import javax.swing.text.Position; +import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.TreePath; import org.fife.ui.rsyntaxtextarea.AbstractTokenMakerFactory; @@ -633,17 +636,23 @@ public TextEditor(final Context context) { new BoxLayout(getContentPane(), BoxLayout.Y_AXIS)); final JPanel tree_panel = new JPanel(); - final JButton add_directory = new JButton("[+]"); + final JButton add_directory = new JButton("+"); add_directory.setToolTipText("Add a directory"); - final JButton remove_directory = new JButton("[-]"); + final JButton remove_directory = new JButton("−"); remove_directory.setToolTipText("Remove a top-level directory"); - final JTextField filter = new JTextField("filter..."); - filter.setForeground(Color.gray); - filter.setToolTipText("Use leading '/' for regular expressions"); - + final JTextField filter = new JTextField("File Filter..."); + filter.setForeground(Color.GRAY); + filter.setToolTipText("

Use leading '/' for regular expressions. E.g.:

" + + "
" + + "
/py$ Displays only filenames ending with py
" + + "
/^Demo Displays only filenames starting with Demo
" + + "
" + + "

Press Enter to apply. Clear to reset.

"); tree = new FileSystemTree(log); tree.ignoreExtension("class"); + addContextualMenuToTree(tree); + dragSource = new DragSource(); dragSource.createDefaultDragGestureRecognizer(tree, DnDConstants.ACTION_COPY, new DragAndDrop()); tree.setMinimumSize(new Dimension(200, 600)); @@ -682,45 +691,60 @@ public TextEditor(final Context context) { } }); add_directory.addActionListener(e -> { - final JFileChooser c = new JFileChooser("Choose a directory"); + final String folders = tree.getTopLevelFoldersString(); + final String lastFolder = folders.substring(folders.lastIndexOf(":") + 1); + final JFileChooser c = new JFileChooser(); + c.setDialogTitle("Choose Top-Level Folder"); + c.setCurrentDirectory(new File(lastFolder)); c.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); c.setFileHidingEnabled(true); // hide hidden files + c.setAcceptAllFileFilterUsed(false); // disable "All files" as it has no meaning here + c.setApproveButtonText("Choose Folder"); + c.setMultiSelectionEnabled(false); + c.setDragEnabled(true); + new FileDrop(c, files -> { + if (files.length == 0) return; + final File firstFile = files[0]; + c.setCurrentDirectory((firstFile.isDirectory()) ? firstFile : firstFile.getParentFile()); + c.rescanCurrentDirectory(); + }); if (JFileChooser.APPROVE_OPTION == c.showOpenDialog(getContentPane())) { final File f = c.getSelectedFile(); if (f.isDirectory()) tree.addRootDirectory(f.getAbsolutePath(), false); } + FileDrop.remove(c); }); remove_directory.addActionListener(e -> { final TreePath p = tree.getSelectionPath(); if (null == p) { - JOptionPane.showMessageDialog(TextEditor.this, - "Select a top-level folder first."); + JOptionPane.showMessageDialog(TextEditor.this, "Select a top-level folder first.", "Invalid Folder", + JOptionPane.ERROR_MESSAGE); return; } if (2 == p.getPathCount()) { // Is a child of the root, so it's a top-level folder tree.getModel().removeNodeFromParent(// - (FileSystemTree.Node) p.getLastPathComponent()); - } - else { - JOptionPane.showMessageDialog(TextEditor.this, - "Can only remove top-level folders."); + (FileSystemTree.Node) p.getLastPathComponent()); + } else { + JOptionPane.showMessageDialog(TextEditor.this, "Can only remove top-level folders.", "Invalid Folder", + JOptionPane.ERROR_MESSAGE); } }); filter.addFocusListener(new FocusListener() { @Override public void focusLost(FocusEvent e) { if (0 == filter.getText().length()) { - filter.setForeground(Color.gray); - filter.setText("filter..."); + filter.setForeground(Color.GRAY); + filter.setText("File Filter..."); + tree.setFileFilter(((f) -> true)); // any // no need to press enter } } @Override public void focusGained(FocusEvent e) { - if (filter.getForeground() == Color.gray) { + if (filter.getForeground() == Color.GRAY) { filter.setText(""); - filter.setForeground(Color.black); + filter.setForeground(tree.getForeground()); } } }); @@ -742,10 +766,11 @@ public void keyPressed(final KeyEvent ke) { if ('^' != regex.charAt(1)) regex = "^.*" + regex; if ('$' != regex.charAt(regex.length() -1)) regex += ".*$"; pattern = Pattern.compile(regex); - filter.setForeground(Color.black); - } catch (final PatternSyntaxException pse) { + filter.setForeground(tree.getForeground()); + } catch (final PatternSyntaxException | StringIndexOutOfBoundsException pse) { + // regex is too short to be parseable or is invalid log.warn(pse.getLocalizedMessage()); - filter.setForeground(Color.red); + filter.setForeground(Color.RED); pattern = null; return; } @@ -754,12 +779,12 @@ public void keyPressed(final KeyEvent ke) { } } else { // Interpret as a literal match - tree.setFileFilter((f) -> -1 != f.getName().indexOf(text)); + tree.setFileFilter((f) -> -1 != f.getName().toLowerCase().indexOf(text.toLowerCase())); } } else { // Upon re-typing something - if (filter.getForeground() == Color.red) { - filter.setForeground(Color.black); + if (filter.getForeground() == Color.RED) { + filter.setForeground(tree.getForeground()); } } } @@ -791,7 +816,7 @@ public void keyPressed(final KeyEvent ke) { bc.fill = GridBagConstraints.BOTH; tree_panel.add(tree, bc); final JScrollPane scrolltree = new JScrollPane(tree_panel); - scrolltree.setBackground(Color.white); + //scrolltree.setBackground(Color.white); // No hardwired constants for compatibility w/ dark themes scrolltree.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEmptyBorder(0,5,0,5))); scrolltree.setPreferredSize(new Dimension(200, 600)); body = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, scrolltree, tabbed); @@ -889,6 +914,8 @@ public void dragEnter(DragSourceDragEvent dsde) { @Override public void dragGestureRecognized(DragGestureEvent dge) { TreePath path = tree.getSelectionPath(); + if (path == null) // nothing is currently selected + return; final String filepath = (String)((FileSystemTree.Node) path.getLastPathComponent()).getUserObject(); dragSource.startDrag(dge, DragSource.DefaultCopyDrop, new Transferable() { @Override @@ -3159,4 +3186,51 @@ private static Color getDisabledComponentColor() { return Color.GRAY; } } + + private void addContextualMenuToTree(final FileSystemTree tree) { + final JPopupMenu popup = new JPopupMenu(); + JMenuItem jmi = new JMenuItem("Collapse All"); + jmi.addActionListener(e -> { + SwingUtilities.invokeLater(() -> { + for (int i = tree.getRowCount() - 1; i >= 0; i--) + tree.collapseRow(i); + }); + }); + popup.add(jmi); + jmi = new JMenuItem("Expand One Level"); + jmi.addActionListener(e -> { + SwingUtilities.invokeLater(() -> { + for (int i = tree.getRowCount() - 1; i >= 0; i--) + tree.expandRow(i); + }); + }); + popup.add(jmi); + popup.addSeparator(); + jmi = new JMenuItem("Show in System Explorer"); + jmi.addActionListener(e -> { + final TreePath path = tree.getSelectionPath(); + if (path == null) { + JOptionPane.showMessageDialog(TextEditor.this, "No items are currently selected.", + "Invalid Selection", JOptionPane.INFORMATION_MESSAGE); + return; + } + try { + final String filepath = (String) ((FileSystemTree.Node) path.getLastPathComponent()).getUserObject(); + final File f = new File(filepath); + Desktop.getDesktop().open((f.isDirectory()) ? f : f.getParentFile()); + } catch (final Exception | Error ignored) { + JOptionPane.showMessageDialog(TextEditor.this, + "Folder of selected item does not seem to be accessible.", "Error", JOptionPane.ERROR_MESSAGE); + } + }); + popup.add(jmi); + popup.addSeparator(); + jmi = new JMenuItem("Reset Tree to Home Folder"); + jmi.addActionListener(e -> { + ((DefaultMutableTreeNode) tree.getModel().getRoot()).removeAllChildren(); + tree.addTopLevelFoldersFrom(System.getProperty("user.home")); + }); + popup.add(jmi); + tree.setComponentPopupMenu(popup); + } } From 7f9b60e0e6745cae18ed321224fb0f24b3b6a79c Mon Sep 17 00:00:00 2001 From: tferr Date: Mon, 21 Feb 2022 22:55:07 -0500 Subject: [PATCH 04/21] Move all FileSystemTree related commands into own class This simplifies things quite a bit. While at it, add options for Regex, Cases sensitive, built-in help, etc. --- .../ui/swing/script/FileSystemTree.java | 2 +- .../ui/swing/script/FileSystemTreePanel.java | 379 ++++++++++++++++++ .../scijava/ui/swing/script/TextEditor.java | 202 +--------- 3 files changed, 387 insertions(+), 196 deletions(-) create mode 100644 src/main/java/org/scijava/ui/swing/script/FileSystemTreePanel.java diff --git a/src/main/java/org/scijava/ui/swing/script/FileSystemTree.java b/src/main/java/org/scijava/ui/swing/script/FileSystemTree.java index 20e9983d..937f31a1 100644 --- a/src/main/java/org/scijava/ui/swing/script/FileSystemTree.java +++ b/src/main/java/org/scijava/ui/swing/script/FileSystemTree.java @@ -256,7 +256,7 @@ public interface LeafListener { public void leafDoubleClicked(final File file); } - private final Logger log; + final Logger log; private ArrayList leaf_listeners = new ArrayList<>(); diff --git a/src/main/java/org/scijava/ui/swing/script/FileSystemTreePanel.java b/src/main/java/org/scijava/ui/swing/script/FileSystemTreePanel.java new file mode 100644 index 00000000..ac89be38 --- /dev/null +++ b/src/main/java/org/scijava/ui/swing/script/FileSystemTreePanel.java @@ -0,0 +1,379 @@ +/* + * #%L + * Script Editor and Interpreter for SciJava script languages. + * %% + * Copyright (C) 2009 - 2022 SciJava developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +package org.scijava.ui.swing.script; + +import java.awt.Color; +import java.awt.Desktop; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.RenderingHints; +import java.awt.event.FocusAdapter; +import java.awt.event.FocusEvent; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.io.File; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import javax.swing.FocusManager; +import javax.swing.JButton; +import javax.swing.JCheckBoxMenuItem; +import javax.swing.JFileChooser; +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JTextField; +import javax.swing.SwingUtilities; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.TreePath; + +class FileSystemTreePanel extends JPanel { + + private static final long serialVersionUID = -710040159139542578L; + private final FileSystemTree tree; + private final SearchField searchField; + private boolean regex; + private boolean caseSensitive; + + FileSystemTreePanel(final FileSystemTree tree) { + this.tree = tree; + searchField = initializedField(); + setLayout(new GridBagLayout()); + final GridBagConstraints bc = new GridBagConstraints(); + bc.gridx = 0; + bc.gridy = 0; + bc.weightx = 0; + bc.weighty = 0; + bc.anchor = GridBagConstraints.NORTHWEST; + bc.fill = GridBagConstraints.NONE; + add(addDirectoryButton(), bc); + bc.gridx = 1; + add(removeDirectoryButton(), bc); + bc.gridx = 2; + bc.fill = GridBagConstraints.BOTH; + bc.weightx = 1; + add(searchField, bc); + bc.fill = GridBagConstraints.NONE; + bc.weightx = 0; + bc.gridx = 3; + add(searchOptionsButton(), bc); + bc.gridx = 0; + bc.gridwidth = 4; + bc.gridy = 1; + bc.weightx = 1.0; + bc.weighty = 1.0; + bc.fill = GridBagConstraints.BOTH; + add(tree, bc); + addContextualMenuToTree(); + } + + private SearchField initializedField() { + final SearchField field = new SearchField(); + field.addFocusListener(new FocusAdapter() { + @Override + public void focusLost(final FocusEvent e) { + if (0 == field.getText().length()) { + tree.setFileFilter(((f) -> true)); // any // no need to press enter + } + } + }); + field.addKeyListener(new KeyAdapter() { + Pattern pattern = null; + + @Override + public void keyPressed(final KeyEvent ke) { + if (ke.getKeyCode() == KeyEvent.VK_ENTER) { + final String text = field.getText(); + if (0 == text.length()) { + tree.setFileFilter(((f) -> true)); // any + return; + } + + if (isRegexEnabled()) { // if ('/' == text.charAt(0)) { + // Interpret as a regular expression + // Attempt to compile the pattern + try { + String regex = text; // text.substring(1); + if ('^' != regex.charAt(1)) + regex = "^.*" + regex; + if ('$' != regex.charAt(regex.length() - 1)) + regex += ".*$"; + pattern = Pattern.compile(regex); + field.setForeground(tree.getForeground()); + } catch (final PatternSyntaxException | StringIndexOutOfBoundsException pse) { + // regex is too short to be parseable or is invalid + tree.log.warn(pse.getLocalizedMessage()); + field.setForeground(Color.RED); + pattern = null; + return; + } + if (null != pattern) { + tree.setFileFilter((f) -> pattern.matcher(f.getName()).matches()); + } + } else { + // Interpret as a literal match + if (isCaseSensitive()) + tree.setFileFilter((f) -> -1 != f.getName().indexOf(text)); + else + tree.setFileFilter((f) -> -1 != f.getName().toLowerCase().indexOf(text.toLowerCase())); + } + } else { + // Upon re-typing something + if (field.getForeground() == Color.RED) { + field.setForeground(tree.getForeground()); + } + } + } + }); + return field; + } + + private JButton addDirectoryButton() { + final JButton add_directory = new JButton("+"); + add_directory.setToolTipText("Add a directory"); + add_directory.addActionListener(e -> { + final String folders = tree.getTopLevelFoldersString(); + final String lastFolder = folders.substring(folders.lastIndexOf(":") + 1); + final JFileChooser c = new JFileChooser(); + c.setDialogTitle("Choose Top-Level Folder"); + c.setCurrentDirectory(new File(lastFolder)); + c.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + c.setFileHidingEnabled(true); // hide hidden files + c.setAcceptAllFileFilterUsed(false); // disable "All files" as it has no meaning here + c.setApproveButtonText("Choose Folder"); + c.setMultiSelectionEnabled(false); + c.setDragEnabled(true); + new FileDrop(c, files -> { + if (files.length == 0) + return; + final File firstFile = files[0]; + c.setCurrentDirectory((firstFile.isDirectory()) ? firstFile : firstFile.getParentFile()); + c.rescanCurrentDirectory(); + }); + if (JFileChooser.APPROVE_OPTION == c.showOpenDialog(this)) { + final File f = c.getSelectedFile(); + if (f.isDirectory()) + tree.addRootDirectory(f.getAbsolutePath(), false); + } + FileDrop.remove(c); + }); + return add_directory; + } + + private JButton removeDirectoryButton() { + final JButton remove_directory = new JButton("−"); + remove_directory.setToolTipText("Remove a top-level directory"); + remove_directory.addActionListener(e -> { + final TreePath p = tree.getSelectionPath(); + if (null == p) { + JOptionPane.showMessageDialog(this, "Select a top-level folder first.", "Invalid Folder", + JOptionPane.ERROR_MESSAGE); + return; + } + if (2 == p.getPathCount()) { + // Is a child of the root, so it's a top-level folder + tree.getModel().removeNodeFromParent(// + (FileSystemTree.Node) p.getLastPathComponent()); + } else { + JOptionPane.showMessageDialog(this, "Can only remove top-level folders.", "Invalid Folder", + JOptionPane.ERROR_MESSAGE); + } + }); + return remove_directory; + } + + private JButton searchOptionsButton() { + final JButton options = new JButton("⋮"); + options.setToolTipText("Filtering options"); + final JPopupMenu popup = new JPopupMenu(); + final JCheckBoxMenuItem jcbmi1 = new JCheckBoxMenuItem("Case Sensitive", isCaseSensitive()); + jcbmi1.addItemListener(e -> { + setCaseSensitive(jcbmi1.isSelected()); + }); + popup.add(jcbmi1); + final JCheckBoxMenuItem jcbmi2 = new JCheckBoxMenuItem("Enable Regex", isCaseSensitive()); + jcbmi2.addItemListener(e -> { + setRegexEnabled(jcbmi2.isSelected()); + }); + popup.add(jcbmi2); + popup.addSeparator(); + JMenuItem jmi = new JMenuItem("Reset Filter"); + jmi.addActionListener(e -> { + searchField.setText(""); + tree.setFileFilter(((f) -> true)); + }); + popup.add(jmi); + popup.addSeparator(); + jmi = new JMenuItem("Context Help..."); + jmi.addActionListener(e -> showHelpMsg()); + popup.add(jmi); + options.addActionListener(e -> popup.show(options, options.getWidth() / 2, options.getHeight() / 2)); + return options; + } + + @SuppressWarnings("unused") + private boolean allTreeNodesCollapsed() { + for (int i = 0; i < tree.getRowCount(); i++) + if (!tree.isCollapsed(i)) return false; + return true; + } + + private void addContextualMenuToTree() { + final JPopupMenu popup = new JPopupMenu(); + JMenuItem jmi = new JMenuItem("Collapse All"); + jmi.addActionListener(e -> { + SwingUtilities.invokeLater(() -> { + for (int i = tree.getRowCount() - 1; i >= 0; i--) + tree.collapseRow(i); + }); + }); + popup.add(jmi); + jmi = new JMenuItem("Expand All Folders"); + jmi.addActionListener(e -> { + SwingUtilities.invokeLater(() -> { + for (int i = tree.getRowCount() - 1; i >= 0; i--) + tree.expandRow(i); + }); + }); + popup.add(jmi); + popup.addSeparator(); + jmi = new JMenuItem("Show in System Explorer"); + jmi.addActionListener(e -> { + final TreePath path = tree.getSelectionPath(); + if (path == null) { + JOptionPane.showMessageDialog(this, "No items are currently selected.", + "Invalid Selection", JOptionPane.INFORMATION_MESSAGE); + return; + } + try { + final String filepath = (String) ((FileSystemTree.Node) path.getLastPathComponent()).getUserObject(); + final File f = new File(filepath); + Desktop.getDesktop().open((f.isDirectory()) ? f : f.getParentFile()); + } catch (final Exception | Error ignored) { + JOptionPane.showMessageDialog(this, + "Folder of selected item does not seem to be accessible.", "Error", JOptionPane.ERROR_MESSAGE); + } + }); + popup.add(jmi); + popup.addSeparator(); + jmi = new JMenuItem("Reset Tree to Home Folder"); + jmi.addActionListener(e -> { + ((DefaultMutableTreeNode) tree.getModel().getRoot()).removeAllChildren(); + tree.addTopLevelFoldersFrom(System.getProperty("user.home")); + }); + popup.add(jmi); + tree.setComponentPopupMenu(popup); + } + + private void showHelpMsg() { + final String msg = "
" // + + "

Overview

" // + + "

The File Explorer provides a direct view of selected folders. Changes in " // + + "the native file system are synchronized in real time.

" // + + "

Add/Remove Folders

" // + + "

To add a folder, use the [+] button, or drag & drop folders from the native " // + + "System Explorer. To remove a folder: select it, then use the [-] button. To reset " + + "or reveal items: use the commands in the contextual popup menu.

" // + + "

Filtering Files

" // + + "

Filters affect only filenames (not folders) and are applied by typing a "// + + "filtering string and pressing [Enter]. Filters act only on files being listed, " // + + "and ignore collapsed folders. Examples of regex usage:

" // + + "
" // + + " " // + + " " // + + " " // + + " " // + + " " // + + " " // + + " " // + + " " // + + " " // + + " " // + + " " // + + " " // + + " " // + + " " // + + " " // + + " " // + + "
PatternResult
py$Display filenames ending with py
^DemoDisplay filenames starting with Demo
"; + JOptionPane.showMessageDialog(this, msg, "File Explorer", JOptionPane.PLAIN_MESSAGE); + } + + private boolean isCaseSensitive() { + return caseSensitive; + } + + private boolean isRegexEnabled() { + return regex; + } + + private void setCaseSensitive(final boolean b) { + caseSensitive = b; + searchField.update(); + } + + private void setRegexEnabled(final boolean b) { + regex = b; + searchField.update(); + } + + private class SearchField extends JTextField { + + private static final long serialVersionUID = 7004232238240585434L; + private static final String REGEX_HOLDER = " [?*]"; + private static final String CASE_HOLDER = " [Aa]"; + private static final String DEF_HOLDER = "File filter... "; + + void update() { + update(getGraphics()); + } + + @Override + protected void paintComponent(final java.awt.Graphics g) { + super.paintComponent(g); + if (super.getText().isEmpty() && !(FocusManager.getCurrentKeyboardFocusManager().getFocusOwner() == this)) { + final Graphics2D g2 = (Graphics2D) g.create(); + g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + g2.setColor(Color.GRAY); + g2.setFont(getFont().deriveFont(Font.ITALIC)); + final StringBuilder sb = new StringBuilder(DEF_HOLDER); + if (isCaseSensitive()) + sb.append(CASE_HOLDER); + if (isRegexEnabled()) + sb.append(REGEX_HOLDER); + g2.drawString(sb.toString(), 4, g2.getFontMetrics().getHeight()); + g2.dispose(); + } + } + } +} diff --git a/src/main/java/org/scijava/ui/swing/script/TextEditor.java b/src/main/java/org/scijava/ui/swing/script/TextEditor.java index affdeb3f..d1f6fa55 100644 --- a/src/main/java/org/scijava/ui/swing/script/TextEditor.java +++ b/src/main/java/org/scijava/ui/swing/script/TextEditor.java @@ -31,7 +31,6 @@ import java.awt.Color; import java.awt.Cursor; -import java.awt.Desktop; import java.awt.Dimension; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; @@ -51,8 +50,6 @@ import java.awt.event.ActionListener; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; -import java.awt.event.FocusEvent; -import java.awt.event.FocusListener; import java.awt.event.ItemEvent; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; @@ -107,7 +104,6 @@ import javax.swing.ButtonGroup; import javax.swing.JButton; import javax.swing.JCheckBoxMenuItem; -import javax.swing.JFileChooser; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JMenu; @@ -115,13 +111,11 @@ import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPanel; -import javax.swing.JPopupMenu; import javax.swing.JRadioButtonMenuItem; import javax.swing.JScrollPane; import javax.swing.JSplitPane; import javax.swing.JTabbedPane; import javax.swing.JTextArea; -import javax.swing.JTextField; import javax.swing.KeyStroke; import javax.swing.SwingUtilities; import javax.swing.UIManager; @@ -631,30 +625,15 @@ public TextEditor(final Context context) { tabbed.addChangeListener(this); open(null); // make sure the editor pane is added - tabbed.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4)); + //tabbed.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4)); getContentPane().setLayout( new BoxLayout(getContentPane(), BoxLayout.Y_AXIS)); - - final JPanel tree_panel = new JPanel(); - final JButton add_directory = new JButton("+"); - add_directory.setToolTipText("Add a directory"); - final JButton remove_directory = new JButton("−"); - remove_directory.setToolTipText("Remove a top-level directory"); - - final JTextField filter = new JTextField("File Filter..."); - filter.setForeground(Color.GRAY); - filter.setToolTipText("

Use leading '/' for regular expressions. E.g.:

" - + "
" - + "
/py$ Displays only filenames ending with py
" - + "
/^Demo Displays only filenames starting with Demo
" - + "
" - + "

Press Enter to apply. Clear to reset.

"); + tree = new FileSystemTree(log); - tree.ignoreExtension("class"); - addContextualMenuToTree(tree); - + tree.setFont(tree.getFont().deriveFont(getEditorPane().getFontSize())); dragSource = new DragSource(); dragSource.createDefaultDragGestureRecognizer(tree, DnDConstants.ACTION_COPY, new DragAndDrop()); + tree.ignoreExtension("class"); tree.setMinimumSize(new Dimension(200, 600)); tree.addLeafListener(f -> { final String name = f.getName(); @@ -690,134 +669,13 @@ public TextEditor(final Context context) { open(f); } }); - add_directory.addActionListener(e -> { - final String folders = tree.getTopLevelFoldersString(); - final String lastFolder = folders.substring(folders.lastIndexOf(":") + 1); - final JFileChooser c = new JFileChooser(); - c.setDialogTitle("Choose Top-Level Folder"); - c.setCurrentDirectory(new File(lastFolder)); - c.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); - c.setFileHidingEnabled(true); // hide hidden files - c.setAcceptAllFileFilterUsed(false); // disable "All files" as it has no meaning here - c.setApproveButtonText("Choose Folder"); - c.setMultiSelectionEnabled(false); - c.setDragEnabled(true); - new FileDrop(c, files -> { - if (files.length == 0) return; - final File firstFile = files[0]; - c.setCurrentDirectory((firstFile.isDirectory()) ? firstFile : firstFile.getParentFile()); - c.rescanCurrentDirectory(); - }); - if (JFileChooser.APPROVE_OPTION == c.showOpenDialog(getContentPane())) { - final File f = c.getSelectedFile(); - if (f.isDirectory()) tree.addRootDirectory(f.getAbsolutePath(), false); - } - FileDrop.remove(c); - }); - remove_directory.addActionListener(e -> { - final TreePath p = tree.getSelectionPath(); - if (null == p) { - JOptionPane.showMessageDialog(TextEditor.this, "Select a top-level folder first.", "Invalid Folder", - JOptionPane.ERROR_MESSAGE); - return; - } - if (2 == p.getPathCount()) { - // Is a child of the root, so it's a top-level folder - tree.getModel().removeNodeFromParent(// - (FileSystemTree.Node) p.getLastPathComponent()); - } else { - JOptionPane.showMessageDialog(TextEditor.this, "Can only remove top-level folders.", "Invalid Folder", - JOptionPane.ERROR_MESSAGE); - } - }); - filter.addFocusListener(new FocusListener() { - @Override - public void focusLost(FocusEvent e) { - if (0 == filter.getText().length()) { - filter.setForeground(Color.GRAY); - filter.setText("File Filter..."); - tree.setFileFilter(((f) -> true)); // any // no need to press enter - } - } - - @Override - public void focusGained(FocusEvent e) { - if (filter.getForeground() == Color.GRAY) { - filter.setText(""); - filter.setForeground(tree.getForeground()); - } - } - }); - filter.addKeyListener(new KeyAdapter() { - Pattern pattern = null; - @Override - public void keyPressed(final KeyEvent ke) { - if (ke.getKeyCode() == KeyEvent.VK_ENTER) { - final String text = filter.getText(); - if (0 == text.length()) { - tree.setFileFilter(((f) -> true)); // any - return; - } - if ('/' == text.charAt(0)) { - // Interpret as a regular expression - // Attempt to compile the pattern - try { - String regex = text.substring(1); - if ('^' != regex.charAt(1)) regex = "^.*" + regex; - if ('$' != regex.charAt(regex.length() -1)) regex += ".*$"; - pattern = Pattern.compile(regex); - filter.setForeground(tree.getForeground()); - } catch (final PatternSyntaxException | StringIndexOutOfBoundsException pse) { - // regex is too short to be parseable or is invalid - log.warn(pse.getLocalizedMessage()); - filter.setForeground(Color.RED); - pattern = null; - return; - } - if (null != pattern) { - tree.setFileFilter((f) -> pattern.matcher(f.getName()).matches()); - } - } else { - // Interpret as a literal match - tree.setFileFilter((f) -> -1 != f.getName().toLowerCase().indexOf(text.toLowerCase())); - } - } else { - // Upon re-typing something - if (filter.getForeground() == Color.RED) { - filter.setForeground(tree.getForeground()); - } - } - } - }); - + // Restore top-level directories tree.addTopLevelFoldersFrom(getEditorPane().loadFolders()); - final GridBagLayout g = new GridBagLayout(); - tree_panel.setLayout(g); - final GridBagConstraints bc = new GridBagConstraints(); - bc.gridx = 0; - bc.gridy = 0; - bc.weightx = 0; - bc.weighty = 0; - bc.anchor = GridBagConstraints.NORTHWEST; - bc.fill = GridBagConstraints.NONE; - tree_panel.add(add_directory, bc); - bc.gridx = 1; - tree_panel.add(remove_directory, bc); - bc.gridx = 2; - bc.fill = GridBagConstraints.BOTH; - tree_panel.add(filter, bc); - bc.gridx = 0; - bc.gridwidth = 3; - bc.gridy = 1; - bc.weightx = 1.0; - bc.weighty = 1.0; - bc.fill = GridBagConstraints.BOTH; - tree_panel.add(tree, bc); - final JScrollPane scrolltree = new JScrollPane(tree_panel); + final JScrollPane scrolltree = new JScrollPane(new FileSystemTreePanel(tree)); //scrolltree.setBackground(Color.white); // No hardwired constants for compatibility w/ dark themes - scrolltree.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEmptyBorder(0,5,0,5))); + scrolltree.setBorder(BorderFactory.createEmptyBorder(0,5,0,5)); scrolltree.setPreferredSize(new Dimension(200, 600)); body = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, scrolltree, tabbed); body.setOneTouchExpandable(true); @@ -3187,50 +3045,4 @@ private static Color getDisabledComponentColor() { } } - private void addContextualMenuToTree(final FileSystemTree tree) { - final JPopupMenu popup = new JPopupMenu(); - JMenuItem jmi = new JMenuItem("Collapse All"); - jmi.addActionListener(e -> { - SwingUtilities.invokeLater(() -> { - for (int i = tree.getRowCount() - 1; i >= 0; i--) - tree.collapseRow(i); - }); - }); - popup.add(jmi); - jmi = new JMenuItem("Expand One Level"); - jmi.addActionListener(e -> { - SwingUtilities.invokeLater(() -> { - for (int i = tree.getRowCount() - 1; i >= 0; i--) - tree.expandRow(i); - }); - }); - popup.add(jmi); - popup.addSeparator(); - jmi = new JMenuItem("Show in System Explorer"); - jmi.addActionListener(e -> { - final TreePath path = tree.getSelectionPath(); - if (path == null) { - JOptionPane.showMessageDialog(TextEditor.this, "No items are currently selected.", - "Invalid Selection", JOptionPane.INFORMATION_MESSAGE); - return; - } - try { - final String filepath = (String) ((FileSystemTree.Node) path.getLastPathComponent()).getUserObject(); - final File f = new File(filepath); - Desktop.getDesktop().open((f.isDirectory()) ? f : f.getParentFile()); - } catch (final Exception | Error ignored) { - JOptionPane.showMessageDialog(TextEditor.this, - "Folder of selected item does not seem to be accessible.", "Error", JOptionPane.ERROR_MESSAGE); - } - }); - popup.add(jmi); - popup.addSeparator(); - jmi = new JMenuItem("Reset Tree to Home Folder"); - jmi.addActionListener(e -> { - ((DefaultMutableTreeNode) tree.getModel().getRoot()).removeAllChildren(); - tree.addTopLevelFoldersFrom(System.getProperty("user.home")); - }); - popup.add(jmi); - tree.setComponentPopupMenu(popup); - } } From 55aaaaa7af9672f4ab5541770ed36ff3d5a85a16 Mon Sep 17 00:00:00 2001 From: tferr Date: Tue, 22 Feb 2022 13:12:21 -0500 Subject: [PATCH 05/21] TextEditor: Improvements to tabbed pane - Controls for tab placement - Opening files via drag & drop - Navigate tabs using scrollwheel - Fix drag & drop related exception Plus minor code cleanup of FileSystemTreePanel. Signed-off-by: Curtis Rueden --- .../ui/swing/script/FileSystemTreePanel.java | 93 ++++++++++------ .../scijava/ui/swing/script/TextEditor.java | 105 ++++++++++++++---- .../ui/swing/script/TextEditorTab.java | 1 + 3 files changed, 144 insertions(+), 55 deletions(-) diff --git a/src/main/java/org/scijava/ui/swing/script/FileSystemTreePanel.java b/src/main/java/org/scijava/ui/swing/script/FileSystemTreePanel.java index ac89be38..50a4d9e9 100644 --- a/src/main/java/org/scijava/ui/swing/script/FileSystemTreePanel.java +++ b/src/main/java/org/scijava/ui/swing/script/FileSystemTreePanel.java @@ -53,10 +53,20 @@ import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JTextField; -import javax.swing.SwingUtilities; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.TreePath; +import org.scijava.Context; +import org.scijava.app.AppService; +import org.scijava.plugin.Parameter; + +/** + * Convenience class for displaying a {@link FileSystemTree} with some bells and + * whistles, incliding a filter toolbar. + * + * @author Albert Cardona + * @author Tiago Ferreira + */ class FileSystemTreePanel extends JPanel { private static final long serialVersionUID = -710040159139542578L; @@ -65,8 +75,12 @@ class FileSystemTreePanel extends JPanel { private boolean regex; private boolean caseSensitive; - FileSystemTreePanel(final FileSystemTree tree) { + @Parameter + private AppService appService; + + FileSystemTreePanel(final FileSystemTree tree, final Context context) { this.tree = tree; + context.inject(this); searchField = initializedField(); setLayout(new GridBagLayout()); final GridBagConstraints bc = new GridBagConstraints(); @@ -118,7 +132,7 @@ public void keyPressed(final KeyEvent ke) { tree.setFileFilter(((f) -> true)); // any return; } - + if (isRegexEnabled()) { // if ('/' == text.charAt(0)) { // Interpret as a regular expression // Attempt to compile the pattern @@ -234,7 +248,7 @@ private JButton searchOptionsButton() { }); popup.add(jmi); popup.addSeparator(); - jmi = new JMenuItem("Context Help..."); + jmi = new JMenuItem("About File Explorer ..."); jmi.addActionListener(e -> showHelpMsg()); popup.add(jmi); options.addActionListener(e -> popup.show(options, options.getWidth() / 2, options.getHeight() / 2)); @@ -244,35 +258,26 @@ private JButton searchOptionsButton() { @SuppressWarnings("unused") private boolean allTreeNodesCollapsed() { for (int i = 0; i < tree.getRowCount(); i++) - if (!tree.isCollapsed(i)) return false; + if (!tree.isCollapsed(i)) + return false; return true; } private void addContextualMenuToTree() { final JPopupMenu popup = new JPopupMenu(); JMenuItem jmi = new JMenuItem("Collapse All"); - jmi.addActionListener(e -> { - SwingUtilities.invokeLater(() -> { - for (int i = tree.getRowCount() - 1; i >= 0; i--) - tree.collapseRow(i); - }); - }); + jmi.addActionListener(e -> collapseAllNodes()); popup.add(jmi); - jmi = new JMenuItem("Expand All Folders"); - jmi.addActionListener(e -> { - SwingUtilities.invokeLater(() -> { - for (int i = tree.getRowCount() - 1; i >= 0; i--) - tree.expandRow(i); - }); - }); + jmi = new JMenuItem("Expand Folders"); + jmi.addActionListener(e -> expandImmediateNodes()); popup.add(jmi); popup.addSeparator(); jmi = new JMenuItem("Show in System Explorer"); jmi.addActionListener(e -> { final TreePath path = tree.getSelectionPath(); if (path == null) { - JOptionPane.showMessageDialog(this, "No items are currently selected.", - "Invalid Selection", JOptionPane.INFORMATION_MESSAGE); + JOptionPane.showMessageDialog(this, "No items are currently selected.", "Invalid Selection", + JOptionPane.INFORMATION_MESSAGE); return; } try { @@ -280,42 +285,59 @@ private void addContextualMenuToTree() { final File f = new File(filepath); Desktop.getDesktop().open((f.isDirectory()) ? f : f.getParentFile()); } catch (final Exception | Error ignored) { - JOptionPane.showMessageDialog(this, - "Folder of selected item does not seem to be accessible.", "Error", JOptionPane.ERROR_MESSAGE); + JOptionPane.showMessageDialog(this, "Folder of selected item does not seem to be accessible.", "Error", + JOptionPane.ERROR_MESSAGE); } }); popup.add(jmi); popup.addSeparator(); - jmi = new JMenuItem("Reset Tree to Home Folder"); - jmi.addActionListener(e -> { - ((DefaultMutableTreeNode) tree.getModel().getRoot()).removeAllChildren(); - tree.addTopLevelFoldersFrom(System.getProperty("user.home")); - }); + jmi = new JMenuItem("Reset to Home Folder"); + jmi.addActionListener(e -> changeRootPath(System.getProperty("user.home"))); + popup.add(jmi); + jmi = new JMenuItem("Reset to Fiji.app/"); + jmi.addActionListener(e -> changeRootPath(appService.getApp().getBaseDirectory().getAbsolutePath())); popup.add(jmi); tree.setComponentPopupMenu(popup); } + void changeRootPath(final String path) { + ((DefaultMutableTreeNode) tree.getModel().getRoot()).removeAllChildren(); + tree.addTopLevelFoldersFrom(path); + tree.getModel().reload(); // this will collapse all nodes + expandImmediateNodes(); + } + + private void collapseAllNodes() { + for (int i = tree.getRowCount() - 1; i >= 0; i--) + tree.collapseRow(i); + } + + private void expandImmediateNodes() { + for (int i = tree.getRowCount() - 1; i >= 0; i--) + tree.expandRow(i); + } + private void showHelpMsg() { final String msg = "
" // + "

Overview

" // - + "

The File Explorer provides a direct view of selected folders. Changes in " // + + "

The File Explorer pane provides a direct view of selected folders. Changes in " // + "the native file system are synchronized in real time.

" // + "

Add/Remove Folders

" // + "

To add a folder, use the [+] button, or drag & drop folders from the native " // + "System Explorer. To remove a folder: select it, then use the [-] button. To reset " + "or reveal items: use the commands in the contextual popup menu.

" // + + "

Accessing Files & Paths

" // + + "

Double-click on a file to open it. Drag & drop items into the editor pane " + + "to paste their paths into the active script.

" // + "

Filtering Files

" // - + "

Filters affect only filenames (not folders) and are applied by typing a "// - + "filtering string and pressing [Enter]. Filters act only on files being listed, " // - + "and ignore collapsed folders. Examples of regex usage:

" // + + "

Filters affect filenames (not folders) and are applied by typing a filtering "// + + "string + [Enter]. Filters act only on files being listed, and ignore collapsed " // + + "folders. Examples of regex usage:

" // + "
" // - + " " // + " " // + " " // + " " // + " " // - + " " // - + " " // + " " // + " " // + " " // @@ -324,9 +346,8 @@ private void showHelpMsg() { + " " // + " " // + " " // - + " " // + "
PatternResult
py$Display filenames ending with py^DemoDisplay filenames starting with Demo
"; - JOptionPane.showMessageDialog(this, msg, "File Explorer", JOptionPane.PLAIN_MESSAGE); + JOptionPane.showMessageDialog(this, msg, "File Explorer Pane", JOptionPane.PLAIN_MESSAGE); } private boolean isCaseSensitive() { diff --git a/src/main/java/org/scijava/ui/swing/script/TextEditor.java b/src/main/java/org/scijava/ui/swing/script/TextEditor.java index d1f6fa55..d414e9ef 100644 --- a/src/main/java/org/scijava/ui/swing/script/TextEditor.java +++ b/src/main/java/org/scijava/ui/swing/script/TextEditor.java @@ -79,6 +79,7 @@ import java.nio.file.StandardOpenOption; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; @@ -92,8 +93,6 @@ import java.util.concurrent.ExecutionException; import java.util.jar.JarEntry; import java.util.jar.JarOutputStream; -import java.util.regex.Pattern; -import java.util.regex.PatternSyntaxException; import java.util.zip.ZipException; import javax.script.ScriptEngine; @@ -111,6 +110,7 @@ import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPanel; +import javax.swing.JPopupMenu; import javax.swing.JRadioButtonMenuItem; import javax.swing.JScrollPane; import javax.swing.JSplitPane; @@ -125,11 +125,11 @@ import javax.swing.event.DocumentListener; import javax.swing.text.BadLocationException; import javax.swing.text.Position; -import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.TreePath; import org.fife.ui.rsyntaxtextarea.AbstractTokenMakerFactory; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; +import org.fife.ui.rsyntaxtextarea.Theme; import org.fife.ui.rsyntaxtextarea.TokenMakerFactory; import org.scijava.Context; import org.scijava.app.AppService; @@ -184,6 +184,7 @@ public class TextEditor extends JFrame implements ActionListener, { private static final Set TEMPLATE_PATHS = new HashSet<>(); + private static final int BORDER_SIZE = 4; public static final String AUTO_IMPORT_PREFS = "script.editor.AutoImport"; public static final String WINDOW_HEIGHT = "script.editor.height"; public static final String WINDOW_WIDTH = "script.editor.width"; @@ -284,6 +285,15 @@ public TextEditor(final Context context) { context.inject(this); initializeTokenMakers(); + // NB: All panes must be initialized before menus are assembled! + tabbed = new JTabbedPane(); + tabbed.setBorder(BorderFactory.createEmptyBorder(0,BORDER_SIZE,0,BORDER_SIZE)); + tree = new FileSystemTree(log); + final JScrollPane scrolltree = new JScrollPane(new FileSystemTreePanel(tree, context)); + scrolltree.setBorder(BorderFactory.createEmptyBorder(0,BORDER_SIZE,0,BORDER_SIZE)); + scrolltree.setPreferredSize(new Dimension(200, 600)); + body = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, scrolltree, tabbed); + // -- BEGIN MENUS -- // Initialize menu @@ -621,15 +631,33 @@ public TextEditor(final Context context) { // -- END MENUS -- // Add the editor and output area - tabbed = new JTabbedPane(); tabbed.addChangeListener(this); + new FileDrop(tabbed, files -> { + final ArrayList filteredFiles = new ArrayList<>(); + assembleFlatFileCollection(filteredFiles, files); + if (filteredFiles.isEmpty()) { + JOptionPane.showMessageDialog(TextEditor.this, "None of the dropped file(s) seems parseable.", + "Invalid Drop", JOptionPane.WARNING_MESSAGE); + return; + } + final boolean confirm = filteredFiles.size() < 10 || (JOptionPane.showConfirmDialog(TextEditor.this, + "Confirm loading of " + filteredFiles.size()+ " items?", "Confirm?", + JOptionPane.OK_CANCEL_OPTION) == JOptionPane.OK_OPTION); + if (confirm) { + filteredFiles.forEach(f -> open(f)); + } + }); open(null); // make sure the editor pane is added + getContentPane().setLayout(new BoxLayout(getContentPane(), BoxLayout.Y_AXIS)); - //tabbed.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4)); - getContentPane().setLayout( - new BoxLayout(getContentPane(), BoxLayout.Y_AXIS)); + // Tweaks for JSplitPane + body.setOneTouchExpandable(true); + body.addPropertyChangeListener(evt -> { + if ("dividerLocation".equals(evt.getPropertyName())) saveWindowSizeToPrefs(); + }); - tree = new FileSystemTree(log); + // Tweaks for FileSystemTree + tree.addTopLevelFoldersFrom(getEditorPane().loadFolders()); // Restore top-level directories tree.setFont(tree.getFont().deriveFont(getEditorPane().getFontSize())); dragSource = new DragSource(); dragSource.createDefaultDragGestureRecognizer(tree, DnDConstants.ACTION_COPY, new DragAndDrop()); @@ -670,17 +698,43 @@ public TextEditor(final Context context) { } }); - // Restore top-level directories - tree.addTopLevelFoldersFrom(getEditorPane().loadFolders()); - - final JScrollPane scrolltree = new JScrollPane(new FileSystemTreePanel(tree)); - //scrolltree.setBackground(Color.white); // No hardwired constants for compatibility w/ dark themes - scrolltree.setBorder(BorderFactory.createEmptyBorder(0,5,0,5)); - scrolltree.setPreferredSize(new Dimension(200, 600)); - body = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, scrolltree, tabbed); - body.setOneTouchExpandable(true); - body.addPropertyChangeListener(evt -> { - if ("dividerLocation".equals(evt.getPropertyName())) saveWindowSizeToPrefs(); + // Tweaks for tabbed pane + final JPopupMenu popup = new JPopupMenu(); + tabbed.setComponentPopupMenu(popup); + final ButtonGroup bGroup = new ButtonGroup(); + for (final String pos : new String[] { "Top", "Left", "Bottom", "Right" }) { + final JMenuItem jcbmi = new JCheckBoxMenuItem("Place on " + pos, "Top".equals(pos)); + jcbmi.addItemListener(e -> { + switch (pos) { + case "Top": + tabbed.setTabPlacement(JTabbedPane.TOP); + break; + case "Bottom": + tabbed.setTabPlacement(JTabbedPane.BOTTOM); + break; + case "Left": + tabbed.setTabPlacement(JTabbedPane.LEFT); + break; + case "Right": + tabbed.setTabPlacement(JTabbedPane.RIGHT); + break; + } + }); + bGroup.add(jcbmi); + popup.add(jcbmi); + } + tabbed.addMouseWheelListener(e -> { + //https://stackoverflow.com/a/38463104 + final JTabbedPane pane = (JTabbedPane) e.getSource(); + final int units = e.getWheelRotation(); + final int oldIndex = pane.getSelectedIndex(); + final int newIndex = oldIndex + units; + if (newIndex < 0) + pane.setSelectedIndex(0); + else if (newIndex >= pane.getTabCount()) + pane.setSelectedIndex(pane.getTabCount() - 1); + else + pane.setSelectedIndex(newIndex); }); getContentPane().add(body); @@ -3037,6 +3091,19 @@ private static void addSeparator(final JMenu menu, final String header) { menu.add(label); } + private static Collection assembleFlatFileCollection(final Collection collection, final File[] files) { + if (files == null) return collection; // can happen while pressing 'Esc'!? + for (final File file : files) { + if (file == null || isBinary(file)) + continue; + else if (file.isDirectory()) + assembleFlatFileCollection(collection, file.listFiles()); + else //if (!file.isHidden()) + collection.add(file); + } + return collection; + } + private static Color getDisabledComponentColor() { try { return UIManager.getColor("MenuItem.disabledForeground"); diff --git a/src/main/java/org/scijava/ui/swing/script/TextEditorTab.java b/src/main/java/org/scijava/ui/swing/script/TextEditorTab.java index 7a1d31d4..8dd3b6f2 100644 --- a/src/main/java/org/scijava/ui/swing/script/TextEditorTab.java +++ b/src/main/java/org/scijava/ui/swing/script/TextEditorTab.java @@ -103,6 +103,7 @@ public void drop(DropTargetDropEvent e) { e.rejectDrop(); return; } + e.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE); // fix for InvalidDnDOperationException: No drop current Transferable t = e.getTransferable(); if (!t.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) return; try { From 759eb45612371ffa4e91f03311ad50e533dff2bc Mon Sep 17 00:00:00 2001 From: tferr Date: Tue, 22 Feb 2022 13:28:52 -0500 Subject: [PATCH 06/21] TextEditor: Revamp menus - Edit> - Organize by categories - Moved entries to Options> menu - Window> - Toggles for panels - Tab navigation (previous Tabs> menu) - Options> - Use titled dividers - Indention controls - Themes!! - Autocompletion controls - Prefs. controls - Help> - List of developer resources --- .../scijava/ui/swing/script/TextEditor.java | 229 +++++++++++++----- 1 file changed, 174 insertions(+), 55 deletions(-) diff --git a/src/main/java/org/scijava/ui/swing/script/TextEditor.java b/src/main/java/org/scijava/ui/swing/script/TextEditor.java index d414e9ef..a3058d8f 100644 --- a/src/main/java/org/scijava/ui/swing/script/TextEditor.java +++ b/src/main/java/org/scijava/ui/swing/script/TextEditor.java @@ -217,15 +217,16 @@ public class TextEditor extends JFrame implements ActionListener, openMacroFunctions, decreaseFontSize, increaseFontSize, chooseFontSize, chooseTabSize, gitGrep, replaceTabsWithSpaces, replaceSpacesWithTabs, toggleWhiteSpaceLabeling, zapGremlins, - savePreferences, toggleAutoCompletionMenu, toggleAutoCompletionKeyRequired, + savePreferences, toggleAutoCompletionMenu, openClassOrPackageHelp; private RecentFilesMenuItem openRecent; private JMenu gitMenu, tabsMenu, fontSizeMenu, tabSizeMenu, toolsMenu, - runMenu, whiteSpaceMenu; + runMenu; private int tabsMenuTabsStart; private Set tabsMenuItems; private FindAndReplaceDialog findDialog; - private JCheckBoxMenuItem autoSave, wrapLines, tabsEmulated, autoImport; + private JCheckBoxMenuItem autoSave, wrapLines, tabsEmulated, autoImport, + toggleAutoCompletionKeyRequired, paintTabs; private JTextArea errorScreen = new JTextArea(); private final FileSystemTree tree; @@ -360,43 +361,10 @@ public TextEditor(final Context context) { // clearScreen = addToMenu(edit, "Clear output panel", 0, 0); // clearScreen.setMnemonic(KeyEvent.VK_L); - zapGremlins = addToMenu(edit, "Zap Gremlins", 0, 0); - - edit.addSeparator(); - addImport = addToMenu(edit, "Add import...", 0, 0); - addImport.setMnemonic(KeyEvent.VK_I); - removeUnusedImports = addToMenu(edit, "Remove unused imports", 0, 0); - removeUnusedImports.setMnemonic(KeyEvent.VK_U); - sortImports = addToMenu(edit, "Sort imports", 0, 0); - sortImports.setMnemonic(KeyEvent.VK_S); - respectAutoImports = prefService.getBoolean(getClass(), AUTO_IMPORT_PREFS, false); - autoImport = - new JCheckBoxMenuItem("Auto-import (deprecated)", respectAutoImports); - autoImport.addItemListener(e -> { - respectAutoImports = e.getStateChange() == ItemEvent.SELECTED; - prefService.put(getClass(), AUTO_IMPORT_PREFS, respectAutoImports); - }); - edit.add(autoImport); - - whiteSpaceMenu = new JMenu("Whitespace"); - whiteSpaceMenu.setMnemonic(KeyEvent.VK_W); - removeTrailingWhitespace = - addToMenu(whiteSpaceMenu, "Remove trailing whitespace", 0, 0); + addSeparator(edit, "Utilities:"); + removeTrailingWhitespace = addToMenu(edit, "Remove Trailing Whitespace", 0, 0); removeTrailingWhitespace.setMnemonic(KeyEvent.VK_W); - replaceTabsWithSpaces = - addToMenu(whiteSpaceMenu, "Replace tabs with spaces", 0, 0); - replaceTabsWithSpaces.setMnemonic(KeyEvent.VK_S); - replaceSpacesWithTabs = - addToMenu(whiteSpaceMenu, "Replace spaces with tabs", 0, 0); - replaceSpacesWithTabs.setMnemonic(KeyEvent.VK_T); - toggleWhiteSpaceLabeling = new JRadioButtonMenuItem("Label whitespace"); - toggleWhiteSpaceLabeling.setMnemonic(KeyEvent.VK_L); - toggleWhiteSpaceLabeling.addActionListener(e -> { - getTextArea().setWhitespaceVisible(toggleWhiteSpaceLabeling.isSelected()); - }); - whiteSpaceMenu.add(toggleWhiteSpaceLabeling); - - edit.add(whiteSpaceMenu); + zapGremlins = addToMenu(edit, "Zap Gremlins", 0, 0); mbar.add(edit); @@ -493,6 +461,23 @@ public TextEditor(final Context context) { toolsMenu = new JMenu("Tools"); toolsMenu.setMnemonic(KeyEvent.VK_O); + addSeparator(toolsMenu, "Imports"); + addImport = addToMenu(toolsMenu, "Add Import...", 0, 0); + addImport.setMnemonic(KeyEvent.VK_I); + respectAutoImports = prefService.getBoolean(getClass(), AUTO_IMPORT_PREFS, false); + autoImport = + new JCheckBoxMenuItem("Auto-import (Deprecated)", respectAutoImports); + autoImport.addItemListener(e -> { + respectAutoImports = e.getStateChange() == ItemEvent.SELECTED; + prefService.put(getClass(), AUTO_IMPORT_PREFS, respectAutoImports); + }); + toolsMenu.add(autoImport); + removeUnusedImports = addToMenu(toolsMenu, "Remove Unused Imports", 0, 0); + removeUnusedImports.setMnemonic(KeyEvent.VK_U); + sortImports = addToMenu(toolsMenu, "Sort Imports", 0, 0); + sortImports.setMnemonic(KeyEvent.VK_S); + + addSeparator(toolsMenu, "Source & APIs:"); openHelpWithoutFrames = addToMenu(toolsMenu, "Open Help for Class...", 0, 0); openHelpWithoutFrames.setMnemonic(KeyEvent.VK_O); @@ -530,10 +515,24 @@ public TextEditor(final Context context) { gitGrep.setMnemonic(KeyEvent.VK_G); mbar.add(gitMenu); - // -- Tabs menu -- - - tabsMenu = new JMenu("Tabs"); - tabsMenu.setMnemonic(KeyEvent.VK_A); + // -- Window Menu (previously labeled as Tabs menu -- + tabsMenu = new JMenu("Window"); + tabsMenu.setMnemonic(KeyEvent.VK_W); + addSeparator(tabsMenu, "Panes:"); + final JCheckBoxMenuItem jcmi = new JCheckBoxMenuItem("File Explorer", true); + final int[] treePos = { body.getDividerLocation() }; + jcmi.addItemListener(e -> { + if (jcmi.isSelected()) { + body.setDividerLocation(treePos[0]); + } else { + // see https://stackoverflow.com/q/4934499 + treePos[0] = body.getDividerLocation(); + body.getLeftComponent().setMinimumSize(new Dimension()); + body.setDividerLocation(0.0d); + } + }); + tabsMenu.add(jcmi); + addSeparator(tabsMenu, "Tabs:"); nextTab = addToMenu(tabsMenu, "Next Tab", KeyEvent.VK_PAGE_DOWN, ctrl); nextTab.setMnemonic(KeyEvent.VK_N); previousTab = @@ -584,8 +583,16 @@ public TextEditor(final Context context) { fontSizeMenu.add(chooseFontSize); options.add(fontSizeMenu); - // Add tab size adjusting menu - tabSizeMenu = new JMenu("Tab sizes"); + addSeparator(options, "Indentation:"); + paintTabs = new JCheckBoxMenuItem("Indent Guides"); + paintTabs.setMnemonic(KeyEvent.VK_I); + paintTabs.addChangeListener(e -> getEditorPane().setPaintTabLines(paintTabs.getState())); + options.add(paintTabs); + tabsEmulated = new JCheckBoxMenuItem("Indent Using Spaces"); + tabsEmulated.setMnemonic(KeyEvent.VK_S); + tabsEmulated.addChangeListener(e -> getEditorPane().setTabsEmulated(tabsEmulated.getState())); + options.add(tabsEmulated); + tabSizeMenu = new JMenu("Tab Width"); tabSizeMenu.setMnemonic(KeyEvent.VK_T); final ButtonGroup bg = new ButtonGroup(); for (final int size : new int[] { 2, 4, 8 }) { @@ -604,29 +611,36 @@ public TextEditor(final Context context) { bg.add(chooseTabSize); tabSizeMenu.add(chooseTabSize); options.add(tabSizeMenu); + replaceSpacesWithTabs = addToMenu(options, "Replace Spaces With Tabs", 0, 0); + replaceTabsWithSpaces = addToMenu(options, "Replace Tabs With Spaces", 0, 0); - wrapLines = new JCheckBoxMenuItem("Wrap lines"); + addSeparator(options, "View:"); + toggleWhiteSpaceLabeling = new JCheckBoxMenuItem("Label Whitespace"); + toggleWhiteSpaceLabeling.setMnemonic(KeyEvent.VK_L); + toggleWhiteSpaceLabeling.addActionListener(e -> { + getTextArea().setWhitespaceVisible(toggleWhiteSpaceLabeling.isSelected()); + }); + options.add(toggleWhiteSpaceLabeling); + wrapLines = new JCheckBoxMenuItem("Wrap Lines"); + wrapLines.setMnemonic(KeyEvent.VK_W); wrapLines.addChangeListener(e -> getEditorPane().setLineWrap(wrapLines.getState())); options.add(wrapLines); + options.add(applyThemeMenu()); - // Add Tab inserts as spaces - tabsEmulated = new JCheckBoxMenuItem("Tab key inserts spaces"); - tabsEmulated.addChangeListener(e -> getEditorPane().setTabsEmulated(tabsEmulated.getState())); - options.add(tabsEmulated); - - toggleAutoCompletionMenu = new JCheckBoxMenuItem("Auto completion"); + addSeparator(options, "Code Completions:"); + toggleAutoCompletionMenu = new JCheckBoxMenuItem("Enable Autocompletion"); toggleAutoCompletionMenu.setSelected(prefService.getBoolean(TextEditor.class, "autoComplete", true)); toggleAutoCompletionMenu.addChangeListener(e -> toggleAutoCompletion()); options.add(toggleAutoCompletionMenu); - toggleAutoCompletionKeyRequired = new JCheckBoxMenuItem("Auto-completion Requires Ctrl+Space"); + toggleAutoCompletionKeyRequired = new JCheckBoxMenuItem("Show Completions Only With Ctrl+Space"); toggleAutoCompletionKeyRequired.setSelected(prefService.getBoolean(TextEditor.class, "autoCompleteKeyRequired", true)); toggleAutoCompletionKeyRequired.addChangeListener(e -> toggleAutoCompletionKeyRequired()); options.add(toggleAutoCompletionKeyRequired); options.addSeparator(); - savePreferences = addToMenu(options, "Save Preferences", 0, 0); - + options.add(getPrefsMenu()); mbar.add(options); + mbar.add(helpMenu()); // -- END MENUS -- @@ -1424,6 +1438,57 @@ private void toggleAutoCompletionKeyRequired() { prefService.put(TextEditor.class, "autoCompleteKeyRequired", toggleAutoCompletionKeyRequired.isSelected()); } + private JMenu applyThemeMenu() { + final LinkedHashMap map = new LinkedHashMap<>(); + map.put("Default", "default"); + map.put("-", "-"); + map.put("Dark", "dark"); + map.put("Druid", "druid"); + map.put("Eclipse (Light)", "eclipse"); + map.put("IntelliJ (Light)", "idea"); + map.put("Monokai", "monokai"); + map.put("Visual Studio (Light)", "vs"); + final JMenu menu = new JMenu("Theme"); + map.forEach((k, v) -> { + if ("-".equals(k)) { + menu.addSeparator(); + return; + } + final JMenuItem item = new JMenuItem(k); + item.addActionListener(e -> { + try { + applyTheme(v); + } catch (IllegalArgumentException ex) { + JOptionPane.showMessageDialog(TextEditor.this, + "An exception occured. Theme could not be loaded"); + ex.printStackTrace(); + } + }); + menu.add(item); + }); + return menu; + } + + /** + * Applies a theme to all the panes of this editor. + * + * @param theme either "default", "dark", "druid", "eclipse", "idea", "monokai", + * "vs" + * @throws IllegalArgumentException If {@code theme} is not a valid option, or + * the resource could not be loaded + */ + public void applyTheme(final String theme) throws IllegalArgumentException { + try { + final Theme th = Theme.load(getClass() + .getResourceAsStream("/org/fife/ui/rsyntaxtextarea/themes/" + theme + ".xml")); + for (int i = 0; i < tabbed.getTabCount(); i++) { + th.apply(getEditorPane(i)); + } + } catch (final Exception ex) { + throw new IllegalArgumentException(ex); + } + } + protected boolean handleTabsMenu(final Object source) { if (!(source instanceof JMenuItem)) return false; final JMenuItem item = (JMenuItem) source; @@ -3080,6 +3145,60 @@ private void changeFontSize(final JTextArea a, final float size) { a.setFont(a.getFont().deriveFont(size)); } + private JMenu getPrefsMenu() { + final JMenu menu = new JMenu("Preferences"); + JMenuItem item = new JMenuItem("Save"); + menu.add(item); + item.addActionListener(e -> { + getEditorPane().savePreferences(tree.getTopLevelFoldersString()); + write("Script Editor Settings Saved...\n"); + }); + item = new JMenuItem("Reset..."); + menu.add(item); + item.addActionListener(e -> { + final int choice = JOptionPane.showConfirmDialog(TextEditor.this, + "Reset preferences to defaults? (a restart may be required)", "Reset?", + JOptionPane.OK_CANCEL_OPTION); + if (JOptionPane.OK_OPTION == choice) { + prefService.clear(EditorPane.class); + write("Script Editor Settings Reset.\n"); + } + }); + return menu; + } + + private JMenu helpMenu() { + final JMenu menu = new JMenu("Help"); + //addSeparator(menu, "Online Resources"); + menu.add(helpMenuItem("Image.sc Forum ", "https://forum.image.sc/")); + menu.add(helpMenuItem("ImageJ Search Portal", "https://search.imagej.net/")); + menu.addSeparator(); + menu.add(helpMenuItem("SciJava Javadoc Portal", "https://javadoc.scijava.org/")); + menu.add(helpMenuItem("SciJava Maven Repository", "https://maven.scijava.org/")); + menu.addSeparator(); + menu.add(helpMenuItem("Fiji on GitHub", "https://github.com/fiji")); + menu.add(helpMenuItem("SciJava on GitHub", "https://github.com/scijava/")); + menu.addSeparator(); + menu.add(helpMenuItem("IJ1 Macro Functions", "https://imagej.nih.gov/ij/developer/macro/functions.html")); + menu.addSeparator(); + menu.add(helpMenuItem("ImageJ Docs: Development", "https://imagej.net/develop/")); + menu.add(helpMenuItem("ImageJ Docs: Scripting", "https://imagej.net/scripting/")); + menu.add(helpMenuItem("ImageJ Notebook Tutorials", "https://github.com/imagej/tutorials#readme")); + return menu; + } + + private JMenuItem helpMenuItem(final String label, final String url) { + final JMenuItem item = new JMenuItem(label); + item.addActionListener(e -> { + try { + platformService.open(new URL(url)); + } catch (final IOException ignored) { + error("Web page could not be open. " + "Please visit
" + url + "
using your web browser."); + } + }); + return item; + } + private static void addSeparator(final JMenu menu, final String header) { final JLabel label = new JLabel(header); // label.setHorizontalAlignment(SwingConstants.LEFT); From 1525e86224ab63f91988a25735540eb280dc8ce3 Mon Sep 17 00:00:00 2001 From: tferr Date: Tue, 22 Feb 2022 14:41:52 -0500 Subject: [PATCH 07/21] TextEditor: Code cleanup and misc improvements Especially 'contextual help' menu entries. Signed-off-by: Curtis Rueden --- .../scijava/ui/swing/script/TextEditor.java | 133 +++++++++--------- 1 file changed, 65 insertions(+), 68 deletions(-) diff --git a/src/main/java/org/scijava/ui/swing/script/TextEditor.java b/src/main/java/org/scijava/ui/swing/script/TextEditor.java index a3058d8f..0c3fd18e 100644 --- a/src/main/java/org/scijava/ui/swing/script/TextEditor.java +++ b/src/main/java/org/scijava/ui/swing/script/TextEditor.java @@ -178,6 +178,8 @@ * * @author Johannes Schindelin * @author Jonathan Hale + * @author Albert Cardona + * @author Tiago Ferreira */ public class TextEditor extends JFrame implements ActionListener, ChangeListener, CloseConfirmable, DocumentListener @@ -210,14 +212,14 @@ public class TextEditor extends JFrame implements ActionListener, close, undo, redo, cut, copy, paste, find, replace, selectAll, kill, gotoLine, makeJar, makeJarWithSource, removeUnusedImports, sortImports, removeTrailingWhitespace, findNext, findPrevious, openHelp, addImport, - //clearScreen, redundant with "Clear" JButton nextError, previousError, openHelpWithoutFrames, nextTab, previousTab, runSelection, extractSourceJar, toggleBookmark, - listBookmarks, openSourceForClass, openSourceForMenuItem, + listBookmarks, openSourceForClass, + //openSourceForMenuItem, // this never had an actionListener!?? openMacroFunctions, decreaseFontSize, increaseFontSize, chooseFontSize, chooseTabSize, gitGrep, replaceTabsWithSpaces, replaceSpacesWithTabs, toggleWhiteSpaceLabeling, zapGremlins, - savePreferences, toggleAutoCompletionMenu, + toggleAutoCompletionMenu, openClassOrPackageHelp; private RecentFilesMenuItem openRecent; private JMenu gitMenu, tabsMenu, fontSizeMenu, tabSizeMenu, toolsMenu, @@ -357,10 +359,6 @@ public TextEditor(final Context context) { listBookmarks = addToMenu(edit, "List Bookmarks...", 0, 0); listBookmarks.setMnemonic(KeyEvent.VK_O); - // This is redundant with 'Clear' JButton -// clearScreen = addToMenu(edit, "Clear output panel", 0, 0); -// clearScreen.setMnemonic(KeyEvent.VK_L); - addSeparator(edit, "Utilities:"); removeTrailingWhitespace = addToMenu(edit, "Remove Trailing Whitespace", 0, 0); removeTrailingWhitespace.setMnemonic(KeyEvent.VK_W); @@ -478,25 +476,12 @@ public TextEditor(final Context context) { sortImports.setMnemonic(KeyEvent.VK_S); addSeparator(toolsMenu, "Source & APIs:"); - openHelpWithoutFrames = - addToMenu(toolsMenu, "Open Help for Class...", 0, 0); - openHelpWithoutFrames.setMnemonic(KeyEvent.VK_O); - openHelp = - addToMenu(toolsMenu, "Open Help for Class (with frames)...", 0, 0); - openHelp.setMnemonic(KeyEvent.VK_P); - openClassOrPackageHelp = addToMenu(toolsMenu, "Source or javadoc for class or package...", 0, 0); - openClassOrPackageHelp.setMnemonic(KeyEvent.VK_S); - openMacroFunctions = - addToMenu(toolsMenu, "Open Help on Macro Functions...", 0, 0); - openMacroFunctions.setMnemonic(KeyEvent.VK_H); - extractSourceJar = addToMenu(toolsMenu, "Extract source .jar...", 0, 0); + extractSourceJar = addToMenu(toolsMenu, "Extract Source Jar...", 0, 0); extractSourceJar.setMnemonic(KeyEvent.VK_E); - openSourceForClass = - addToMenu(toolsMenu, "Open .java file for class...", 0, 0); + openSourceForClass = addToMenu(toolsMenu, "Open Java File for Class...", 0, 0); openSourceForClass.setMnemonic(KeyEvent.VK_J); - openSourceForMenuItem = - addToMenu(toolsMenu, "Open .java file for menu item...", 0, 0); - openSourceForMenuItem.setMnemonic(KeyEvent.VK_M); + //openSourceForMenuItem = addToMenu(toolsMenu, "Open Java File for Menu Item...", 0, 0); + //openSourceForMenuItem.setMnemonic(KeyEvent.VK_M); mbar.add(toolsMenu); // -- Git menu -- @@ -757,7 +742,6 @@ else if (newIndex >= pane.getTabCount()) addAccelerator(compileAndRun, KeyEvent.VK_F5, 0, true); addAccelerator(nextTab, KeyEvent.VK_PAGE_DOWN, ctrl, true); addAccelerator(previousTab, KeyEvent.VK_PAGE_UP, ctrl, true); - addAccelerator(increaseFontSize, KeyEvent.VK_EQUALS, ctrl | shift, true); // make sure that the window is not closed by accident @@ -1342,7 +1326,8 @@ else if (source == chooseTabSize) { commandService.run(ChooseTabSize.class, true, "editor", this); } else if (source == addImport) { - addImport(getSelectedClassNameOrAsk()); + addImport(getSelectedClassNameOrAsk("Add import (complete qualified name of class/package)", + "Which Class to Import?")); } else if (source == removeUnusedImports) new TokenFunctions(getTextArea()) .removeUnusedImports(); @@ -1361,9 +1346,6 @@ else if (source == replaceSpacesWithTabs) getTextArea() // else if (source == toggleAutoCompletionMenu) { // toggleAutoCompletion(); // } - else if (source == savePreferences) { - getEditorPane().savePreferences(tree.getTopLevelFoldersString()); - } else if (source == openHelp) openHelp(null); else if (source == openHelpWithoutFrames) openHelp(null, false); else if (source == openClassOrPackageHelp) openClassOrPackageHelp(null); @@ -1375,7 +1357,7 @@ else if (source == openMacroFunctions) try { } else if (source == extractSourceJar) extractSourceJar(); else if (source == openSourceForClass) { - final String className = getSelectedClassNameOrAsk(); + final String className = getSelectedClassNameOrAsk("Class (Fully qualified name):", "Which Class?"); if (className != null) { try { final String url = new FileFunctions(this).getSourceURL(className); @@ -1911,31 +1893,31 @@ void updateLanguageMenu(final ScriptLanguage language) { final boolean isCompileable = language != null && language.isCompiledLanguage(); - runMenu.setVisible(isRunnable); + runMenu.setEnabled(isRunnable); compileAndRun.setText(isCompileable ? "Compile and Run" : "Run"); compileAndRun.setEnabled(isRunnable); - runSelection.setVisible(isRunnable && !isCompileable); - compile.setVisible(isCompileable); - autoSave.setVisible(isCompileable); - makeJar.setVisible(isCompileable); - makeJarWithSource.setVisible(isCompileable); + runSelection.setEnabled(isRunnable && !isCompileable); + compile.setEnabled(isCompileable); + autoSave.setEnabled(isCompileable); + makeJar.setEnabled(isCompileable); + makeJarWithSource.setEnabled(isCompileable); final boolean isJava = language != null && language.getLanguageName().equals("Java"); - addImport.setVisible(isJava); - removeUnusedImports.setVisible(isJava); - sortImports.setVisible(isJava); - openSourceForMenuItem.setVisible(isJava); + addImport.setEnabled(isJava); + removeUnusedImports.setEnabled(isJava); + sortImports.setEnabled(isJava); + //openSourceForMenuItem.setEnabled(isJava); final boolean isMacro = language != null && language.getLanguageName().equals("ImageJ Macro"); - openMacroFunctions.setVisible(isMacro); - openSourceForClass.setVisible(!isMacro); + openMacroFunctions.setEnabled(isMacro); + openSourceForClass.setEnabled(!isMacro); - openHelp.setVisible(!isMacro && isRunnable); - openHelpWithoutFrames.setVisible(!isMacro && isRunnable); - nextError.setVisible(!isMacro && isRunnable); - previousError.setVisible(!isMacro && isRunnable); + openHelp.setEnabled(!isMacro && isRunnable); + openHelpWithoutFrames.setEnabled(!isMacro && isRunnable); + nextError.setEnabled(!isMacro && isRunnable); + previousError.setEnabled(!isMacro && isRunnable); final boolean isInGit = getEditorPane().getGitDirectory() != null; gitMenu.setVisible(isInGit); @@ -2466,19 +2448,19 @@ public void compile() { } } - public String getSelectedTextOrAsk(final String label) { + public String getSelectedTextOrAsk(final String msg, final String title) { String selection = getTextArea().getSelectedText(); if (selection == null || selection.indexOf('\n') >= 0) { - selection = - JOptionPane.showInputDialog(this, label + ":", label + "...", - JOptionPane.QUESTION_MESSAGE); - if (selection == null) return null; + selection = JOptionPane.showInputDialog(this, msg + "\nAlternatively, select appropriate text and re-run.", + title, JOptionPane.QUESTION_MESSAGE); + if (selection == null) + return null; } return selection; } - public String getSelectedClassNameOrAsk() { - String className = getSelectedTextOrAsk("Class name"); + public String getSelectedClassNameOrAsk(final String msg, final String title) { + String className = getSelectedTextOrAsk(msg, title); if (className != null) className = className.trim(); return className; } @@ -2640,7 +2622,7 @@ public void openHelp(final String className) { * @param withFrames */ public void openHelp(String className, final boolean withFrames) { - if (className == null) className = getSelectedClassNameOrAsk(); + if (className == null) className = getSelectedClassNameOrAsk("Class (fully qualified name):", "Online Javadocs..."); if (className == null) return; final Class c = Types.load(className, false); @@ -2704,7 +2686,7 @@ public void openHelp(String className, final boolean withFrames) { */ public void openClassOrPackageHelp(String text) { if (text == null) - text = getSelectedClassNameOrAsk(); + text = getSelectedClassNameOrAsk("Class or package (complete or partial name):", "Which Class/Package?"); if (null == text) return; new Thread(new FindClassSourceAndJavadoc(text)).start(); // fork away from event dispatch thread } @@ -2724,7 +2706,11 @@ public void run() { setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); } if (matches.isEmpty()) { - JOptionPane.showMessageDialog(getEditorPane(), "No info found for:\n'" + text +'"'); + if (JOptionPane.showConfirmDialog(TextEditor.this, + "No info found for:\n'" + text + "'\nSearch for it on the web?", "Search the Web?", + JOptionPane.OK_CANCEL_OPTION) == JOptionPane.OK_OPTION) { + openURL("https://duckduckgo.com/?q=" + text.trim().replace(" ", "+")); + } return; } final JPanel panel = new JPanel(); @@ -2830,7 +2816,7 @@ public void writeError(String message) { } private void error(final String message) { - JOptionPane.showMessageDialog(this, message); + JOptionPane.showMessageDialog(this, message, "Error", JOptionPane.ERROR_MESSAGE); } public void handleException(final Throwable e) { @@ -3169,10 +3155,19 @@ private JMenu getPrefsMenu() { private JMenu helpMenu() { final JMenu menu = new JMenu("Help"); - //addSeparator(menu, "Online Resources"); + addSeparator(menu, "Contextual Help:"); + openHelpWithoutFrames = addToMenu(menu, " Open Help for Class...", 0, 0); + openHelpWithoutFrames.setMnemonic(KeyEvent.VK_O); + openHelp = addToMenu(menu, " Open Help for Class (With Frames)...", 0, 0); + openHelp.setMnemonic(KeyEvent.VK_P); + openClassOrPackageHelp = addToMenu(menu, " Lookup Class or Package...", 0, 0); + openClassOrPackageHelp.setMnemonic(KeyEvent.VK_S); + openMacroFunctions = addToMenu(menu, " Open Help on Macro Function(s)...", 0, 0); + openMacroFunctions.setMnemonic(KeyEvent.VK_H); + addSeparator(menu, "Online Resources:"); menu.add(helpMenuItem("Image.sc Forum ", "https://forum.image.sc/")); menu.add(helpMenuItem("ImageJ Search Portal", "https://search.imagej.net/")); - menu.addSeparator(); + //menu.addSeparator(); menu.add(helpMenuItem("SciJava Javadoc Portal", "https://javadoc.scijava.org/")); menu.add(helpMenuItem("SciJava Maven Repository", "https://maven.scijava.org/")); menu.addSeparator(); @@ -3180,25 +3175,27 @@ private JMenu helpMenu() { menu.add(helpMenuItem("SciJava on GitHub", "https://github.com/scijava/")); menu.addSeparator(); menu.add(helpMenuItem("IJ1 Macro Functions", "https://imagej.nih.gov/ij/developer/macro/functions.html")); - menu.addSeparator(); menu.add(helpMenuItem("ImageJ Docs: Development", "https://imagej.net/develop/")); menu.add(helpMenuItem("ImageJ Docs: Scripting", "https://imagej.net/scripting/")); + menu.addSeparator(); menu.add(helpMenuItem("ImageJ Notebook Tutorials", "https://github.com/imagej/tutorials#readme")); return menu; } private JMenuItem helpMenuItem(final String label, final String url) { - final JMenuItem item = new JMenuItem(label); - item.addActionListener(e -> { - try { - platformService.open(new URL(url)); - } catch (final IOException ignored) { - error("Web page could not be open. " + "Please visit
" + url + "
using your web browser."); - } - }); + final JMenuItem item = new JMenuItem(" " +label); // indent entries + item.addActionListener(e -> openURL(url)); return item; } + private void openURL(final String url) { + try { + platformService.open(new URL(url)); + } catch (final IOException ignored) { + error("Web page could not be open. " + "Please visit
" + url + "
using your web browser."); + } + } + private static void addSeparator(final JMenu menu, final String header) { final JLabel label = new JLabel(header); // label.setHorizontalAlignment(SwingConstants.LEFT); From 4f5c3efe0b3fd9389ec0f22afee3e313dac22f98 Mon Sep 17 00:00:00 2001 From: tferr Date: Tue, 22 Feb 2022 14:55:30 -0500 Subject: [PATCH 08/21] Remove hardwired settings and code cleanup --- .../ui/swing/script/FileSystemTreePanel.java | 2 +- .../scijava/ui/swing/script/TextEditor.java | 55 ++++++------ .../ui/swing/script/TextEditorTab.java | 83 +++++++------------ 3 files changed, 57 insertions(+), 83 deletions(-) diff --git a/src/main/java/org/scijava/ui/swing/script/FileSystemTreePanel.java b/src/main/java/org/scijava/ui/swing/script/FileSystemTreePanel.java index 50a4d9e9..8e4ee088 100644 --- a/src/main/java/org/scijava/ui/swing/script/FileSystemTreePanel.java +++ b/src/main/java/org/scijava/ui/swing/script/FileSystemTreePanel.java @@ -62,7 +62,7 @@ /** * Convenience class for displaying a {@link FileSystemTree} with some bells and - * whistles, incliding a filter toolbar. + * whistles, including a filter toolbar. * * @author Albert Cardona * @author Tiago Ferreira diff --git a/src/main/java/org/scijava/ui/swing/script/TextEditor.java b/src/main/java/org/scijava/ui/swing/script/TextEditor.java index 0c3fd18e..d08dafe7 100644 --- a/src/main/java/org/scijava/ui/swing/script/TextEditor.java +++ b/src/main/java/org/scijava/ui/swing/script/TextEditor.java @@ -683,7 +683,7 @@ public TextEditor(final Context context) { "Could not open the file at: " + f.getAbsolutePath()); return; } - catch (Exception e) { + catch (final Exception e) { log.error(e); error("Could not open image at " + f); } @@ -768,6 +768,7 @@ public void windowGainedFocus(final WindowEvent e) { } }); + // Tweaks for Console errorScreen.setFont(getEditorPane().getFont()); errorScreen.setEditable(false); errorScreen.setLineWrap(true); @@ -811,19 +812,19 @@ public void componentResized(final ComponentEvent e) { final EditorPane editorPane = getEditorPane(); editorPane.requestFocus(); } - + private class DragAndDrop implements DragSourceListener, DragGestureListener { @Override - public void dragDropEnd(DragSourceDropEvent dsde) {} + public void dragDropEnd(final DragSourceDropEvent dsde) {} @Override - public void dragEnter(DragSourceDragEvent dsde) { + public void dragEnter(final DragSourceDragEvent dsde) { dsde.getDragSourceContext().setCursor(DragSource.DefaultMoveNoDrop); } @Override - public void dragGestureRecognized(DragGestureEvent dge) { - TreePath path = tree.getSelectionPath(); + public void dragGestureRecognized(final DragGestureEvent dge) { + final TreePath path = tree.getSelectionPath(); if (path == null) // nothing is currently selected return; final String filepath = (String)((FileSystemTree.Node) path.getLastPathComponent()).getUserObject(); @@ -839,7 +840,7 @@ public DataFlavor[] getTransferDataFlavors() { } @Override - public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException { + public Object getTransferData(final DataFlavor flavor) throws UnsupportedFlavorException, IOException { if (isDataFlavorSupported(flavor)) return Arrays.asList(new String[]{filepath}); return null; @@ -848,12 +849,12 @@ public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorExcepti } @Override - public void dragExit(DragSourceEvent dse) { + public void dragExit(final DragSourceEvent dse) { dse.getDragSourceContext().setCursor(DragSource.DefaultMoveNoDrop); } @Override - public void dragOver(DragSourceDragEvent dsde) { + public void dragOver(final DragSourceDragEvent dsde) { if (tree == dsde.getSource()) { dsde.getDragSourceContext().setCursor(DragSource.DefaultCopyNoDrop); } else if (dsde.getDropAction() == DnDConstants.ACTION_COPY) { @@ -864,7 +865,7 @@ public void dragOver(DragSourceDragEvent dsde) { } @Override - public void dropActionChanged(DragSourceDragEvent dsde) {} + public void dropActionChanged(final DragSourceDragEvent dsde) {} } public LogService log() { return log; } @@ -1440,7 +1441,7 @@ private JMenu applyThemeMenu() { item.addActionListener(e -> { try { applyTheme(v); - } catch (IllegalArgumentException ex) { + } catch (final IllegalArgumentException ex) { JOptionPane.showMessageDialog(TextEditor.this, "An exception occured. Theme could not be loaded"); ex.printStackTrace(); @@ -1642,7 +1643,7 @@ public TextEditorTab open(final File file) { if (isBinary(file)) { try { uiService.show(ioService.open(file.getAbsolutePath())); - } catch (IOException e) { + } catch (final IOException e) { log.error(e); } return null; @@ -1650,7 +1651,7 @@ public TextEditorTab open(final File file) { try { TextEditorTab tab = (tabbed.getTabCount() == 0) ? null : getTab(); - TextEditorTab prior = tab; + final TextEditorTab prior = tab; final boolean wasNew = tab != null && tab.editorPane.isNew(); float font_size = 0; // to set the new editor's font like the last active one, if any if (!wasNew) { @@ -2340,7 +2341,7 @@ private void writePromptLog(final ScriptLanguage language, final String text) { final String path = getPromptCommandsFilename(language); final File file = new File(path); try { - boolean exists = file.exists(); + final boolean exists = file.exists(); if (!exists) { // Ensure parent directories exist file.getParentFile().mkdirs(); @@ -2348,7 +2349,7 @@ private void writePromptLog(final ScriptLanguage language, final String text) { } Files.write(Paths.get(path), Arrays.asList(new String[]{text, "#"}), Charset.forName("UTF-8"), StandardOpenOption.APPEND, StandardOpenOption.DSYNC); - } catch (IOException e) { + } catch (final IOException e) { log.error("Failed to write executed prompt command to file " + path, e); } } @@ -2374,11 +2375,11 @@ private ArrayList loadPromptLog(final ScriptLanguage language) { final String sep = System.getProperty("line.separator"); // used fy Files.write above commands.addAll(Arrays.asList(new String(bytes, Charset.forName("UTF-8")).split(sep + "#" + sep))); if (0 == commands.get(commands.size()-1).length()) commands.remove(commands.size() -1); // last entry is empty - } catch (IOException e) { + } catch (final IOException e) { log.error("Failed to read history of prompt commands from file " + path, e); return lines; } finally { - try { if (null != ra) ra.close(); } catch (IOException e) { log.error(e); } + try { if (null != ra) ra.close(); } catch (final IOException e) { log.error(e); } } if (commands.size() > 1000) { commands = commands.subList(commands.size() - 1000, commands.size()); @@ -2394,7 +2395,7 @@ private ArrayList loadPromptLog(final ScriptLanguage language) { if (!new File(path + "-tmp").renameTo(new File(path))) { log.error("Could not rename command log file " + path + "-tmp to " + path); } - } catch (Exception e) { + } catch (final Exception e) { log.error("Failed to crop history of prompt commands file " + path, e); } } @@ -2717,7 +2718,7 @@ public void run() { final GridBagLayout gridbag = new GridBagLayout(); final GridBagConstraints c = new GridBagConstraints(); panel.setLayout(gridbag); - panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + panel.setBorder(BorderFactory.createEmptyBorder(BORDER_SIZE, BORDER_SIZE, BORDER_SIZE, BORDER_SIZE)); final List keys = new ArrayList(matches.keySet()); Collections.sort(keys); c.gridy = 0; @@ -2741,13 +2742,11 @@ public void run() { final JButton link = new JButton(title); gridbag.setConstraints(link, c); panel.add(link); - link.addActionListener(new ActionListener() { - public void actionPerformed(final ActionEvent event) { - try { - platformService.open(new URL(url)); - } catch (Exception e) { - e.printStackTrace(); - } + link.addActionListener(event -> { + try { + platformService.open(new URL(url)); + } catch (final Exception e) { + e.printStackTrace(); } }); } @@ -2920,7 +2919,7 @@ private Reader evalScript(final String filename, Reader reader, try { // Same engine, with persistent state this.scriptInfo.setScript( reader ); - } catch (IOException e) { + } catch (final IOException e) { log.error(e); } } @@ -2988,7 +2987,7 @@ public void keyPressed(final KeyEvent ke) { execute(getTab(), text, true); prompt.setText(""); screen.scrollRectToVisible(screen.modelToView(screen.getDocument().getLength())); - } catch (Throwable t) { + } catch (final Throwable t) { log.error(t); } ke.consume(); // avoid writing the line break diff --git a/src/main/java/org/scijava/ui/swing/script/TextEditorTab.java b/src/main/java/org/scijava/ui/swing/script/TextEditorTab.java index 8dd3b6f2..14f71422 100644 --- a/src/main/java/org/scijava/ui/swing/script/TextEditorTab.java +++ b/src/main/java/org/scijava/ui/swing/script/TextEditorTab.java @@ -30,7 +30,6 @@ package org.scijava.ui.swing.script; import java.awt.Dimension; -import java.awt.Font; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.datatransfer.DataFlavor; @@ -67,7 +66,6 @@ public class TextEditorTab extends JSplitPane { private static final String DOWN_ARROW = "\u25BC"; - private static final String RIGHT_ARROW = "\u25B6"; protected final EditorPane editorPane; @@ -95,16 +93,16 @@ public TextEditorTab(final TextEditor textEditor) { editorPane = new EditorPane(); dropTargetListener = new DropTargetListener() { @Override - public void dropActionChanged(DropTargetDragEvent arg0) {} + public void dropActionChanged(final DropTargetDragEvent arg0) {} @Override - public void drop(DropTargetDropEvent e) { + public void drop(final DropTargetDropEvent e) { if (e.getDropAction() != DnDConstants.ACTION_COPY) { e.rejectDrop(); return; } e.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE); // fix for InvalidDnDOperationException: No drop current - Transferable t = e.getTransferable(); + final Transferable t = e.getTransferable(); if (!t.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) return; try { final Object o = t.getTransferData(DataFlavor.javaFileListFlavor); @@ -112,7 +110,7 @@ public void drop(DropTargetDropEvent e) { final List list = (List) o; if (list.isEmpty()) return; String path; - Object first = list.get(0); + final Object first = list.get(0); if (first instanceof String) path = (String) first; else if (first instanceof File) path = ((File) first).getAbsolutePath(); else return; @@ -120,29 +118,30 @@ public void drop(DropTargetDropEvent e) { // Point p = e.getLocation(); // ... but it is more predictable (less surprising) to insert where the caret is: editorPane.getRSyntaxDocument().insertString(editorPane.getCaretPosition(), path, null); - } catch (Exception ex) { + } catch (final Exception ex) { ex.printStackTrace(); } } @Override - public void dragOver(DropTargetDragEvent e) { + public void dragOver(final DropTargetDragEvent e) { if (e.getDropAction() != DnDConstants.ACTION_COPY) e.rejectDrag(); } @Override - public void dragExit(DropTargetEvent e) {} + public void dragExit(final DropTargetEvent e) {} @Override - public void dragEnter(DropTargetDragEvent e) { + public void dragEnter(final DropTargetDragEvent e) { if (e.getDropAction() != DnDConstants.ACTION_COPY) e.rejectDrag(); } }; dropTarget = new DropTarget(editorPane, DnDConstants.ACTION_COPY, dropTargetListener); + // tweaks for console screen.setEditable(false); screen.setLineWrap(true); - screen.setFont(new Font("Courier", Font.PLAIN, 12)); + screen.setFont(getEditorPane().getFont()); final JPanel bottom = new JPanel(); bottom.setLayout(new GridBagLayout()); @@ -156,38 +155,21 @@ public void dragEnter(DropTargetDragEvent e) { bc.fill = GridBagConstraints.NONE; runit = new JButton("Run"); runit.setToolTipText("control + R"); - runit.addActionListener(new ActionListener() { - - @Override - public void actionPerformed(final ActionEvent ae) { - textEditor.runText(); - } - }); + runit.addActionListener(ae -> textEditor.runText()); bottom.add(runit, bc); bc.gridx = 1; batchit = new JButton("Batch"); - batchit.addActionListener(new ActionListener() { - - @Override - public void actionPerformed(final ActionEvent ae) { - textEditor.runBatch(); - } - }); + batchit.setToolTipText("Requires at least one @File SciJava parameter to be declared"); + batchit.addActionListener(e -> textEditor.runBatch()); bottom.add(batchit, bc); bc.gridx = 2; killit = new JButton("Kill"); killit.setEnabled(false); - killit.addActionListener(new ActionListener() { - - @Override - public void actionPerformed(final ActionEvent ae) { - kill(); - } - }); + killit.addActionListener(ae -> kill()); bottom.add(killit, bc); - + bc.gridx = 3; incremental = new JCheckBox("Persistent"); incremental.setEnabled(true); @@ -220,7 +202,7 @@ public void actionPerformed(final ActionEvent ae) { switchSplit.setToolTipText("Switch location"); switchSplit.addActionListener(new ActionListener() { @Override - public void actionPerformed(ActionEvent e) { + public void actionPerformed(final ActionEvent e) { if (DOWN_ARROW.equals(switchSplit.getText())) { TextEditorTab.this.setOrientation(JSplitPane.VERTICAL_SPLIT); } else { @@ -241,10 +223,6 @@ public void actionPerformed(ActionEvent e) { bc.weightx = 1; bc.weighty = 1; bc.gridwidth = 8; - screen.setEditable(false); - screen.setLineWrap(true); - final Font font = new Font("Courier", Font.PLAIN, 12); - screen.setFont(font); scroll = new JScrollPane(screen); bottom.add(scroll, bc); @@ -278,22 +256,19 @@ public void actionPerformed(ActionEvent e) { bc.gridx = 3; final JButton prompt_help = new JButton("?"); - prompt_help.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent a) { - final String msg = "This REPL (read-evaluate-print-loop) parses " + textEditor.getCurrentLanguage().getLanguageName() + " code.\n\n" - + "Key bindings:\n" - + "* enter: evaluate code\n" - + "* shift+enter: add line break (also alt-enter and meta-enter)\n" - + "* page UP or ctrl+p: show previous entry in the history\n" - + "* page DOWN or ctrl+n: show next entry in the history\n" - + "\n" - + "If 'Use arrow keys' is checked, then up/down arrows work like page UP/DOWN,\n" - + "and shift+up/down arrow work like arrow keys before for caret movement\n" - + "within a multi-line prompt." - ; - JOptionPane.showMessageDialog(textEditor, msg, "REPL Help", JOptionPane.INFORMATION_MESSAGE); - } + prompt_help.addActionListener(a -> { + final String msg = "This REPL (read-evaluate-print-loop) parses " + textEditor.getCurrentLanguage().getLanguageName() + " code.\n\n" + + "Key bindings:\n" + + "* enter: evaluate code\n" + + "* shift+enter: add line break (also alt-enter and meta-enter)\n" + + "* page UP or ctrl+p: show previous entry in the history\n" + + "* page DOWN or ctrl+n: show next entry in the history\n" + + "\n" + + "If 'Use arrow keys' is checked, then up/down arrows work like page UP/DOWN,\n" + + "and shift+up/down arrow work like arrow keys before for caret movement\n" + + "within a multi-line prompt." + ; + JOptionPane.showMessageDialog(textEditor, msg, "REPL Help", JOptionPane.INFORMATION_MESSAGE); }); prompt_panel.add(prompt_help, bc); From 7d1cbf43bedcc7cef3bd42738a9e2549fd584c3f Mon Sep 17 00:00:00 2001 From: tferr Date: Tue, 22 Feb 2022 16:55:27 -0500 Subject: [PATCH 09/21] Code cleanup + Extend preferences to new options --- .../scijava/ui/swing/script/EditorPane.java | 79 +++++++--- .../scijava/ui/swing/script/TextEditor.java | 142 +++++++++++------- 2 files changed, 148 insertions(+), 73 deletions(-) diff --git a/src/main/java/org/scijava/ui/swing/script/EditorPane.java b/src/main/java/org/scijava/ui/swing/script/EditorPane.java index 19264749..614f3afd 100644 --- a/src/main/java/org/scijava/ui/swing/script/EditorPane.java +++ b/src/main/java/org/scijava/ui/swing/script/EditorPane.java @@ -93,6 +93,8 @@ public class EditorPane extends RSyntaxTextArea implements DocumentListener { private boolean undoInProgress; private boolean redoInProgress; + private boolean autoCompletionEnabled = true; + private boolean autoCompletionNoKeyRequired = false; @Parameter Context context; @@ -111,8 +113,18 @@ public class EditorPane extends RSyntaxTextArea implements DocumentListener { * Constructor. */ public EditorPane() { - setLineWrap(false); - setTabSize(8); + + // set sensible defaults + setAntiAliasingEnabled(true); + setAutoIndentEnabled(true); + setBracketMatchingEnabled(true); + setCloseCurlyBraces(true); + setCloseMarkupTags(true); + setCodeFoldingEnabled(true); + setShowMatchedBracketPopup(true); + + // load preferences + loadPreferences(); getActionMap() .put(DefaultEditorKit.nextWordAction, wordMovement(+1, false)); @@ -517,18 +529,21 @@ protected void setLanguage(final ScriptLanguage language, } } - private boolean autoCompletionEnabled = true; public void setAutoCompletionEnabled(boolean value) { autoCompletionEnabled = value; - setLanguage(currentLanguage); + if (currentLanguage != null) setLanguage(currentLanguage); } - private boolean autoCompletionKeyRequired = false; - void setAutoCompletionKeyRequired(boolean value) { - autoCompletionKeyRequired = value; + void setAutoCompletionNoKeyRequired(boolean value) { + autoCompletionNoKeyRequired = value; } - public boolean getAutoCompletionKeyRequired() { - return autoCompletionKeyRequired; + + public boolean isAutoCompletionEnabled() { + return autoCompletionNoKeyRequired; + } + + public boolean isAutoCompletionNoKeyRequired() { + return autoCompletionNoKeyRequired; } /** @@ -717,21 +732,44 @@ public void convertSpacesToTabs() { public static final String LINE_WRAP_PREFS = "script.editor.WrapLines"; public static final String TAB_SIZE_PREFS = "script.editor.TabSize"; public static final String TABS_EMULATED_PREFS = "script.editor.TabsEmulated"; + public static final String WHITESPACE_VISIBLE_PREFS = "script.editor.Whitespace"; + public static final String TABLINES_VISIBLE_PREFS = "script.editor.Tablines"; + public static final String THEME_PREFS = "script.editor.theme"; + public static final String AUTOCOMPLETE_PREFS = "script.editor.Autocomp"; + public static final String AUTOCOMPLETE_NOKEY_PREFS = "script.editor.AutocompKey"; public static final String FOLDERS_PREFS = "script.editor.folders"; - public static final int DEFAULT_TAB_SIZE = 4; + public static final String DEFAULT_THEME = "default"; /** - * Loads the preferences for the Tab and apply them. + * Loads and applies the preferences for the tab (theme excluded). + * @see TextEditor#applyTheme(String) */ public void loadPreferences() { - resetTabSize(); - setFontSize(prefService.getFloat(getClass(), FONT_SIZE_PREFS, getFontSize())); - setLineWrap(prefService.getBoolean(getClass(), LINE_WRAP_PREFS, getLineWrap())); - setTabsEmulated(prefService.getBoolean(getClass(), TABS_EMULATED_PREFS, - getTabsEmulated())); + if (prefService == null) { + setLineWrap(false); + setTabSize(DEFAULT_TAB_SIZE); + setLineWrap(false); + setTabsEmulated(false); + setPaintTabLines(false); + setAutoCompletionEnabled(true); + setAutoCompletionNoKeyRequired(false); + } else { + resetTabSize(); + setFontSize(prefService.getFloat(getClass(), FONT_SIZE_PREFS, getFontSize())); + setLineWrap(prefService.getBoolean(getClass(), LINE_WRAP_PREFS, getLineWrap())); + setTabsEmulated(prefService.getBoolean(getClass(), TABS_EMULATED_PREFS, getTabsEmulated())); + setWhitespaceVisible(prefService.getBoolean(getClass(), WHITESPACE_VISIBLE_PREFS, isWhitespaceVisible())); + setPaintTabLines(prefService.getBoolean(getClass(), TABLINES_VISIBLE_PREFS, getPaintTabLines())); + setAutoCompletionEnabled(prefService.getBoolean(getClass(), AUTOCOMPLETE_PREFS, true)); + setAutoCompletionNoKeyRequired(prefService.getBoolean(getClass(), AUTOCOMPLETE_NOKEY_PREFS, false)); + } } - + + public String themeName() { + return prefService.get(getClass(), THEME_PREFS, DEFAULT_THEME); + } + public String loadFolders() { return prefService.get(getClass(), FOLDERS_PREFS, System.getProperty("user.home")); } @@ -739,12 +777,17 @@ public String loadFolders() { /** * Retrieves and saves the preferences to the persistent store */ - public void savePreferences(final String top_folders) { + public void savePreferences(final String top_folders, final String theme) { prefService.put(getClass(), TAB_SIZE_PREFS, getTabSize()); prefService.put(getClass(), FONT_SIZE_PREFS, getFontSize()); prefService.put(getClass(), LINE_WRAP_PREFS, getLineWrap()); prefService.put(getClass(), TABS_EMULATED_PREFS, getTabsEmulated()); + prefService.put(getClass(), WHITESPACE_VISIBLE_PREFS, isWhitespaceVisible()); + prefService.put(getClass(), TABLINES_VISIBLE_PREFS, getPaintTabLines()); + prefService.put(getClass(), AUTOCOMPLETE_PREFS, isAutoCompletionEnabled()); + prefService.put(getClass(), AUTOCOMPLETE_NOKEY_PREFS, isAutoCompletionNoKeyRequired()); if (null != top_folders) prefService.put(getClass(), FOLDERS_PREFS, top_folders); + if (null != theme) prefService.put(getClass(), THEME_PREFS, theme); } /** diff --git a/src/main/java/org/scijava/ui/swing/script/TextEditor.java b/src/main/java/org/scijava/ui/swing/script/TextEditor.java index d08dafe7..6588cf7e 100644 --- a/src/main/java/org/scijava/ui/swing/script/TextEditor.java +++ b/src/main/java/org/scijava/ui/swing/script/TextEditor.java @@ -218,9 +218,7 @@ public class TextEditor extends JFrame implements ActionListener, //openSourceForMenuItem, // this never had an actionListener!?? openMacroFunctions, decreaseFontSize, increaseFontSize, chooseFontSize, chooseTabSize, gitGrep, replaceTabsWithSpaces, - replaceSpacesWithTabs, toggleWhiteSpaceLabeling, zapGremlins, - toggleAutoCompletionMenu, - openClassOrPackageHelp; + replaceSpacesWithTabs, zapGremlins,openClassOrPackageHelp; private RecentFilesMenuItem openRecent; private JMenu gitMenu, tabsMenu, fontSizeMenu, tabSizeMenu, toolsMenu, runMenu; @@ -228,9 +226,9 @@ public class TextEditor extends JFrame implements ActionListener, private Set tabsMenuItems; private FindAndReplaceDialog findDialog; private JCheckBoxMenuItem autoSave, wrapLines, tabsEmulated, autoImport, - toggleAutoCompletionKeyRequired, paintTabs; + autoCompletionKey, autoCompletion, paintTabs, whiteSpace; private JTextArea errorScreen = new JTextArea(); - + private final FileSystemTree tree; private final JSplitPane body; @@ -239,6 +237,8 @@ public class TextEditor extends JFrame implements ActionListener, private ErrorHandler errorHandler; private boolean respectAutoImports; + private String activeTheme; + @Parameter private Context context; @@ -297,6 +297,9 @@ public TextEditor(final Context context) { scrolltree.setPreferredSize(new Dimension(200, 600)); body = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, scrolltree, tabbed); + // These items are dynamic and need to be initialized before EditorPane creation + initializeDynamicMenuComponents(); + // -- BEGIN MENUS -- // Initialize menu @@ -600,27 +603,13 @@ public TextEditor(final Context context) { replaceTabsWithSpaces = addToMenu(options, "Replace Tabs With Spaces", 0, 0); addSeparator(options, "View:"); - toggleWhiteSpaceLabeling = new JCheckBoxMenuItem("Label Whitespace"); - toggleWhiteSpaceLabeling.setMnemonic(KeyEvent.VK_L); - toggleWhiteSpaceLabeling.addActionListener(e -> { - getTextArea().setWhitespaceVisible(toggleWhiteSpaceLabeling.isSelected()); - }); - options.add(toggleWhiteSpaceLabeling); - wrapLines = new JCheckBoxMenuItem("Wrap Lines"); - wrapLines.setMnemonic(KeyEvent.VK_W); - wrapLines.addChangeListener(e -> getEditorPane().setLineWrap(wrapLines.getState())); + options.add(whiteSpace); options.add(wrapLines); options.add(applyThemeMenu()); addSeparator(options, "Code Completions:"); - toggleAutoCompletionMenu = new JCheckBoxMenuItem("Enable Autocompletion"); - toggleAutoCompletionMenu.setSelected(prefService.getBoolean(TextEditor.class, "autoComplete", true)); - toggleAutoCompletionMenu.addChangeListener(e -> toggleAutoCompletion()); - options.add(toggleAutoCompletionMenu); - toggleAutoCompletionKeyRequired = new JCheckBoxMenuItem("Show Completions Only With Ctrl+Space"); - toggleAutoCompletionKeyRequired.setSelected(prefService.getBoolean(TextEditor.class, "autoCompleteKeyRequired", true)); - toggleAutoCompletionKeyRequired.addChangeListener(e -> toggleAutoCompletionKeyRequired()); - options.add(toggleAutoCompletionKeyRequired); + options.add(autoCompletion); + options.add(autoCompletionKey); options.addSeparator(); options.add(getPrefsMenu()); @@ -810,7 +799,11 @@ public void componentResized(final ComponentEvent e) { open(null); final EditorPane editorPane = getEditorPane(); + // Apply preferences that have not yet been set + updateUI(true); + applyTheme(editorPane.themeName()); editorPane.requestFocus(); + } private class DragAndDrop implements DragSourceListener, DragGestureListener { @@ -894,6 +887,37 @@ private synchronized void initializeTokenMakers() { } } + private void initializeDynamicMenuComponents() { + + // Options menu. These will be updated once EditorPane is created + wrapLines = new JCheckBoxMenuItem("Wrap Lines", false); + wrapLines.setMnemonic(KeyEvent.VK_W); + wrapLines.addChangeListener(e -> setWrapLines(wrapLines.getState())); + whiteSpace = new JCheckBoxMenuItem("Label Whitespace", false); + whiteSpace.setMnemonic(KeyEvent.VK_L); + whiteSpace.addChangeListener(e -> setWhiteSpaceVisible(whiteSpace.isSelected())); + autoCompletion = new JCheckBoxMenuItem("Enable Autocompletion", true); + autoCompletion.addChangeListener(e -> setAutoCompletionEnabled(autoCompletion.getState())); + autoCompletionKey = new JCheckBoxMenuItem("Show Completions Without Ctrl+Space", false); + autoCompletionKey.addChangeListener(e -> setAutoCompletionNoKeyRequired(autoCompletionKey.getState())); + + // Help menu. These are 'dynamic' items + openMacroFunctions = new JMenuItem(" Open Help on Macro Function(s)..."); + openMacroFunctions.setMnemonic(KeyEvent.VK_H); + openMacroFunctions.addActionListener(e -> { + try { + new MacroFunctions(this).openHelp(getTextArea().getSelectedText()); + } catch (final IOException ex) { + handleException(ex); + } + }); + openHelp = new JMenuItem(" Open Help for Class (With Frames)..."); + openHelp.setMnemonic(KeyEvent.VK_H); + openHelp.addActionListener( e-> openHelp(null)); + openHelpWithoutFrames = new JMenuItem(" Open Help for Class..."); + openHelpWithoutFrames.addActionListener(e -> openHelp(null, false)); + } + /** * Check whether the file was edited outside of this {@link EditorPane} and * ask the user whether to reload. @@ -1340,22 +1364,8 @@ else if (source == replaceTabsWithSpaces) getTextArea() .convertTabsToSpaces(); else if (source == replaceSpacesWithTabs) getTextArea() .convertSpacesToTabs(); -// else if (source == clearScreen) { -// getTab().getScreen().setText(""); -// } else if (source == zapGremlins) zapGremlins(); -// else if (source == toggleAutoCompletionMenu) { -// toggleAutoCompletion(); -// } - else if (source == openHelp) openHelp(null); - else if (source == openHelpWithoutFrames) openHelp(null, false); else if (source == openClassOrPackageHelp) openClassOrPackageHelp(null); - else if (source == openMacroFunctions) try { - new MacroFunctions(this).openHelp(getTextArea().getSelectedText()); - } - catch (final IOException e) { - handleException(e); - } else if (source == extractSourceJar) extractSourceJar(); else if (source == openSourceForClass) { final String className = getSelectedClassNameOrAsk("Class (Fully qualified name):", "Which Class?"); @@ -1405,20 +1415,28 @@ else if (source == increaseFontSize || source == decreaseFontSize) { else if (handleTabsMenu(source)) return; } - private void toggleAutoCompletion() { + private void setAutoCompletionEnabled(final boolean enabled) { + for (int i = 0; i < tabbed.getTabCount(); i++) { + getEditorPane(i).setAutoCompletionEnabled(enabled); + } + } + + private void setAutoCompletionNoKeyRequired(final boolean noKeyRequired) { + for (int i = 0; i < tabbed.getTabCount(); i++) { + getEditorPane(i).setAutoCompletionNoKeyRequired(noKeyRequired); + } + } + + private void setWhiteSpaceVisible(final boolean visible) { for (int i = 0; i < tabbed.getTabCount(); i++) { - final EditorPane editorPane = getEditorPane(i); - editorPane.setAutoCompletionEnabled(toggleAutoCompletionMenu.isSelected()); + getEditorPane(i).setWhitespaceVisible(visible); } - prefService.put(TextEditor.class, "autoComplete", toggleAutoCompletionMenu.isSelected()); } - private void toggleAutoCompletionKeyRequired() { + private void setWrapLines(final boolean wrap) { for (int i = 0; i < tabbed.getTabCount(); i++) { - final EditorPane editorPane = getEditorPane(i); - editorPane.setAutoCompletionEnabled(toggleAutoCompletionKeyRequired.isSelected()); + getEditorPane(i).setLineWrap(wrap); } - prefService.put(TextEditor.class, "autoCompleteKeyRequired", toggleAutoCompletionKeyRequired.isSelected()); } private JMenu applyThemeMenu() { @@ -1431,13 +1449,15 @@ private JMenu applyThemeMenu() { map.put("IntelliJ (Light)", "idea"); map.put("Monokai", "monokai"); map.put("Visual Studio (Light)", "vs"); + final ButtonGroup group = new ButtonGroup(); final JMenu menu = new JMenu("Theme"); map.forEach((k, v) -> { if ("-".equals(k)) { menu.addSeparator(); return; } - final JMenuItem item = new JMenuItem(k); + final JRadioButtonMenuItem item = new JRadioButtonMenuItem(k); + group.add(item); item.addActionListener(e -> { try { applyTheme(v); @@ -1470,6 +1490,7 @@ public void applyTheme(final String theme) throws IllegalArgumentException { } catch (final Exception ex) { throw new IllegalArgumentException(ex); } + this.activeTheme = theme; } protected boolean handleTabsMenu(final Object source) { @@ -1495,7 +1516,7 @@ public void stateChanged(final ChangeEvent e) { editorPane.requestFocus(); checkForOutsideChanges(); - toggleWhiteSpaceLabeling.setSelected(editorPane.isWhitespaceVisible()); + whiteSpace.setSelected(editorPane.isWhitespaceVisible()); editorPane.setLanguageByFileName(editorPane.getFileName()); updateLanguageMenu(editorPane.getCurrentLanguage()); @@ -1923,10 +1944,18 @@ void updateLanguageMenu(final ScriptLanguage language) { final boolean isInGit = getEditorPane().getGitDirectory() != null; gitMenu.setVisible(isInGit); - updateTabAndFontSize(false); + updateUI(false); } + /** + * Use {@link #updateUI(boolean)} instead + */ + @Deprecated public void updateTabAndFontSize(final boolean setByLanguage) { + updateUI(setByLanguage); + } + + public void updateUI(final boolean setByLanguage) { final EditorPane pane = getEditorPane(); if (pane.getCurrentLanguage() == null) return; @@ -1973,6 +2002,10 @@ else if (tabSize == Integer.parseInt(item.getText())) { } wrapLines.setState(pane.getLineWrap()); tabsEmulated.setState(pane.getTabsEmulated()); + paintTabs.setState(pane.getPaintTabLines()); + whiteSpace.setState(pane.isWhitespaceVisible()); + autoCompletion.setState(pane.isAutoCompletionEnabled()); + autoCompletionKey.setState(pane.isAutoCompletionNoKeyRequired()); } public void setEditorPaneFileName(final String baseName) { @@ -3135,8 +3168,8 @@ private JMenu getPrefsMenu() { JMenuItem item = new JMenuItem("Save"); menu.add(item); item.addActionListener(e -> { - getEditorPane().savePreferences(tree.getTopLevelFoldersString()); - write("Script Editor Settings Saved...\n"); + getEditorPane().savePreferences(tree.getTopLevelFoldersString(), activeTheme); + write("Script Editor: Preferences Saved...\n"); }); item = new JMenuItem("Reset..."); menu.add(item); @@ -3146,7 +3179,8 @@ private JMenu getPrefsMenu() { JOptionPane.OK_CANCEL_OPTION); if (JOptionPane.OK_OPTION == choice) { prefService.clear(EditorPane.class); - write("Script Editor Settings Reset.\n"); + prefService.clear(TextEditor.class); + write("Script Editor: Preferences Reset.\n"); } }); return menu; @@ -3155,14 +3189,12 @@ private JMenu getPrefsMenu() { private JMenu helpMenu() { final JMenu menu = new JMenu("Help"); addSeparator(menu, "Contextual Help:"); - openHelpWithoutFrames = addToMenu(menu, " Open Help for Class...", 0, 0); + menu.add(openHelpWithoutFrames); openHelpWithoutFrames.setMnemonic(KeyEvent.VK_O); - openHelp = addToMenu(menu, " Open Help for Class (With Frames)...", 0, 0); - openHelp.setMnemonic(KeyEvent.VK_P); + menu.add(openHelp); openClassOrPackageHelp = addToMenu(menu, " Lookup Class or Package...", 0, 0); openClassOrPackageHelp.setMnemonic(KeyEvent.VK_S); - openMacroFunctions = addToMenu(menu, " Open Help on Macro Function(s)...", 0, 0); - openMacroFunctions.setMnemonic(KeyEvent.VK_H); + menu.add(openMacroFunctions); addSeparator(menu, "Online Resources:"); menu.add(helpMenuItem("Image.sc Forum ", "https://forum.image.sc/")); menu.add(helpMenuItem("ImageJ Search Portal", "https://search.imagej.net/")); From 42bad8c96631b571e5f0bfac77b4e08c48c0dfe7 Mon Sep 17 00:00:00 2001 From: tferr Date: Tue, 22 Feb 2022 19:30:13 -0500 Subject: [PATCH 10/21] TextEditor: Use dark theme with dark L&F + 'Mark Occurences' controls While at it, ensure Menu choices updates on theme change --- .../scijava/ui/swing/script/EditorPane.java | 3 ++ .../scijava/ui/swing/script/TextEditor.java | 52 ++++++++++++++++--- 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/scijava/ui/swing/script/EditorPane.java b/src/main/java/org/scijava/ui/swing/script/EditorPane.java index 614f3afd..887c49af 100644 --- a/src/main/java/org/scijava/ui/swing/script/EditorPane.java +++ b/src/main/java/org/scijava/ui/swing/script/EditorPane.java @@ -737,6 +737,7 @@ public void convertSpacesToTabs() { public static final String THEME_PREFS = "script.editor.theme"; public static final String AUTOCOMPLETE_PREFS = "script.editor.Autocomp"; public static final String AUTOCOMPLETE_NOKEY_PREFS = "script.editor.AutocompKey"; + public static final String MARK_OCCURRENCES_PREFS = "script.editor.Occurrences"; public static final String FOLDERS_PREFS = "script.editor.folders"; public static final int DEFAULT_TAB_SIZE = 4; public static final String DEFAULT_THEME = "default"; @@ -754,6 +755,7 @@ public void loadPreferences() { setPaintTabLines(false); setAutoCompletionEnabled(true); setAutoCompletionNoKeyRequired(false); + setMarkOccurrences(false); } else { resetTabSize(); setFontSize(prefService.getFloat(getClass(), FONT_SIZE_PREFS, getFontSize())); @@ -763,6 +765,7 @@ public void loadPreferences() { setPaintTabLines(prefService.getBoolean(getClass(), TABLINES_VISIBLE_PREFS, getPaintTabLines())); setAutoCompletionEnabled(prefService.getBoolean(getClass(), AUTOCOMPLETE_PREFS, true)); setAutoCompletionNoKeyRequired(prefService.getBoolean(getClass(), AUTOCOMPLETE_NOKEY_PREFS, false)); + setMarkOccurrences(prefService.getBoolean(getClass(), MARK_OCCURRENCES_PREFS, false)); } } diff --git a/src/main/java/org/scijava/ui/swing/script/TextEditor.java b/src/main/java/org/scijava/ui/swing/script/TextEditor.java index 6588cf7e..9717706d 100644 --- a/src/main/java/org/scijava/ui/swing/script/TextEditor.java +++ b/src/main/java/org/scijava/ui/swing/script/TextEditor.java @@ -82,6 +82,7 @@ import java.util.Collection; import java.util.Collections; import java.util.Date; +import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; @@ -98,6 +99,7 @@ import javax.script.ScriptEngine; import javax.script.ScriptException; import javax.swing.AbstractAction; +import javax.swing.AbstractButton; import javax.swing.BorderFactory; import javax.swing.BoxLayout; import javax.swing.ButtonGroup; @@ -226,7 +228,8 @@ public class TextEditor extends JFrame implements ActionListener, private Set tabsMenuItems; private FindAndReplaceDialog findDialog; private JCheckBoxMenuItem autoSave, wrapLines, tabsEmulated, autoImport, - autoCompletionKey, autoCompletion, paintTabs, whiteSpace; + autoCompletionKey, autoCompletion, markOccurences, paintTabs, whiteSpace; + private ButtonGroup themeRadioGroup; private JTextArea errorScreen = new JTextArea(); private final FileSystemTree tree; @@ -604,6 +607,7 @@ public TextEditor(final Context context) { addSeparator(options, "View:"); options.add(whiteSpace); + options.add(markOccurences); options.add(wrapLines); options.add(applyThemeMenu()); @@ -801,7 +805,8 @@ public void componentResized(final ComponentEvent e) { final EditorPane editorPane = getEditorPane(); // Apply preferences that have not yet been set updateUI(true); - applyTheme(editorPane.themeName()); + // if dark L&F and using the default theme, assume 'dark' theme + applyTheme((isDarkLaF() && "default".equals(editorPane.themeName())) ? "dark" : editorPane.themeName()); editorPane.requestFocus(); } @@ -893,6 +898,9 @@ private void initializeDynamicMenuComponents() { wrapLines = new JCheckBoxMenuItem("Wrap Lines", false); wrapLines.setMnemonic(KeyEvent.VK_W); wrapLines.addChangeListener(e -> setWrapLines(wrapLines.getState())); + markOccurences = new JCheckBoxMenuItem("Mark Occurences", false); + markOccurences.setToolTipText("Highlights all occurrences of a selected element"); + markOccurences.addChangeListener(e -> setMarkOccurrences(markOccurences.getState())); whiteSpace = new JCheckBoxMenuItem("Label Whitespace", false); whiteSpace.setMnemonic(KeyEvent.VK_L); whiteSpace.addChangeListener(e -> setWhiteSpaceVisible(whiteSpace.isSelected())); @@ -900,6 +908,7 @@ private void initializeDynamicMenuComponents() { autoCompletion.addChangeListener(e -> setAutoCompletionEnabled(autoCompletion.getState())); autoCompletionKey = new JCheckBoxMenuItem("Show Completions Without Ctrl+Space", false); autoCompletionKey.addChangeListener(e -> setAutoCompletionNoKeyRequired(autoCompletionKey.getState())); + themeRadioGroup = new ButtonGroup(); // Help menu. These are 'dynamic' items openMacroFunctions = new JMenuItem(" Open Help on Macro Function(s)..."); @@ -1427,6 +1436,12 @@ private void setAutoCompletionNoKeyRequired(final boolean noKeyRequired) { } } + private void setMarkOccurrences(final boolean markOccurrences) { + for (int i = 0; i < tabbed.getTabCount(); i++) { + getEditorPane(i).setMarkOccurrences(markOccurrences); + } + } + private void setWhiteSpaceVisible(final boolean visible) { for (int i = 0; i < tabbed.getTabCount(); i++) { getEditorPane(i).setWhitespaceVisible(visible); @@ -1449,7 +1464,7 @@ private JMenu applyThemeMenu() { map.put("IntelliJ (Light)", "idea"); map.put("Monokai", "monokai"); map.put("Visual Studio (Light)", "vs"); - final ButtonGroup group = new ButtonGroup(); + themeRadioGroup = new ButtonGroup(); final JMenu menu = new JMenu("Theme"); map.forEach((k, v) -> { if ("-".equals(k)) { @@ -1457,10 +1472,11 @@ private JMenu applyThemeMenu() { return; } final JRadioButtonMenuItem item = new JRadioButtonMenuItem(k); - group.add(item); + item.setActionCommand(v); + themeRadioGroup.add(item); item.addActionListener(e -> { try { - applyTheme(v); + applyTheme(v, false); } catch (final IllegalArgumentException ex) { JOptionPane.showMessageDialog(TextEditor.this, "An exception occured. Theme could not be loaded"); @@ -1481,9 +1497,13 @@ private JMenu applyThemeMenu() { * the resource could not be loaded */ public void applyTheme(final String theme) throws IllegalArgumentException { + applyTheme(theme, true); + } + + private void applyTheme(final String theme, final boolean updateUI) throws IllegalArgumentException { try { - final Theme th = Theme.load(getClass() - .getResourceAsStream("/org/fife/ui/rsyntaxtextarea/themes/" + theme + ".xml")); + final Theme th = Theme + .load(getClass().getResourceAsStream("/org/fife/ui/rsyntaxtextarea/themes/" + theme + ".xml")); for (int i = 0; i < tabbed.getTabCount(); i++) { th.apply(getEditorPane(i)); } @@ -1491,6 +1511,16 @@ public void applyTheme(final String theme) throws IllegalArgumentException { throw new IllegalArgumentException(ex); } this.activeTheme = theme; + if (updateUI && themeRadioGroup != null) { + final Enumeration choices = themeRadioGroup.getElements(); + while (choices.hasMoreElements()) { + AbstractButton choice = choices.nextElement(); + if (theme == choice.getActionCommand()) { + choice.setSelected(true); + break; + } + } + } } protected boolean handleTabsMenu(final Object source) { @@ -2000,6 +2030,7 @@ else if (tabSize == Integer.parseInt(item.getText())) { defaultSize = true; } } + markOccurences.setState(pane.getMarkOccurrences()); wrapLines.setState(pane.getLineWrap()); tabsEmulated.setState(pane.getTabsEmulated()); paintTabs.setState(pane.getPaintTabLines()); @@ -2505,6 +2536,7 @@ private static void append(final JTextArea textArea, final String text) { textArea.setCaretPosition(length); } + public void markCompileStart() { markCompileStart(true); } @@ -3258,5 +3290,11 @@ private static Color getDisabledComponentColor() { return Color.GRAY; } } + + private static boolean isDarkLaF() { + // see https://stackoverflow.com/a/3943023 + final Color b = new JLabel().getBackground(); + return (b.getRed()*0.299 + b.getGreen()*0.587 + b.getBlue() *0.114) < 186; + } } From 9c2fce604dc8ba60d632dd59efb7b2ac8e644700 Mon Sep 17 00:00:00 2001 From: tferr Date: Tue, 22 Feb 2022 21:42:26 -0500 Subject: [PATCH 11/21] Option for 'fallback completions'. API and code cleanup --- .../scijava/ui/swing/script/EditorPane.java | 49 ++++++++++++------- .../scijava/ui/swing/script/TextEditor.java | 39 ++++++++++----- 2 files changed, 58 insertions(+), 30 deletions(-) diff --git a/src/main/java/org/scijava/ui/swing/script/EditorPane.java b/src/main/java/org/scijava/ui/swing/script/EditorPane.java index 887c49af..6a1dd36c 100644 --- a/src/main/java/org/scijava/ui/swing/script/EditorPane.java +++ b/src/main/java/org/scijava/ui/swing/script/EditorPane.java @@ -94,7 +94,8 @@ public class EditorPane extends RSyntaxTextArea implements DocumentListener { private boolean undoInProgress; private boolean redoInProgress; private boolean autoCompletionEnabled = true; - private boolean autoCompletionNoKeyRequired = false; + private boolean autoCompletionJavaFallback; + private boolean autoCompletionWithoutKey; @Parameter Context context; @@ -521,29 +522,39 @@ protected void setLanguage(final ScriptLanguage language, setText(header += getText()); } + if (!autoCompletionEnabled) return; + // try to get language support for current language, may be null. support = languageSupportService.getLanguageSupport(currentLanguage); - - if (support != null && autoCompletionEnabled) { + if (support == null && autoCompletionJavaFallback) + support = languageSupportService.getLanguageSupport(scriptService.getLanguageByName("Java")); + if (support != null) support.install(this); - } } - public void setAutoCompletionEnabled(boolean value) { + public void setAutoCompletion(boolean value) { autoCompletionEnabled = value; if (currentLanguage != null) setLanguage(currentLanguage); } - void setAutoCompletionNoKeyRequired(boolean value) { - autoCompletionNoKeyRequired = value; + void setFallbackAutoCompletion(boolean value) { + autoCompletionJavaFallback = value; + } + + void setKeylessAutoCompletion(boolean value) { + autoCompletionWithoutKey = value; } public boolean isAutoCompletionEnabled() { - return autoCompletionNoKeyRequired; + return autoCompletionWithoutKey; + } + + public boolean isKeylessAutoCompletionEnabled() { + return autoCompletionWithoutKey; } - public boolean isAutoCompletionNoKeyRequired() { - return autoCompletionNoKeyRequired; + public boolean isFallbackAutoCompletionEnabled() { + return autoCompletionJavaFallback; } /** @@ -735,8 +746,9 @@ public void convertSpacesToTabs() { public static final String WHITESPACE_VISIBLE_PREFS = "script.editor.Whitespace"; public static final String TABLINES_VISIBLE_PREFS = "script.editor.Tablines"; public static final String THEME_PREFS = "script.editor.theme"; - public static final String AUTOCOMPLETE_PREFS = "script.editor.Autocomp"; - public static final String AUTOCOMPLETE_NOKEY_PREFS = "script.editor.AutocompKey"; + public static final String AUTOCOMPLETE_PREFS = "script.editor.AC"; + public static final String AUTOCOMPLETE_NOKEY_PREFS = "script.editor.ACNoKey"; + public static final String AUTOCOMPLETE_FALLBACK_PREFS = "script.editor.ACFallback"; public static final String MARK_OCCURRENCES_PREFS = "script.editor.Occurrences"; public static final String FOLDERS_PREFS = "script.editor.folders"; public static final int DEFAULT_TAB_SIZE = 4; @@ -753,8 +765,9 @@ public void loadPreferences() { setLineWrap(false); setTabsEmulated(false); setPaintTabLines(false); - setAutoCompletionEnabled(true); - setAutoCompletionNoKeyRequired(false); + setAutoCompletion(true); + setKeylessAutoCompletion(false); + setFallbackAutoCompletion(false); setMarkOccurrences(false); } else { resetTabSize(); @@ -763,8 +776,9 @@ public void loadPreferences() { setTabsEmulated(prefService.getBoolean(getClass(), TABS_EMULATED_PREFS, getTabsEmulated())); setWhitespaceVisible(prefService.getBoolean(getClass(), WHITESPACE_VISIBLE_PREFS, isWhitespaceVisible())); setPaintTabLines(prefService.getBoolean(getClass(), TABLINES_VISIBLE_PREFS, getPaintTabLines())); - setAutoCompletionEnabled(prefService.getBoolean(getClass(), AUTOCOMPLETE_PREFS, true)); - setAutoCompletionNoKeyRequired(prefService.getBoolean(getClass(), AUTOCOMPLETE_NOKEY_PREFS, false)); + setAutoCompletion(prefService.getBoolean(getClass(), AUTOCOMPLETE_PREFS, true)); + setKeylessAutoCompletion(prefService.getBoolean(getClass(), AUTOCOMPLETE_NOKEY_PREFS, false)); + setFallbackAutoCompletion(prefService.getBoolean(getClass(), AUTOCOMPLETE_FALLBACK_PREFS, false)); setMarkOccurrences(prefService.getBoolean(getClass(), MARK_OCCURRENCES_PREFS, false)); } } @@ -788,7 +802,8 @@ public void savePreferences(final String top_folders, final String theme) { prefService.put(getClass(), WHITESPACE_VISIBLE_PREFS, isWhitespaceVisible()); prefService.put(getClass(), TABLINES_VISIBLE_PREFS, getPaintTabLines()); prefService.put(getClass(), AUTOCOMPLETE_PREFS, isAutoCompletionEnabled()); - prefService.put(getClass(), AUTOCOMPLETE_NOKEY_PREFS, isAutoCompletionNoKeyRequired()); + prefService.put(getClass(), AUTOCOMPLETE_NOKEY_PREFS, isKeylessAutoCompletionEnabled()); + prefService.put(getClass(), AUTOCOMPLETE_FALLBACK_PREFS, isFallbackAutoCompletionEnabled()); if (null != top_folders) prefService.put(getClass(), FOLDERS_PREFS, top_folders); if (null != theme) prefService.put(getClass(), THEME_PREFS, theme); } diff --git a/src/main/java/org/scijava/ui/swing/script/TextEditor.java b/src/main/java/org/scijava/ui/swing/script/TextEditor.java index 9717706d..72b76c90 100644 --- a/src/main/java/org/scijava/ui/swing/script/TextEditor.java +++ b/src/main/java/org/scijava/ui/swing/script/TextEditor.java @@ -228,7 +228,8 @@ public class TextEditor extends JFrame implements ActionListener, private Set tabsMenuItems; private FindAndReplaceDialog findDialog; private JCheckBoxMenuItem autoSave, wrapLines, tabsEmulated, autoImport, - autoCompletionKey, autoCompletion, markOccurences, paintTabs, whiteSpace; + autocompletion, fallbackAutocompletion, keylessAutocompletion, + markOccurences, paintTabs, whiteSpace; private ButtonGroup themeRadioGroup; private JTextArea errorScreen = new JTextArea(); @@ -612,8 +613,9 @@ public TextEditor(final Context context) { options.add(applyThemeMenu()); addSeparator(options, "Code Completions:"); - options.add(autoCompletion); - options.add(autoCompletionKey); + options.add(autocompletion); + options.add(keylessAutocompletion); + options.add(fallbackAutocompletion); options.addSeparator(); options.add(getPrefsMenu()); @@ -808,7 +810,6 @@ public void componentResized(final ComponentEvent e) { // if dark L&F and using the default theme, assume 'dark' theme applyTheme((isDarkLaF() && "default".equals(editorPane.themeName())) ? "dark" : editorPane.themeName()); editorPane.requestFocus(); - } private class DragAndDrop implements DragSourceListener, DragGestureListener { @@ -904,10 +905,16 @@ private void initializeDynamicMenuComponents() { whiteSpace = new JCheckBoxMenuItem("Label Whitespace", false); whiteSpace.setMnemonic(KeyEvent.VK_L); whiteSpace.addChangeListener(e -> setWhiteSpaceVisible(whiteSpace.isSelected())); - autoCompletion = new JCheckBoxMenuItem("Enable Autocompletion", true); - autoCompletion.addChangeListener(e -> setAutoCompletionEnabled(autoCompletion.getState())); - autoCompletionKey = new JCheckBoxMenuItem("Show Completions Without Ctrl+Space", false); - autoCompletionKey.addChangeListener(e -> setAutoCompletionNoKeyRequired(autoCompletionKey.getState())); + autocompletion = new JCheckBoxMenuItem("Enable Autocompletion", true); + autocompletion.addChangeListener(e -> setAutoCompletionEnabled(autocompletion.getState())); + keylessAutocompletion = new JCheckBoxMenuItem("Show Completions Without Ctrl+Space", false); + keylessAutocompletion.setToolTipText("If selected, completion pop-up automatically appears" + + " while typing
NB: Not all languages support this feature"); + keylessAutocompletion.addChangeListener(e -> setKeylessAutoCompletion(keylessAutocompletion.getState())); + fallbackAutocompletion = new JCheckBoxMenuItem("Unsupported languages: Fallback to Java", false); + fallbackAutocompletion.setToolTipText("If selected, Java completions will be used when scripting
" + + "a language for which auto-completions are not available"); + fallbackAutocompletion.addChangeListener(e -> setFallbackAutoCompletion(fallbackAutocompletion.getState())); themeRadioGroup = new ButtonGroup(); // Help menu. These are 'dynamic' items @@ -1426,13 +1433,19 @@ else if (source == increaseFontSize || source == decreaseFontSize) { private void setAutoCompletionEnabled(final boolean enabled) { for (int i = 0; i < tabbed.getTabCount(); i++) { - getEditorPane(i).setAutoCompletionEnabled(enabled); + getEditorPane(i).setAutoCompletion(enabled); + } + } + + private void setKeylessAutoCompletion(final boolean noKeyRequired) { + for (int i = 0; i < tabbed.getTabCount(); i++) { + getEditorPane(i).setKeylessAutoCompletion(noKeyRequired); } } - private void setAutoCompletionNoKeyRequired(final boolean noKeyRequired) { + private void setFallbackAutoCompletion(final boolean fallback) { for (int i = 0; i < tabbed.getTabCount(); i++) { - getEditorPane(i).setAutoCompletionNoKeyRequired(noKeyRequired); + getEditorPane(i).setFallbackAutoCompletion(fallback); } } @@ -2035,8 +2048,8 @@ else if (tabSize == Integer.parseInt(item.getText())) { tabsEmulated.setState(pane.getTabsEmulated()); paintTabs.setState(pane.getPaintTabLines()); whiteSpace.setState(pane.isWhitespaceVisible()); - autoCompletion.setState(pane.isAutoCompletionEnabled()); - autoCompletionKey.setState(pane.isAutoCompletionNoKeyRequired()); + autocompletion.setState(pane.isAutoCompletionEnabled()); + keylessAutocompletion.setState(pane.isKeylessAutoCompletionEnabled()); } public void setEditorPaneFileName(final String baseName) { From 76f1e669c6e1351b4c0f3fa70130396dd0851a7e Mon Sep 17 00:00:00 2001 From: tferr Date: Wed, 23 Feb 2022 04:53:36 -0500 Subject: [PATCH 12/21] TextEditor: Add option for toggling console --- .../scijava/ui/swing/script/TextEditor.java | 43 ++++++++++++------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/scijava/ui/swing/script/TextEditor.java b/src/main/java/org/scijava/ui/swing/script/TextEditor.java index 72b76c90..44430284 100644 --- a/src/main/java/org/scijava/ui/swing/script/TextEditor.java +++ b/src/main/java/org/scijava/ui/swing/script/TextEditor.java @@ -242,6 +242,7 @@ public class TextEditor extends JFrame implements ActionListener, private boolean respectAutoImports; private String activeTheme; + private int[] panePositions; @Parameter @@ -511,19 +512,12 @@ public TextEditor(final Context context) { tabsMenu = new JMenu("Window"); tabsMenu.setMnemonic(KeyEvent.VK_W); addSeparator(tabsMenu, "Panes:"); - final JCheckBoxMenuItem jcmi = new JCheckBoxMenuItem("File Explorer", true); - final int[] treePos = { body.getDividerLocation() }; - jcmi.addItemListener(e -> { - if (jcmi.isSelected()) { - body.setDividerLocation(treePos[0]); - } else { - // see https://stackoverflow.com/q/4934499 - treePos[0] = body.getDividerLocation(); - body.getLeftComponent().setMinimumSize(new Dimension()); - body.setDividerLocation(0.0d); - } - }); - tabsMenu.add(jcmi); + final JCheckBoxMenuItem jcmi1 = new JCheckBoxMenuItem("File Explorer", true); + jcmi1.addItemListener(e -> collapseSplitPane(0, !jcmi1.isSelected())); + tabsMenu.add(jcmi1); + final JCheckBoxMenuItem jcmi2 = new JCheckBoxMenuItem("Console", true); + jcmi2.addItemListener(e -> collapseSplitPane(1, !jcmi2.isSelected())); + tabsMenu.add(jcmi2); addSeparator(tabsMenu, "Tabs:"); nextTab = addToMenu(tabsMenu, "Next Tab", KeyEvent.VK_PAGE_DOWN, ctrl); nextTab.setMnemonic(KeyEvent.VK_N); @@ -805,10 +799,12 @@ public void componentResized(final ComponentEvent e) { open(null); final EditorPane editorPane = getEditorPane(); + // If dark L&F and using the default theme, assume 'dark' theme + applyTheme((isDarkLaF() && "default".equals(editorPane.themeName())) ? "dark" : editorPane.themeName()); // Apply preferences that have not yet been set updateUI(true); - // if dark L&F and using the default theme, assume 'dark' theme - applyTheme((isDarkLaF() && "default".equals(editorPane.themeName())) ? "dark" : editorPane.themeName()); + // Store locations of splitpanes + panePositions = new int[]{body.getDividerLocation(), getTab().getScreenAndPromptSplit().getDividerLocation()}; editorPane.requestFocus(); } @@ -1536,6 +1532,23 @@ private void applyTheme(final String theme, final boolean updateUI) throws Illeg } } + private void collapseSplitPane(final int pane, final boolean collapse) { + final JSplitPane jsp = (pane == 0) ? body : getTab(); + if (collapse) { + // see https://stackoverflow.com/q/4934499 + panePositions[pane] = jsp.getDividerLocation(); + if (pane == 0) { // collapse to left + jsp.getLeftComponent().setMinimumSize(new Dimension()); + jsp.setDividerLocation(0.0d); + } else { // collapse to bottom + jsp.getTopComponent().setMinimumSize(new Dimension()); + jsp.setDividerLocation(1.0d); + } + } else { + jsp.setDividerLocation(panePositions[pane]); + } + } + protected boolean handleTabsMenu(final Object source) { if (!(source instanceof JMenuItem)) return false; final JMenuItem item = (JMenuItem) source; From ddf3f7c996e25931f28b620c0149064cb90e2f71 Mon Sep 17 00:00:00 2001 From: tferr Date: Wed, 23 Feb 2022 04:54:30 -0500 Subject: [PATCH 13/21] Cosmetic changes and code cleanup --- .../ui/swing/script/FileSystemTreePanel.java | 25 ++++++++--- .../scijava/ui/swing/script/TextEditor.java | 43 +++++++++---------- 2 files changed, 40 insertions(+), 28 deletions(-) diff --git a/src/main/java/org/scijava/ui/swing/script/FileSystemTreePanel.java b/src/main/java/org/scijava/ui/swing/script/FileSystemTreePanel.java index 8e4ee088..9b16b6fc 100644 --- a/src/main/java/org/scijava/ui/swing/script/FileSystemTreePanel.java +++ b/src/main/java/org/scijava/ui/swing/script/FileSystemTreePanel.java @@ -31,10 +31,12 @@ import java.awt.Color; import java.awt.Desktop; +import java.awt.Dimension; import java.awt.Font; import java.awt.Graphics2D; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; +import java.awt.Insets; import java.awt.RenderingHints; import java.awt.event.FocusAdapter; import java.awt.event.FocusEvent; @@ -172,8 +174,21 @@ public void keyPressed(final KeyEvent ke) { return field; } + private JButton thinButton(final String label) { + final JButton b = new JButton(label); + final double FACTOR = .25; + final Insets insets =b.getMargin(); + b.setMargin(new Insets(insets.top, (int) (insets.left * + FACTOR), insets.bottom, (int) (insets.right * FACTOR))); + //b.setBorder(null); + // set height to that of searchField. Do not allow vertical resizing + b.setPreferredSize(new Dimension(b.getPreferredSize().width, (int) searchField.getPreferredSize().getHeight())); + b.setMaximumSize(new Dimension(b.getMaximumSize().width, (int) searchField.getPreferredSize().getHeight())); + return b; + } + private JButton addDirectoryButton() { - final JButton add_directory = new JButton("+"); + final JButton add_directory = thinButton("+"); add_directory.setToolTipText("Add a directory"); add_directory.addActionListener(e -> { final String folders = tree.getTopLevelFoldersString(); @@ -205,7 +220,7 @@ private JButton addDirectoryButton() { } private JButton removeDirectoryButton() { - final JButton remove_directory = new JButton("−"); + final JButton remove_directory = thinButton("−"); remove_directory.setToolTipText("Remove a top-level directory"); remove_directory.addActionListener(e -> { final TreePath p = tree.getSelectionPath(); @@ -227,7 +242,7 @@ private JButton removeDirectoryButton() { } private JButton searchOptionsButton() { - final JButton options = new JButton("⋮"); + final JButton options = thinButton("⋮"); options.setToolTipText("Filtering options"); final JPopupMenu popup = new JPopupMenu(); final JCheckBoxMenuItem jcbmi1 = new JCheckBoxMenuItem("Case Sensitive", isCaseSensitive()); @@ -371,8 +386,8 @@ private void setRegexEnabled(final boolean b) { private class SearchField extends JTextField { private static final long serialVersionUID = 7004232238240585434L; - private static final String REGEX_HOLDER = " [?*]"; - private static final String CASE_HOLDER = " [Aa]"; + private static final String REGEX_HOLDER = "[?*]"; + private static final String CASE_HOLDER = "[Aa]"; private static final String DEF_HOLDER = "File filter... "; void update() { diff --git a/src/main/java/org/scijava/ui/swing/script/TextEditor.java b/src/main/java/org/scijava/ui/swing/script/TextEditor.java index 44430284..6688f292 100644 --- a/src/main/java/org/scijava/ui/swing/script/TextEditor.java +++ b/src/main/java/org/scijava/ui/swing/script/TextEditor.java @@ -295,10 +295,11 @@ public TextEditor(final Context context) { // NB: All panes must be initialized before menus are assembled! tabbed = new JTabbedPane(); - tabbed.setBorder(BorderFactory.createEmptyBorder(0,BORDER_SIZE,0,BORDER_SIZE)); tree = new FileSystemTree(log); final JScrollPane scrolltree = new JScrollPane(new FileSystemTreePanel(tree, context)); - scrolltree.setBorder(BorderFactory.createEmptyBorder(0,BORDER_SIZE,0,BORDER_SIZE)); + // set borders. Needed for drag & drop and collapsing split pane + //tabbed.setBorder(BorderFactory.createEmptyBorder(0,BORDER_SIZE,0,BORDER_SIZE)); + //scrolltree.setBorder(BorderFactory.createEmptyBorder(0,0,0,BORDER_SIZE)); scrolltree.setPreferredSize(new Dimension(200, 600)); body = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, scrolltree, tabbed); @@ -612,7 +613,7 @@ public TextEditor(final Context context) { options.add(fallbackAutocompletion); options.addSeparator(); - options.add(getPrefsMenu()); + appendPreferences(options); mbar.add(options); mbar.add(helpMenu()); @@ -904,7 +905,7 @@ private void initializeDynamicMenuComponents() { autocompletion = new JCheckBoxMenuItem("Enable Autocompletion", true); autocompletion.addChangeListener(e -> setAutoCompletionEnabled(autocompletion.getState())); keylessAutocompletion = new JCheckBoxMenuItem("Show Completions Without Ctrl+Space", false); - keylessAutocompletion.setToolTipText("If selected, completion pop-up automatically appears" + keylessAutocompletion.setToolTipText("If selected, the completion pop-up automatically appears" + " while typing
NB: Not all languages support this feature"); keylessAutocompletion.addChangeListener(e -> setKeylessAutoCompletion(keylessAutocompletion.getState())); fallbackAutocompletion = new JCheckBoxMenuItem("Unsupported languages: Fallback to Java", false); @@ -914,7 +915,7 @@ private void initializeDynamicMenuComponents() { themeRadioGroup = new ButtonGroup(); // Help menu. These are 'dynamic' items - openMacroFunctions = new JMenuItem(" Open Help on Macro Function(s)..."); + openMacroFunctions = new JMenuItem("Open Help on Macro Function(s)..."); openMacroFunctions.setMnemonic(KeyEvent.VK_H); openMacroFunctions.addActionListener(e -> { try { @@ -923,10 +924,10 @@ private void initializeDynamicMenuComponents() { handleException(ex); } }); - openHelp = new JMenuItem(" Open Help for Class (With Frames)..."); + openHelp = new JMenuItem("Open Help for Class (With Frames)..."); openHelp.setMnemonic(KeyEvent.VK_H); openHelp.addActionListener( e-> openHelp(null)); - openHelpWithoutFrames = new JMenuItem(" Open Help for Class..."); + openHelpWithoutFrames = new JMenuItem("Open Help for Class..."); openHelpWithoutFrames.addActionListener(e -> openHelp(null, false)); } @@ -1469,9 +1470,9 @@ private JMenu applyThemeMenu() { map.put("-", "-"); map.put("Dark", "dark"); map.put("Druid", "druid"); + map.put("Monokai", "monokai"); map.put("Eclipse (Light)", "eclipse"); map.put("IntelliJ (Light)", "idea"); - map.put("Monokai", "monokai"); map.put("Visual Studio (Light)", "vs"); themeRadioGroup = new ButtonGroup(); final JMenu menu = new JMenu("Theme"); @@ -1523,8 +1524,8 @@ private void applyTheme(final String theme, final boolean updateUI) throws Illeg if (updateUI && themeRadioGroup != null) { final Enumeration choices = themeRadioGroup.getElements(); while (choices.hasMoreElements()) { - AbstractButton choice = choices.nextElement(); - if (theme == choice.getActionCommand()) { + final AbstractButton choice = choices.nextElement(); + if (theme.equals(choice.getActionCommand())) { choice.setSelected(true); break; } @@ -2845,13 +2846,11 @@ public void run() { } final JScrollPane jsp = new JScrollPane(panel); //jsp.setPreferredSize(new Dimension(800, 500)); - SwingUtilities.invokeLater(new Runnable() { - public void run() { - final JFrame frame = new JFrame(text); - frame.getContentPane().add(jsp); - frame.pack(); - frame.setVisible(true); - } + SwingUtilities.invokeLater(() -> { + final JFrame frame = new JFrame(text); + frame.getContentPane().add(jsp); + frame.pack(); + frame.setVisible(true); }); } } @@ -3221,9 +3220,8 @@ private void changeFontSize(final JTextArea a, final float size) { a.setFont(a.getFont().deriveFont(size)); } - private JMenu getPrefsMenu() { - final JMenu menu = new JMenu("Preferences"); - JMenuItem item = new JMenuItem("Save"); + private void appendPreferences(final JMenu menu) { + JMenuItem item = new JMenuItem("Save Preferences"); menu.add(item); item.addActionListener(e -> { getEditorPane().savePreferences(tree.getTopLevelFoldersString(), activeTheme); @@ -3241,7 +3239,6 @@ private JMenu getPrefsMenu() { write("Script Editor: Preferences Reset.\n"); } }); - return menu; } private JMenu helpMenu() { @@ -3250,7 +3247,7 @@ private JMenu helpMenu() { menu.add(openHelpWithoutFrames); openHelpWithoutFrames.setMnemonic(KeyEvent.VK_O); menu.add(openHelp); - openClassOrPackageHelp = addToMenu(menu, " Lookup Class or Package...", 0, 0); + openClassOrPackageHelp = addToMenu(menu, "Lookup Class or Package...", 0, 0); openClassOrPackageHelp.setMnemonic(KeyEvent.VK_S); menu.add(openMacroFunctions); addSeparator(menu, "Online Resources:"); @@ -3272,7 +3269,7 @@ private JMenu helpMenu() { } private JMenuItem helpMenuItem(final String label, final String url) { - final JMenuItem item = new JMenuItem(" " +label); // indent entries + final JMenuItem item = new JMenuItem(label); item.addActionListener(e -> openURL(url)); return item; } From 352a7ee51684bd3859c128bebc77af8044bfbd8c Mon Sep 17 00:00:00 2001 From: tferr Date: Wed, 23 Feb 2022 05:44:00 -0500 Subject: [PATCH 14/21] TextEditor: Fix font-size being reset at startup and on theme change --- .../java/org/scijava/ui/swing/script/TextEditor.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/scijava/ui/swing/script/TextEditor.java b/src/main/java/org/scijava/ui/swing/script/TextEditor.java index 6688f292..192ba95a 100644 --- a/src/main/java/org/scijava/ui/swing/script/TextEditor.java +++ b/src/main/java/org/scijava/ui/swing/script/TextEditor.java @@ -647,7 +647,6 @@ public TextEditor(final Context context) { // Tweaks for FileSystemTree tree.addTopLevelFoldersFrom(getEditorPane().loadFolders()); // Restore top-level directories - tree.setFont(tree.getFont().deriveFont(getEditorPane().getFontSize())); dragSource = new DragSource(); dragSource.createDefaultDragGestureRecognizer(tree, DnDConstants.ACTION_COPY, new DragAndDrop()); tree.ignoreExtension("class"); @@ -802,7 +801,9 @@ public void componentResized(final ComponentEvent e) { final EditorPane editorPane = getEditorPane(); // If dark L&F and using the default theme, assume 'dark' theme applyTheme((isDarkLaF() && "default".equals(editorPane.themeName())) ? "dark" : editorPane.themeName()); - // Apply preferences that have not yet been set + // Ensure font sizes are consistent across all panels + setFontSize(getEditorPane().getFontSize()); + // Ensure menu commands are up-to-date updateUI(true); // Store locations of splitpanes panePositions = new int[]{body.getDividerLocation(), getTab().getScreenAndPromptSplit().getDividerLocation()}; @@ -1515,7 +1516,11 @@ private void applyTheme(final String theme, final boolean updateUI) throws Illeg final Theme th = Theme .load(getClass().getResourceAsStream("/org/fife/ui/rsyntaxtextarea/themes/" + theme + ".xml")); for (int i = 0; i < tabbed.getTabCount(); i++) { - th.apply(getEditorPane(i)); + // themes include font size, so we'll need to reset that + final EditorPane ep = getEditorPane(i); + final float existingFontSize = ep.getFontSize(); + th.apply(ep); + ep.setFontSize(existingFontSize); } } catch (final Exception ex) { throw new IllegalArgumentException(ex); From f1adb167aadac13a70b8aeb862f3c36d2c24efd3 Mon Sep 17 00:00:00 2001 From: tferr Date: Wed, 23 Feb 2022 08:43:05 -0500 Subject: [PATCH 15/21] Remember size of REPL pane across restarts --- .../scijava/ui/swing/script/TextEditor.java | 21 +++++++++-------- .../ui/swing/script/TextEditorTab.java | 23 ++++++++++++++++--- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/scijava/ui/swing/script/TextEditor.java b/src/main/java/org/scijava/ui/swing/script/TextEditor.java index 192ba95a..f9130237 100644 --- a/src/main/java/org/scijava/ui/swing/script/TextEditor.java +++ b/src/main/java/org/scijava/ui/swing/script/TextEditor.java @@ -197,6 +197,7 @@ public class TextEditor extends JFrame implements ActionListener, public static final String MAIN_DIV_LOCATION = "script.editor.main.divLocation"; public static final String TAB_DIV_LOCATION = "script.editor.tab.divLocation"; public static final String TAB_DIV_ORIENTATION = "script.editor.tab.divOrientation"; + public static final String REPL_DIV_LOCATION = "script.editor.repl.divLocation"; public static final String LAST_LANGUAGE = "script.editor.lastLanguage"; static { @@ -640,7 +641,10 @@ public TextEditor(final Context context) { getContentPane().setLayout(new BoxLayout(getContentPane(), BoxLayout.Y_AXIS)); // Tweaks for JSplitPane - body.setOneTouchExpandable(true); + // TF: disable setOneTouchExpandable() due to inconsistent behavior when + // applying preferences at startup. Also, it does not apply to all L&Fs. + // Users can use the controls in the menu bar to toggle the pane + body.setOneTouchExpandable(false); body.addPropertyChangeListener(evt -> { if ("dividerLocation".equals(evt.getPropertyName())) saveWindowSizeToPrefs(); }); @@ -767,8 +771,8 @@ public void windowGainedFocus(final WindowEvent e) { try { threadService.invoke(() -> { pack(); - body.setDividerLocation(0.2); - getTab().getScreenAndPromptSplit().setDividerLocation(1.0); + body.setDividerLocation(0.2); // Important!: will be read as prefs. default + getTab().setREPLVisible(false); loadPreferences(); pack(); }); @@ -806,7 +810,7 @@ public void componentResized(final ComponentEvent e) { // Ensure menu commands are up-to-date updateUI(true); // Store locations of splitpanes - panePositions = new int[]{body.getDividerLocation(), getTab().getScreenAndPromptSplit().getDividerLocation()}; + panePositions = new int[]{body.getDividerLocation(), getTab().getDividerLocation()}; editorPane.requestFocus(); } @@ -990,9 +994,10 @@ public void loadPreferences() { final TextEditorTab tab = getTab(); final int tabDivLocation = prefService.getInt(getClass(), TAB_DIV_LOCATION, tab.getDividerLocation()); final int tabDivOrientation = prefService.getInt(getClass(), TAB_DIV_ORIENTATION, tab.getOrientation()); + final int replDividerLocation = prefService.getInt(getClass(), REPL_DIV_LOCATION, tab.getScreenAndPromptSplit().getDividerLocation()); tab.setDividerLocation(tabDivLocation); tab.setOrientation(tabDivOrientation); - + tab.getScreenAndPromptSplit().setDividerLocation(replDividerLocation); layoutLoading = false; } @@ -1017,6 +1022,7 @@ public void saveWindowSizeToPrefs() { final TextEditorTab tab = getTab(); prefService.put(getClass(), TAB_DIV_LOCATION, tab.getDividerLocation()); prefService.put(getClass(), TAB_DIV_ORIENTATION, tab.getOrientation()); + prefService.put(getClass(), REPL_DIV_LOCATION, tab.getScreenAndPromptSplit().getDividerLocation()); } final public RSyntaxTextArea getTextArea() { @@ -1541,13 +1547,10 @@ private void applyTheme(final String theme, final boolean updateUI) throws Illeg private void collapseSplitPane(final int pane, final boolean collapse) { final JSplitPane jsp = (pane == 0) ? body : getTab(); if (collapse) { - // see https://stackoverflow.com/q/4934499 panePositions[pane] = jsp.getDividerLocation(); if (pane == 0) { // collapse to left - jsp.getLeftComponent().setMinimumSize(new Dimension()); jsp.setDividerLocation(0.0d); } else { // collapse to bottom - jsp.getTopComponent().setMinimumSize(new Dimension()); jsp.setDividerLocation(1.0d); } } else { @@ -3047,7 +3050,7 @@ public void setIncremental(final boolean incremental) { final JTextArea prompt = this.getTab().getPrompt(); if (incremental) { - getTab().getScreenAndPromptSplit().setDividerLocation(0.5); + getTab().setREPLVisible(true); prompt.addKeyListener(new KeyAdapter() { private final ArrayList commands = loadPromptLog(getCurrentLanguage()); private int index = commands.size(); diff --git a/src/main/java/org/scijava/ui/swing/script/TextEditorTab.java b/src/main/java/org/scijava/ui/swing/script/TextEditorTab.java index 14f71422..eed60a78 100644 --- a/src/main/java/org/scijava/ui/swing/script/TextEditorTab.java +++ b/src/main/java/org/scijava/ui/swing/script/TextEditorTab.java @@ -79,6 +79,7 @@ public class TextEditorTab extends JSplitPane { private final JButton runit, batchit, killit, toggleErrors, switchSplit; private final JCheckBox incremental; private final JSplitPane screenAndPromptSplit; + private int screenAndPromptSplitDividerLocation; private final TextEditor textEditor; private DropTarget dropTarget; @@ -87,7 +88,10 @@ public class TextEditorTab extends JSplitPane { public TextEditorTab(final TextEditor textEditor) { super(JSplitPane.VERTICAL_SPLIT); super.setResizeWeight(350.0 / 430.0); - this.setOneTouchExpandable(true); + // TF: disable setOneTouchExpandable() due to inconsistent behavior when + // applying preferences at startup. Also, it does not apply to all L&Fs. + // Users can use the controls in the menu bar to toggle the pane + this.setOneTouchExpandable(false); this.textEditor = textEditor; editorPane = new EditorPane(); @@ -210,7 +214,7 @@ public void actionPerformed(final ActionEvent e) { } // Keep prompt collapsed if not in use if (!incremental.isSelected()) { - SwingUtilities.invokeLater(() -> screenAndPromptSplit.setDividerLocation(1.0)); + setREPLVisible(false); } } }); @@ -299,7 +303,6 @@ public void actionPerformed(final ActionEvent e) { super.setLeftComponent(editorPane.wrappedInScrollbars()); super.setRightComponent(screenAndPromptSplit); - screenAndPromptSplit.setDividerLocation(600); screenAndPromptSplit.setDividerLocation(1.0); // Persist Script Editor layout whenever split pane divider is adjusted. @@ -314,6 +317,20 @@ JSplitPane getScreenAndPromptSplit() { return screenAndPromptSplit; } + void setREPLVisible(final boolean visible) { + SwingUtilities.invokeLater(() -> { + if (visible) { + if (getScreenAndPromptSplit().getDividerLocation() <= getScreenAndPromptSplit().getMinimumDividerLocation()) + getScreenAndPromptSplit().setDividerLocation(.5d); // half of panel's height + else + getScreenAndPromptSplit().setDividerLocation(screenAndPromptSplitDividerLocation); + } else { // collapse to bottom + screenAndPromptSplitDividerLocation = getScreenAndPromptSplit().getDividerLocation(); + getScreenAndPromptSplit().setDividerLocation(1f); + } + }); + } + @Override public void setOrientation(final int orientation) { super.setOrientation(orientation); From df6f9f2264b03addf2e39ac190796329ff05e571 Mon Sep 17 00:00:00 2001 From: tferr Date: Wed, 23 Feb 2022 10:23:20 -0500 Subject: [PATCH 16/21] Minor corrections - Move FileSystemTree tweaks to FileSystemTreePanel - Ensure Filtering field is large enough to display placeholders - Add explicit error messages to buttons - Cleanup API - Misc --- .../scijava/ui/swing/script/EditorPane.java | 10 ++--- .../ui/swing/script/FileSystemTree.java | 15 ------- .../ui/swing/script/FileSystemTreePanel.java | 40 ++++++++++++++++++- .../scijava/ui/swing/script/TextEditor.java | 17 ++++---- 4 files changed, 54 insertions(+), 28 deletions(-) diff --git a/src/main/java/org/scijava/ui/swing/script/EditorPane.java b/src/main/java/org/scijava/ui/swing/script/EditorPane.java index 6a1dd36c..72c912b7 100644 --- a/src/main/java/org/scijava/ui/swing/script/EditorPane.java +++ b/src/main/java/org/scijava/ui/swing/script/EditorPane.java @@ -546,7 +546,7 @@ void setKeylessAutoCompletion(boolean value) { } public boolean isAutoCompletionEnabled() { - return autoCompletionWithoutKey; + return autoCompletionEnabled; } public boolean isKeylessAutoCompletionEnabled() { @@ -747,7 +747,7 @@ public void convertSpacesToTabs() { public static final String TABLINES_VISIBLE_PREFS = "script.editor.Tablines"; public static final String THEME_PREFS = "script.editor.theme"; public static final String AUTOCOMPLETE_PREFS = "script.editor.AC"; - public static final String AUTOCOMPLETE_NOKEY_PREFS = "script.editor.ACNoKey"; + public static final String AUTOCOMPLETE_KEYLESS_PREFS = "script.editor.ACNoKey"; public static final String AUTOCOMPLETE_FALLBACK_PREFS = "script.editor.ACFallback"; public static final String MARK_OCCURRENCES_PREFS = "script.editor.Occurrences"; public static final String FOLDERS_PREFS = "script.editor.folders"; @@ -766,7 +766,7 @@ public void loadPreferences() { setTabsEmulated(false); setPaintTabLines(false); setAutoCompletion(true); - setKeylessAutoCompletion(false); + setKeylessAutoCompletion(true); // true for backwards compatibility with IJ1 macro auto-completion setFallbackAutoCompletion(false); setMarkOccurrences(false); } else { @@ -777,7 +777,7 @@ public void loadPreferences() { setWhitespaceVisible(prefService.getBoolean(getClass(), WHITESPACE_VISIBLE_PREFS, isWhitespaceVisible())); setPaintTabLines(prefService.getBoolean(getClass(), TABLINES_VISIBLE_PREFS, getPaintTabLines())); setAutoCompletion(prefService.getBoolean(getClass(), AUTOCOMPLETE_PREFS, true)); - setKeylessAutoCompletion(prefService.getBoolean(getClass(), AUTOCOMPLETE_NOKEY_PREFS, false)); + setKeylessAutoCompletion(prefService.getBoolean(getClass(), AUTOCOMPLETE_KEYLESS_PREFS, true)); // true for backwards compatibility with IJ1 macro setFallbackAutoCompletion(prefService.getBoolean(getClass(), AUTOCOMPLETE_FALLBACK_PREFS, false)); setMarkOccurrences(prefService.getBoolean(getClass(), MARK_OCCURRENCES_PREFS, false)); } @@ -802,7 +802,7 @@ public void savePreferences(final String top_folders, final String theme) { prefService.put(getClass(), WHITESPACE_VISIBLE_PREFS, isWhitespaceVisible()); prefService.put(getClass(), TABLINES_VISIBLE_PREFS, getPaintTabLines()); prefService.put(getClass(), AUTOCOMPLETE_PREFS, isAutoCompletionEnabled()); - prefService.put(getClass(), AUTOCOMPLETE_NOKEY_PREFS, isKeylessAutoCompletionEnabled()); + prefService.put(getClass(), AUTOCOMPLETE_KEYLESS_PREFS, isKeylessAutoCompletionEnabled()); prefService.put(getClass(), AUTOCOMPLETE_FALLBACK_PREFS, isFallbackAutoCompletionEnabled()); if (null != top_folders) prefService.put(getClass(), FOLDERS_PREFS, top_folders); if (null != theme) prefService.put(getClass(), THEME_PREFS, theme); diff --git a/src/main/java/org/scijava/ui/swing/script/FileSystemTree.java b/src/main/java/org/scijava/ui/swing/script/FileSystemTree.java index 937f31a1..4c78e679 100644 --- a/src/main/java/org/scijava/ui/swing/script/FileSystemTree.java +++ b/src/main/java/org/scijava/ui/swing/script/FileSystemTree.java @@ -275,21 +275,6 @@ public FileSystemTree(final Logger log) setAutoscrolls(true); setScrollsOnExpand(true); setExpandsSelectedPaths(true); - new FileDrop(this, files -> { - final List dirs = Arrays.asList(files).stream().filter(f -> f.isDirectory()) - .collect(Collectors.toList()); - if (dirs.isEmpty()) { - JOptionPane.showMessageDialog(FileSystemTree.this, "Only folders can be dropped into the file tree.", - "Invalid Drop", JOptionPane.WARNING_MESSAGE); - return; - } - final boolean confirm = dirs.size() < 4 || (JOptionPane.showConfirmDialog(FileSystemTree.this, - "Confirm loading of " + dirs.size() + " folders?", "Confirm?", - JOptionPane.OK_CANCEL_OPTION) == JOptionPane.OK_OPTION); - if (confirm) { - dirs.forEach(dir -> addRootDirectory(dir.getAbsolutePath(), true)); - } - }); addTreeWillExpandListener(new TreeWillExpandListener() { @Override public void treeWillExpand(TreeExpansionEvent event) throws ExpandVetoException { diff --git a/src/main/java/org/scijava/ui/swing/script/FileSystemTreePanel.java b/src/main/java/org/scijava/ui/swing/script/FileSystemTreePanel.java index 9b16b6fc..f3449649 100644 --- a/src/main/java/org/scijava/ui/swing/script/FileSystemTreePanel.java +++ b/src/main/java/org/scijava/ui/swing/script/FileSystemTreePanel.java @@ -33,6 +33,7 @@ import java.awt.Desktop; import java.awt.Dimension; import java.awt.Font; +import java.awt.FontMetrics; import java.awt.Graphics2D; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; @@ -42,9 +43,14 @@ import java.awt.event.FocusEvent; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; +import java.awt.font.FontRenderContext; +import java.awt.geom.Rectangle2D; import java.io.File; +import java.util.Arrays; +import java.util.List; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; +import java.util.stream.Collectors; import javax.swing.FocusManager; import javax.swing.JButton; @@ -54,6 +60,7 @@ import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; +import javax.swing.JScrollPane; import javax.swing.JTextField; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.TreePath; @@ -109,7 +116,23 @@ class FileSystemTreePanel extends JPanel { bc.weightx = 1.0; bc.weighty = 1.0; bc.fill = GridBagConstraints.BOTH; - add(tree, bc); + final JScrollPane treePane = new JScrollPane(tree); + add(treePane, bc); + new FileDrop(treePane, files -> { + final List dirs = Arrays.asList(files).stream().filter(f -> f.isDirectory()) + .collect(Collectors.toList()); + if (dirs.isEmpty()) { + JOptionPane.showMessageDialog(this, "Only folders can be dropped into the file tree.", + "Invalid Drop", JOptionPane.WARNING_MESSAGE); + return; + } + final boolean confirm = dirs.size() < 4 || (JOptionPane.showConfirmDialog(this, + "Confirm loading of " + dirs.size() + " folders?", "Confirm?", + JOptionPane.OK_CANCEL_OPTION) == JOptionPane.OK_OPTION); + if (confirm) { + dirs.forEach(dir -> tree.addRootDirectory(dir.getAbsolutePath(), true)); + } + }); addContextualMenuToTree(); } @@ -390,6 +413,21 @@ private class SearchField extends JTextField { private static final String CASE_HOLDER = "[Aa]"; private static final String DEF_HOLDER = "File filter... "; + SearchField() { + super(); + try { + // make sure pane is large enough to display placeholders + final FontMetrics fm = getFontMetrics(getFont()); + final FontRenderContext frc = fm.getFontRenderContext(); + final String buf = CASE_HOLDER + REGEX_HOLDER + DEF_HOLDER; + final Rectangle2D rect = getFont().getStringBounds(buf, frc); + final int prefWidth = (int) rect.getWidth(); + setColumns(prefWidth / super.getColumnWidth()); + } catch (final Exception ignored) { + // do nothing + } + } + void update() { update(getGraphics()); } diff --git a/src/main/java/org/scijava/ui/swing/script/TextEditor.java b/src/main/java/org/scijava/ui/swing/script/TextEditor.java index f9130237..c3e084db 100644 --- a/src/main/java/org/scijava/ui/swing/script/TextEditor.java +++ b/src/main/java/org/scijava/ui/swing/script/TextEditor.java @@ -297,13 +297,7 @@ public TextEditor(final Context context) { // NB: All panes must be initialized before menus are assembled! tabbed = new JTabbedPane(); tree = new FileSystemTree(log); - final JScrollPane scrolltree = new JScrollPane(new FileSystemTreePanel(tree, context)); - // set borders. Needed for drag & drop and collapsing split pane - //tabbed.setBorder(BorderFactory.createEmptyBorder(0,BORDER_SIZE,0,BORDER_SIZE)); - //scrolltree.setBorder(BorderFactory.createEmptyBorder(0,0,0,BORDER_SIZE)); - scrolltree.setPreferredSize(new Dimension(200, 600)); - body = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, scrolltree, tabbed); - + body = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, new FileSystemTreePanel(tree, context), tabbed); // These items are dynamic and need to be initialized before EditorPane creation initializeDynamicMenuComponents(); @@ -2317,8 +2311,17 @@ public void runText(final boolean selectionOnly) { * Run current script with the batch processor */ public void runBatch() { + if (null == getCurrentLanguage()) { + error("Select a language first! Also, please note that this option\n" + + "requires at least one @File parameter to be declared in the script."); + return; + } // get script from current tab final String script = getTab().getEditorPane().getText(); + if (script.trim().isEmpty()) { + error("This option requires at least one @File parameter to be declared."); + return; + } final ScriptInfo info = new ScriptInfo(context, // "dummy." + getCurrentLanguage().getExtensions().get(0), // new StringReader(script)); From e2625bafe812aef28ae588907882de43762cf6bc Mon Sep 17 00:00:00 2001 From: tferr Date: Thu, 24 Feb 2022 09:14:18 -0500 Subject: [PATCH 17/21] GUI tweaks - GUI Tweaks: - Improve dialogs, capitalization, tooltips, etc. - Organize options menu & disable non-applicable options as needed - Use Find/Replace (as opposed to segregated commands) - Correct changeListener -> ItemListener - Minor code/API cleanup --- .../scijava/ui/swing/script/EditorPane.java | 46 +++++++++--- .../ui/swing/script/FindAndReplaceDialog.java | 12 ++-- .../scijava/ui/swing/script/TextEditor.java | 70 ++++++++++--------- .../ui/swing/script/TextEditorTab.java | 16 ++--- .../swing/script/commands/ChooseFontSize.java | 2 +- .../swing/script/commands/ChooseTabSize.java | 2 +- 6 files changed, 87 insertions(+), 61 deletions(-) diff --git a/src/main/java/org/scijava/ui/swing/script/EditorPane.java b/src/main/java/org/scijava/ui/swing/script/EditorPane.java index 72c912b7..15b428ae 100644 --- a/src/main/java/org/scijava/ui/swing/script/EditorPane.java +++ b/src/main/java/org/scijava/ui/swing/script/EditorPane.java @@ -93,7 +93,7 @@ public class EditorPane extends RSyntaxTextArea implements DocumentListener { private boolean undoInProgress; private boolean redoInProgress; - private boolean autoCompletionEnabled = true; + private boolean autoCompletionEnabled; private boolean autoCompletionJavaFallback; private boolean autoCompletionWithoutKey; @@ -123,6 +123,7 @@ public EditorPane() { setCloseMarkupTags(true); setCodeFoldingEnabled(true); setShowMatchedBracketPopup(true); + setClearWhitespaceLinesEnabled(false); // most folks wont't want this set? // load preferences loadPreferences(); @@ -532,28 +533,51 @@ protected void setLanguage(final ScriptLanguage language, support.install(this); } - public void setAutoCompletion(boolean value) { - autoCompletionEnabled = value; - if (currentLanguage != null) setLanguage(currentLanguage); + /** + * Toggles whether auto-completion is enabled. + * + * @param enabled Whether auto-activation is enabled. + */ + public void setAutoCompletion(final boolean enabled) { + autoCompletionEnabled = enabled; + if (currentLanguage != null) + setLanguage(currentLanguage); } - void setFallbackAutoCompletion(boolean value) { + /** + * Toggles whether auto-completion should adopt Java completions if the current + * language does not support auto-completion. + * + * @param enabled Whether Java should be enabled as fallback language for + * auto-completion + */ + void setFallbackAutoCompletion(final boolean value) { autoCompletionJavaFallback = value; + if (autoCompletionEnabled && currentLanguage != null) + setLanguage(currentLanguage); } - void setKeylessAutoCompletion(boolean value) { - autoCompletionWithoutKey = value; + /** + * Toggles whether auto-activation of auto-completion is enabled. Ignored if + * auto-completion is not enabled. + * + * @param enabled Whether auto-activation is enabled. + */ + void setKeylessAutoCompletion(final boolean enabled) { + autoCompletionWithoutKey = enabled; + if (autoCompletionEnabled && currentLanguage != null) + setLanguage(currentLanguage); } public boolean isAutoCompletionEnabled() { return autoCompletionEnabled; } - public boolean isKeylessAutoCompletionEnabled() { + public boolean isAutoCompletionKeyless() { return autoCompletionWithoutKey; } - public boolean isFallbackAutoCompletionEnabled() { + public boolean isAutoCompletionFallbackEnabled() { return autoCompletionJavaFallback; } @@ -802,8 +826,8 @@ public void savePreferences(final String top_folders, final String theme) { prefService.put(getClass(), WHITESPACE_VISIBLE_PREFS, isWhitespaceVisible()); prefService.put(getClass(), TABLINES_VISIBLE_PREFS, getPaintTabLines()); prefService.put(getClass(), AUTOCOMPLETE_PREFS, isAutoCompletionEnabled()); - prefService.put(getClass(), AUTOCOMPLETE_KEYLESS_PREFS, isKeylessAutoCompletionEnabled()); - prefService.put(getClass(), AUTOCOMPLETE_FALLBACK_PREFS, isFallbackAutoCompletionEnabled()); + prefService.put(getClass(), AUTOCOMPLETE_KEYLESS_PREFS, isAutoCompletionKeyless()); + prefService.put(getClass(), AUTOCOMPLETE_FALLBACK_PREFS, isAutoCompletionFallbackEnabled()); if (null != top_folders) prefService.put(getClass(), FOLDERS_PREFS, top_folders); if (null != theme) prefService.put(getClass(), THEME_PREFS, theme); } diff --git a/src/main/java/org/scijava/ui/swing/script/FindAndReplaceDialog.java b/src/main/java/org/scijava/ui/swing/script/FindAndReplaceDialog.java index 37a1a86b..a476e3d8 100644 --- a/src/main/java/org/scijava/ui/swing/script/FindAndReplaceDialog.java +++ b/src/main/java/org/scijava/ui/swing/script/FindAndReplaceDialog.java @@ -80,8 +80,8 @@ public FindAndReplaceDialog(final TextEditor editor) { c.ipadx = c.ipady = 1; c.fill = GridBagConstraints.HORIZONTAL; c.anchor = GridBagConstraints.LINE_START; - searchField = createField("Find Next", text, c, null); - replaceField = createField("Replace with", text, c, this); + searchField = createField("Find: ", text, c, null); + replaceField = createField("Replace with: ", text, c, this); c.gridwidth = 4; c.gridheight = c.gridy; @@ -95,14 +95,14 @@ public FindAndReplaceDialog(final TextEditor editor) { c.gridwidth = 1; c.gridheight = 1; c.weightx = 0.001; - matchCase = createCheckBox("Match Case", root, c); + matchCase = createCheckBox("Match case", root, c); regex = createCheckBox("Regex", root, c); forward = createCheckBox("Search forward", root, c); forward.setSelected(true); c.gridx = 0; c.gridy++; - markAll = createCheckBox("Mark All", root, c); - wholeWord = createCheckBox("Whole Word", root, c); + markAll = createCheckBox("Mark all", root, c); + wholeWord = createCheckBox("Whole word", root, c); c.gridx = 4; c.gridy = 0; @@ -137,7 +137,7 @@ protected RSyntaxTextArea getTextArea() { @Override public void show(final boolean replace) { - setTitle(replace ? "Replace" : "Find"); + setTitle(replace ? "Find/Replace" : "Find"); replaceLabel.setEnabled(replace); replaceField.setEnabled(replace); replaceField.setBackground(replace ? searchField.getBackground() diff --git a/src/main/java/org/scijava/ui/swing/script/TextEditor.java b/src/main/java/org/scijava/ui/swing/script/TextEditor.java index c3e084db..f4c0fb74 100644 --- a/src/main/java/org/scijava/ui/swing/script/TextEditor.java +++ b/src/main/java/org/scijava/ui/swing/script/TextEditor.java @@ -347,13 +347,12 @@ public TextEditor(final Context context) { copy = addToMenu(edit, "Copy", KeyEvent.VK_C, ctrl); paste = addToMenu(edit, "Paste", KeyEvent.VK_V, ctrl); addSeparator(edit, "Find:"); - find = addToMenu(edit, "Find...", KeyEvent.VK_F, ctrl); + find = addToMenu(edit, "Find/Replace...", KeyEvent.VK_F, ctrl); find.setMnemonic(KeyEvent.VK_F); findNext = addToMenu(edit, "Find Next", KeyEvent.VK_F3, 0); findNext.setMnemonic(KeyEvent.VK_N); findPrevious = addToMenu(edit, "Find Previous", KeyEvent.VK_F3, shift); findPrevious.setMnemonic(KeyEvent.VK_P); - replace = addToMenu(edit, "Find and Replace...", KeyEvent.VK_H, ctrl); addSeparator(edit, "Goto:"); gotoLine = addToMenu(edit, "Goto Line...", KeyEvent.VK_G, ctrl); @@ -566,13 +565,9 @@ public TextEditor(final Context context) { options.add(fontSizeMenu); addSeparator(options, "Indentation:"); - paintTabs = new JCheckBoxMenuItem("Indent Guides"); - paintTabs.setMnemonic(KeyEvent.VK_I); - paintTabs.addChangeListener(e -> getEditorPane().setPaintTabLines(paintTabs.getState())); - options.add(paintTabs); tabsEmulated = new JCheckBoxMenuItem("Indent Using Spaces"); tabsEmulated.setMnemonic(KeyEvent.VK_S); - tabsEmulated.addChangeListener(e -> getEditorPane().setTabsEmulated(tabsEmulated.getState())); + tabsEmulated.addItemListener(e -> setTabsEmulated(tabsEmulated.getState())); options.add(tabsEmulated); tabSizeMenu = new JMenu("Tab Width"); tabSizeMenu.setMnemonic(KeyEvent.VK_T); @@ -598,6 +593,7 @@ public TextEditor(final Context context) { addSeparator(options, "View:"); options.add(whiteSpace); + options.add(paintTabs); options.add(markOccurences); options.add(wrapLines); options.add(applyThemeMenu()); @@ -894,23 +890,24 @@ private void initializeDynamicMenuComponents() { // Options menu. These will be updated once EditorPane is created wrapLines = new JCheckBoxMenuItem("Wrap Lines", false); wrapLines.setMnemonic(KeyEvent.VK_W); - wrapLines.addChangeListener(e -> setWrapLines(wrapLines.getState())); + wrapLines.addItemListener(e -> setWrapLines(wrapLines.getState())); markOccurences = new JCheckBoxMenuItem("Mark Occurences", false); markOccurences.setToolTipText("Highlights all occurrences of a selected element"); - markOccurences.addChangeListener(e -> setMarkOccurrences(markOccurences.getState())); - whiteSpace = new JCheckBoxMenuItem("Label Whitespace", false); - whiteSpace.setMnemonic(KeyEvent.VK_L); - whiteSpace.addChangeListener(e -> setWhiteSpaceVisible(whiteSpace.isSelected())); + markOccurences.addItemListener(e -> setMarkOccurrences(markOccurences.getState())); + whiteSpace = new JCheckBoxMenuItem("Show Whitespace", false); + whiteSpace.addItemListener(e -> setWhiteSpaceVisible(whiteSpace.isSelected())); + paintTabs = new JCheckBoxMenuItem("Show Indent Guides"); + paintTabs.addItemListener(e -> setPaintTabLines(paintTabs.getState())); autocompletion = new JCheckBoxMenuItem("Enable Autocompletion", true); - autocompletion.addChangeListener(e -> setAutoCompletionEnabled(autocompletion.getState())); + autocompletion.setToolTipText("NB: Not all languages support this feature"); + autocompletion.addItemListener(e -> setAutoCompletionEnabled(autocompletion.getState())); keylessAutocompletion = new JCheckBoxMenuItem("Show Completions Without Ctrl+Space", false); - keylessAutocompletion.setToolTipText("If selected, the completion pop-up automatically appears" - + " while typing
NB: Not all languages support this feature"); - keylessAutocompletion.addChangeListener(e -> setKeylessAutoCompletion(keylessAutocompletion.getState())); - fallbackAutocompletion = new JCheckBoxMenuItem("Unsupported languages: Fallback to Java", false); + keylessAutocompletion.setToolTipText("If selected, the completion pop-up automatically appears while typing"); + keylessAutocompletion.addItemListener(e -> setKeylessAutoCompletion(keylessAutocompletion.getState())); + fallbackAutocompletion = new JCheckBoxMenuItem("Use Java Completions as Fallback", false); fallbackAutocompletion.setToolTipText("If selected, Java completions will be used when scripting
" + "a language for which auto-completions are not available"); - fallbackAutocompletion.addChangeListener(e -> setFallbackAutoCompletion(fallbackAutocompletion.getState())); + fallbackAutocompletion.addItemListener(e -> setFallbackAutoCompletion(fallbackAutocompletion.getState())); themeRadioGroup = new ButtonGroup(); // Help menu. These are 'dynamic' items @@ -1347,10 +1344,9 @@ else if (source == close) if (tabbed.getTabCount() < 2) processWindowEvent(new W else if (source == paste) getTextArea().paste(); else if (source == undo) getTextArea().undoLastAction(); else if (source == redo) getTextArea().redoLastAction(); - else if (source == find) findOrReplace(false); + else if (source == find) findOrReplace(true); else if (source == findNext) findDialog.searchOrReplace(false); else if (source == findPrevious) findDialog.searchOrReplace(false, false); - else if (source == replace) findOrReplace(true); else if (source == gotoLine) gotoLine(); else if (source == toggleBookmark) toggleBookmark(); else if (source == listBookmarks) listBookmarks(); @@ -1430,39 +1426,45 @@ else if (source == increaseFontSize || source == decreaseFontSize) { } private void setAutoCompletionEnabled(final boolean enabled) { - for (int i = 0; i < tabbed.getTabCount(); i++) { + for (int i = 0; i < tabbed.getTabCount(); i++) getEditorPane(i).setAutoCompletion(enabled); - } + keylessAutocompletion.setEnabled(enabled); + fallbackAutocompletion.setEnabled(enabled); + } + + private void setTabsEmulated(final boolean emulated) { + for (int i = 0; i < tabbed.getTabCount(); i++) + getEditorPane(i).setTabsEmulated(emulated); + } + + private void setPaintTabLines(final boolean paint) { + for (int i = 0; i < tabbed.getTabCount(); i++) + getEditorPane(i).setPaintTabLines(paint); } private void setKeylessAutoCompletion(final boolean noKeyRequired) { - for (int i = 0; i < tabbed.getTabCount(); i++) { + for (int i = 0; i < tabbed.getTabCount(); i++) getEditorPane(i).setKeylessAutoCompletion(noKeyRequired); - } } private void setFallbackAutoCompletion(final boolean fallback) { - for (int i = 0; i < tabbed.getTabCount(); i++) { + for (int i = 0; i < tabbed.getTabCount(); i++) getEditorPane(i).setFallbackAutoCompletion(fallback); - } } private void setMarkOccurrences(final boolean markOccurrences) { - for (int i = 0; i < tabbed.getTabCount(); i++) { + for (int i = 0; i < tabbed.getTabCount(); i++) getEditorPane(i).setMarkOccurrences(markOccurrences); - } } private void setWhiteSpaceVisible(final boolean visible) { - for (int i = 0; i < tabbed.getTabCount(); i++) { + for (int i = 0; i < tabbed.getTabCount(); i++) getEditorPane(i).setWhitespaceVisible(visible); - } } private void setWrapLines(final boolean wrap) { - for (int i = 0; i < tabbed.getTabCount(); i++) { + for (int i = 0; i < tabbed.getTabCount(); i++) getEditorPane(i).setLineWrap(wrap); - } } private JMenu applyThemeMenu() { @@ -1600,7 +1602,7 @@ public void findOrReplace(final boolean doReplace) { public void gotoLine() { final String line = - JOptionPane.showInputDialog(this, "Line:", "Goto line...", + JOptionPane.showInputDialog(this, "Enter line number:", "Goto Line", JOptionPane.QUESTION_MESSAGE); if (line == null) return; try { @@ -2065,7 +2067,7 @@ else if (tabSize == Integer.parseInt(item.getText())) { paintTabs.setState(pane.getPaintTabLines()); whiteSpace.setState(pane.isWhitespaceVisible()); autocompletion.setState(pane.isAutoCompletionEnabled()); - keylessAutocompletion.setState(pane.isKeylessAutoCompletionEnabled()); + keylessAutocompletion.setState(pane.isAutoCompletionKeyless()); } public void setEditorPaneFileName(final String baseName) { diff --git a/src/main/java/org/scijava/ui/swing/script/TextEditorTab.java b/src/main/java/org/scijava/ui/swing/script/TextEditorTab.java index eed60a78..2108a5b0 100644 --- a/src/main/java/org/scijava/ui/swing/script/TextEditorTab.java +++ b/src/main/java/org/scijava/ui/swing/script/TextEditorTab.java @@ -261,16 +261,16 @@ public void actionPerformed(final ActionEvent e) { bc.gridx = 3; final JButton prompt_help = new JButton("?"); prompt_help.addActionListener(a -> { - final String msg = "This REPL (read-evaluate-print-loop) parses " + textEditor.getCurrentLanguage().getLanguageName() + " code.\n\n" + final String msg = "This REPL (Read-Evaluate-Print-Loop) parses " + textEditor.getCurrentLanguage().getLanguageName() + " code.\n\n" + "Key bindings:\n" - + "* enter: evaluate code\n" - + "* shift+enter: add line break (also alt-enter and meta-enter)\n" - + "* page UP or ctrl+p: show previous entry in the history\n" - + "* page DOWN or ctrl+n: show next entry in the history\n" + + " [Enter]: Evaluate code\n" + + " [Shift+Enter]: Add line break (also alt-enter and meta-enter)\n" + + " [Page UP] or [Ctrl+P]: Show previous entry in the history\n" + + " [Page DOWN] or [Ctrl+N]: Show next entry in the history\n" + "\n" - + "If 'Use arrow keys' is checked, then up/down arrows work like page UP/DOWN,\n" - + "and shift+up/down arrow work like arrow keys before for caret movement\n" - + "within a multi-line prompt." + + "If 'Use arrow keys' is checked, then up/down arrows work like\n" + + "Page UP/DOWN, and Shift+up/down arrows work like arrow\n" + + "keys before for caret movement within a multi-line prompt." ; JOptionPane.showMessageDialog(textEditor, msg, "REPL Help", JOptionPane.INFORMATION_MESSAGE); }); diff --git a/src/main/java/org/scijava/ui/swing/script/commands/ChooseFontSize.java b/src/main/java/org/scijava/ui/swing/script/commands/ChooseFontSize.java index bdb97669..5b8e11a3 100644 --- a/src/main/java/org/scijava/ui/swing/script/commands/ChooseFontSize.java +++ b/src/main/java/org/scijava/ui/swing/script/commands/ChooseFontSize.java @@ -54,7 +54,7 @@ public void run() { final float size = editor.getEditorPane().getFontSize(); changeFontSize(editor.getErrorScreen(), size); changeFontSize(editor.getTab().getScreenInstance(), size); - editor.updateTabAndFontSize(false); + editor.updateUI(false); } private void changeFontSize(final JTextArea a, final float size) { diff --git a/src/main/java/org/scijava/ui/swing/script/commands/ChooseTabSize.java b/src/main/java/org/scijava/ui/swing/script/commands/ChooseTabSize.java index 1caf1001..c26166f2 100644 --- a/src/main/java/org/scijava/ui/swing/script/commands/ChooseTabSize.java +++ b/src/main/java/org/scijava/ui/swing/script/commands/ChooseTabSize.java @@ -54,7 +54,7 @@ public class ChooseTabSize extends DynamicCommand { @Override public void run() { editor.getEditorPane().setTabSize(tabSize); - editor.updateTabAndFontSize(false); + editor.updateUI(false); } protected void initializeChoice() { From 7bbb378229d23b4376d50f6d076b357cd85c573d Mon Sep 17 00:00:00 2001 From: tferr Date: Thu, 24 Feb 2022 10:40:19 -0500 Subject: [PATCH 18/21] Reinstate bookmarks (broken for many years now) --- .../scijava/ui/swing/script/EditorPane.java | 47 ++++++++++++++++--- .../scijava/ui/swing/script/TextEditor.java | 44 ++++++++++++----- 2 files changed, 72 insertions(+), 19 deletions(-) diff --git a/src/main/java/org/scijava/ui/swing/script/EditorPane.java b/src/main/java/org/scijava/ui/swing/script/EditorPane.java index 15b428ae..e1044eff 100644 --- a/src/main/java/org/scijava/ui/swing/script/EditorPane.java +++ b/src/main/java/org/scijava/ui/swing/script/EditorPane.java @@ -33,7 +33,9 @@ import java.awt.Container; import java.awt.Dimension; import java.awt.Font; +import java.awt.Graphics2D; import java.awt.event.ActionEvent; +import java.awt.image.BufferedImage; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; @@ -45,6 +47,8 @@ import java.util.Collection; import java.util.List; +import javax.swing.ImageIcon; +import javax.swing.JOptionPane; import javax.swing.JScrollPane; import javax.swing.JViewport; import javax.swing.ToolTipManager; @@ -60,7 +64,6 @@ import org.fife.ui.rsyntaxtextarea.SyntaxScheme; import org.fife.ui.rtextarea.Gutter; import org.fife.ui.rtextarea.GutterIconInfo; -import org.fife.ui.rtextarea.IconGroup; import org.fife.ui.rtextarea.RTextArea; import org.fife.ui.rtextarea.RTextScrollPane; import org.fife.ui.rtextarea.RecordableTextAction; @@ -88,7 +91,6 @@ public class EditorPane extends RSyntaxTextArea implements DocumentListener { private long fileLastModified; private ScriptLanguage currentLanguage; private Gutter gutter; - private IconGroup iconGroup; private int modifyCount; private boolean undoInProgress; @@ -161,15 +163,39 @@ public RTextScrollPane wrappedInScrollbars() { final RTextScrollPane sp = new RTextScrollPane(this); sp.setPreferredSize(new Dimension(600, 350)); sp.setIconRowHeaderEnabled(true); - gutter = sp.getGutter(); - iconGroup = new IconGroup("bullets", "images/", null, "png", null); - gutter.setBookmarkIcon(iconGroup.getIcon("var")); gutter.setBookmarkingEnabled(true); - + updateBookmarkIcon(); + gutter.setShowCollapsedRegionToolTips(true); + gutter.setFoldIndicatorEnabled(true); return sp; } + protected void updateBookmarkIcon() { + // this will clear existing bookmarks, so we'll need restore existing ones + final GutterIconInfo[] stash = gutter.getBookmarks(); + gutter.setBookmarkIcon(createBookmarkIcon()); + try { + for (final GutterIconInfo info : stash) + gutter.toggleBookmark(info.getMarkedOffset()); + } catch (final BadLocationException ignored) { + JOptionPane.showMessageDialog(this, "Some bookmarks may have been lost.", "Lost Bookmarks", + JOptionPane.WARNING_MESSAGE); + } + } + + private ImageIcon createBookmarkIcon() { + final int size = gutter.getLineNumberFont().getSize(); + final BufferedImage image = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB); + final Graphics2D graphics = image.createGraphics(); + graphics.setColor(gutter.getLineNumberColor()); + graphics.fillRect(0, 0, size, size); + graphics.setXORMode(getCurrentLineHighlightColor()); + graphics.drawRect(0, 0, size - 1, size - 1); + image.flush(); + return new ImageIcon(image); + } + /** * TODO * @@ -633,6 +659,12 @@ public void increaseFontSize(final float factor) { final float size = Math.max(5, font.getSize2D() * factor); setFont(font.deriveFont(size)); setSyntaxScheme(scheme); + // Adjust gutter size + if (gutter != null) { + final float lnSize = size * 0.8f; + gutter.setLineNumberFont(font.deriveFont(lnSize)); + updateBookmarkIcon(); + } Component parent = getParent(); if (parent instanceof JViewport) { parent = parent.getParent(); @@ -669,7 +701,8 @@ public void toggleBookmark(final int line) { } catch (final BadLocationException e) { /* ignore */ - log.error("Cannot toggle bookmark at this location."); + JOptionPane.showMessageDialog(this, "Cannot toggle bookmark at this location.", "Error", + JOptionPane.ERROR_MESSAGE); } } } diff --git a/src/main/java/org/scijava/ui/swing/script/TextEditor.java b/src/main/java/org/scijava/ui/swing/script/TextEditor.java index f4c0fb74..b90ad3fa 100644 --- a/src/main/java/org/scijava/ui/swing/script/TextEditor.java +++ b/src/main/java/org/scijava/ui/swing/script/TextEditor.java @@ -212,12 +212,12 @@ public class TextEditor extends JFrame implements ActionListener, private JTabbedPane tabbed; private JMenuItem newFile, open, save, saveas, compileAndRun, compile, - close, undo, redo, cut, copy, paste, find, replace, selectAll, kill, + close, undo, redo, cut, copy, paste, find, selectAll, kill, gotoLine, makeJar, makeJarWithSource, removeUnusedImports, sortImports, removeTrailingWhitespace, findNext, findPrevious, openHelp, addImport, nextError, previousError, openHelpWithoutFrames, nextTab, - previousTab, runSelection, extractSourceJar, toggleBookmark, - listBookmarks, openSourceForClass, + previousTab, runSelection, extractSourceJar, + openSourceForClass, //openSourceForMenuItem, // this never had an actionListener!?? openMacroFunctions, decreaseFontSize, increaseFontSize, chooseFontSize, chooseTabSize, gitGrep, replaceTabsWithSpaces, @@ -357,10 +357,15 @@ public TextEditor(final Context context) { addSeparator(edit, "Goto:"); gotoLine = addToMenu(edit, "Goto Line...", KeyEvent.VK_G, ctrl); gotoLine.setMnemonic(KeyEvent.VK_G); - toggleBookmark = addToMenu(edit, "Toggle Bookmark", KeyEvent.VK_B, ctrl); + + final JMenuItem toggleBookmark = addToMenu(edit, "Toggle Bookmark", KeyEvent.VK_B, ctrl); toggleBookmark.setMnemonic(KeyEvent.VK_B); - listBookmarks = addToMenu(edit, "List Bookmarks...", 0, 0); - listBookmarks.setMnemonic(KeyEvent.VK_O); + toggleBookmark.addActionListener( e -> toggleBookmark()); + final JMenuItem listBookmarks = addToMenu(edit, "List Bookmarks...", 0, 0); + listBookmarks.setMnemonic(KeyEvent.VK_L); + listBookmarks.addActionListener( e -> listBookmarks()); + final JMenuItem clearBookmarks = addToMenu(edit, "Clear Bookmarks...", 0, 0); + clearBookmarks.addActionListener(e -> clearAllBookmarks()); addSeparator(edit, "Utilities:"); removeTrailingWhitespace = addToMenu(edit, "Remove Trailing Whitespace", 0, 0); @@ -1348,8 +1353,6 @@ else if (source == close) if (tabbed.getTabCount() < 2) processWindowEvent(new W else if (source == findNext) findDialog.searchOrReplace(false); else if (source == findPrevious) findDialog.searchOrReplace(false, false); else if (source == gotoLine) gotoLine(); - else if (source == toggleBookmark) toggleBookmark(); - else if (source == listBookmarks) listBookmarks(); else if (source == selectAll) { getTextArea().setCaretPosition(0); getTextArea().moveCaretPosition(getTextArea().getDocument().getLength()); @@ -1523,6 +1526,7 @@ private void applyTheme(final String theme, final boolean updateUI) throws Illeg final float existingFontSize = ep.getFontSize(); th.apply(ep); ep.setFontSize(existingFontSize); + ep.updateBookmarkIcon(); // update bookmark icon color } } catch (final Exception ex) { throw new IllegalArgumentException(ex); @@ -1624,20 +1628,36 @@ public void toggleBookmark() { getEditorPane().toggleBookmark(); } - public void listBookmarks() { + private Vector getAllBookmarks() { final Vector bookmarks = new Vector<>(); - for (int i = 0; i < tabbed.getTabCount(); i++) { final TextEditorTab tab = (TextEditorTab) tabbed.getComponentAt(i); tab.editorPane.getBookmarks(tab, bookmarks); } if (bookmarks.isEmpty()) { - JOptionPane.showMessageDialog(this, "No Bookmarks currently exist."); - } else { + JOptionPane.showMessageDialog(this, "No Bookmarks currently exist.\n" + + "You can bookmark lines by clicking next to their line number."); + } + return bookmarks; + } + + public void listBookmarks() { + final Vector bookmarks = getAllBookmarks(); + if (!getAllBookmarks().isEmpty()) { new BookmarkDialog(this, bookmarks).setVisible(true); } } + void clearAllBookmarks() { + final Vector bookmarks = getAllBookmarks(); + if (bookmarks.isEmpty()) + return; + if (JOptionPane.showConfirmDialog(TextEditor.this, "Delete all bookmarks?", "Confirm Deletion?", + JOptionPane.OK_CANCEL_OPTION) == JOptionPane.OK_OPTION) { + bookmarks.forEach(bk -> bk.tab.editorPane.toggleBookmark(bk.getLineNumber())); + } + } + public boolean reload() { return reload("Reload the file?"); } From 8ea4784c80a0343b34e9d3b19c5e4eeae1df31c7 Mon Sep 17 00:00:00 2001 From: tferr Date: Thu, 24 Feb 2022 10:41:46 -0500 Subject: [PATCH 19/21] Add fallbacks for auto-completion --- .../scijava/ui/swing/script/EditorPane.java | 37 +++++++++++++++++-- .../scijava/ui/swing/script/TextEditor.java | 6 ++- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/scijava/ui/swing/script/EditorPane.java b/src/main/java/org/scijava/ui/swing/script/EditorPane.java index e1044eff..71526e2f 100644 --- a/src/main/java/org/scijava/ui/swing/script/EditorPane.java +++ b/src/main/java/org/scijava/ui/swing/script/EditorPane.java @@ -58,6 +58,7 @@ import javax.swing.text.DefaultEditorKit; import org.fife.rsta.ac.LanguageSupport; +import org.fife.rsta.ac.LanguageSupportFactory; import org.fife.ui.rsyntaxtextarea.RSyntaxDocument; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; import org.fife.ui.rsyntaxtextarea.Style; @@ -98,6 +99,7 @@ public class EditorPane extends RSyntaxTextArea implements DocumentListener { private boolean autoCompletionEnabled; private boolean autoCompletionJavaFallback; private boolean autoCompletionWithoutKey; + private String supportStatus; @Parameter Context context; @@ -549,14 +551,37 @@ protected void setLanguage(final ScriptLanguage language, setText(header += getText()); } - if (!autoCompletionEnabled) return; - + String supportLevel = "SciJava supported"; // try to get language support for current language, may be null. support = languageSupportService.getLanguageSupport(currentLanguage); - if (support == null && autoCompletionJavaFallback) + + // that did not work. See if there is internal support for it. + if (support == null) { + support = LanguageSupportFactory.get().getSupportFor(styleName); + supportLevel = "Legacy supported"; + } + // that did not work, Fallback to Java + if (!"None".equals(languageName) && support == null && autoCompletionJavaFallback) { support = languageSupportService.getLanguageSupport(scriptService.getLanguageByName("Java")); - if (support != null) + supportLevel = "N/A. Using Java as fallback"; + } + if (support != null) { + support.setAutoCompleteEnabled(autoCompletionEnabled); + support.setAutoActivationEnabled(autoCompletionWithoutKey); + support.setAutoActivationDelay(200); support.install(this); + if (!autoCompletionEnabled) + supportLevel += " but currently disabled\n"; + else { + supportLevel += " triggered by Ctrl+Space"; + if (autoCompletionWithoutKey) + supportLevel += " & auto-display "; + supportLevel += "\n"; + } + } else { + supportLevel = "N/A"; + } + supportStatus = "Active language: " + languageName + "\nAutocompletion: " + supportLevel; } /** @@ -872,4 +897,8 @@ public void resetTabSize() { setTabSize(prefService.getInt(getClass(), TAB_SIZE_PREFS, DEFAULT_TAB_SIZE)); } + String getSupportStatus() { + return supportStatus; + } + } diff --git a/src/main/java/org/scijava/ui/swing/script/TextEditor.java b/src/main/java/org/scijava/ui/swing/script/TextEditor.java index b90ad3fa..12f89470 100644 --- a/src/main/java/org/scijava/ui/swing/script/TextEditor.java +++ b/src/main/java/org/scijava/ui/swing/script/TextEditor.java @@ -1977,12 +1977,11 @@ void setLanguage(final ScriptLanguage language, final boolean addHeader) { this.scriptInfo = null; } getEditorPane().setLanguage(language, addHeader); - prefService.put(getClass(), LAST_LANGUAGE, null == language? "none" : language.getLanguageName()); setTitle(); updateLanguageMenu(language); - updateTabAndFontSize(true); + updateUI(true); } void updateLanguageMenu(final ScriptLanguage language) { @@ -1991,6 +1990,9 @@ void updateLanguageMenu(final ScriptLanguage language) { if (!item.isSelected()) { item.setSelected(true); } + // print autocompletion status to console + if (getEditorPane().getSupportStatus() != null) + write(getEditorPane().getSupportStatus()); final boolean isRunnable = item != noneLanguageItem; final boolean isCompileable = From 44a1fd16e067fcd06e9ac0d5ba8b056735e0917b Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 24 Feb 2022 14:03:30 -0600 Subject: [PATCH 20/21] POM: bump major/minor version Lots of new things now. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 21de4917..018a5dd9 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ script-editor - 0.6.2-SNAPSHOT + 0.7.0-SNAPSHOT SciJava Script Editor Script Editor and Interpreter for SciJava script languages. From ee92ec4034bdcb132f5ea196a64ed29b0e452cc8 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 24 Feb 2022 14:05:08 -0600 Subject: [PATCH 21/21] Avoid repeat printing of same active language --- .../java/org/scijava/ui/swing/script/TextEditor.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/scijava/ui/swing/script/TextEditor.java b/src/main/java/org/scijava/ui/swing/script/TextEditor.java index 12f89470..2dd85353 100644 --- a/src/main/java/org/scijava/ui/swing/script/TextEditor.java +++ b/src/main/java/org/scijava/ui/swing/script/TextEditor.java @@ -88,6 +88,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.TreeMap; import java.util.Vector; @@ -1984,6 +1985,8 @@ void setLanguage(final ScriptLanguage language, final boolean addHeader) { updateUI(true); } + private String lastSupportStatus = null; + void updateLanguageMenu(final ScriptLanguage language) { JMenuItem item = languageMenuItems.get(language); if (item == null) item = noneLanguageItem; @@ -1991,8 +1994,11 @@ void updateLanguageMenu(final ScriptLanguage language) { item.setSelected(true); } // print autocompletion status to console - if (getEditorPane().getSupportStatus() != null) - write(getEditorPane().getSupportStatus()); + String supportStatus = getEditorPane().getSupportStatus(); + if (supportStatus != null && !Objects.equals(supportStatus, lastSupportStatus)) { + write(supportStatus); + lastSupportStatus = supportStatus; + } final boolean isRunnable = item != noneLanguageItem; final boolean isCompileable =