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. 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..71526e2f 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; @@ -54,13 +58,13 @@ 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; 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,11 +92,14 @@ 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; private boolean redoInProgress; + private boolean autoCompletionEnabled; + private boolean autoCompletionJavaFallback; + private boolean autoCompletionWithoutKey; + private String supportStatus; @Parameter Context context; @@ -111,8 +118,19 @@ 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); + setClearWhitespaceLinesEnabled(false); // most folks wont't want this set? + + // load preferences + loadPreferences(); getActionMap() .put(DefaultEditorKit.nextWordAction, wordMovement(+1, false)); @@ -147,15 +165,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 * @@ -509,18 +551,85 @@ protected void setLanguage(final ScriptLanguage language, setText(header += getText()); } + String supportLevel = "SciJava supported"; // try to get language support for current language, may be null. support = languageSupportService.getLanguageSupport(currentLanguage); - if (support != null && autoCompletionEnabled) { + // 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")); + 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; + } + + /** + * 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); } - private boolean autoCompletionEnabled = true; - public void setAutoCompletionEnabled(boolean value) { - autoCompletionEnabled = value; - setLanguage(currentLanguage); + /** + * 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); + } + + /** + * 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 isAutoCompletionKeyless() { + return autoCompletionWithoutKey; + } + + public boolean isAutoCompletionFallbackEnabled() { + return autoCompletionJavaFallback; } /** @@ -575,6 +684,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(); @@ -611,7 +726,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); } } } @@ -709,21 +825,50 @@ 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.AC"; + 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"; - 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); + setAutoCompletion(true); + setKeylessAutoCompletion(true); // true for backwards compatibility with IJ1 macro auto-completion + setFallbackAutoCompletion(false); + setMarkOccurrences(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())); + setAutoCompletion(prefService.getBoolean(getClass(), AUTOCOMPLETE_PREFS, true)); + 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)); + } } - + + public String themeName() { + return prefService.get(getClass(), THEME_PREFS, DEFAULT_THEME); + } + public String loadFolders() { return prefService.get(getClass(), FOLDERS_PREFS, System.getProperty("user.home")); } @@ -731,12 +876,18 @@ 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_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); } /** @@ -746,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/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: + *

+ * + * + * @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..4c78e679 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; @@ -252,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<>(); @@ -270,6 +274,7 @@ public FileSystemTree(final Logger log) getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); setAutoscrolls(true); setScrollsOnExpand(true); + setExpandsSelectedPaths(true); addTreeWillExpandListener(new TreeWillExpandListener() { @Override public void treeWillExpand(TreeExpansionEvent event) throws ExpandVetoException { @@ -439,9 +444,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 +455,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 +498,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/FileSystemTreePanel.java b/src/main/java/org/scijava/ui/swing/script/FileSystemTreePanel.java new file mode 100644 index 00000000..f3449649 --- /dev/null +++ b/src/main/java/org/scijava/ui/swing/script/FileSystemTreePanel.java @@ -0,0 +1,453 @@ +/* + * #%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.Dimension; +import java.awt.Font; +import java.awt.FontMetrics; +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; +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; +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.JScrollPane; +import javax.swing.JTextField; +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, including a filter toolbar. + * + * @author Albert Cardona + * @author Tiago Ferreira + */ +class FileSystemTreePanel extends JPanel { + + private static final long serialVersionUID = -710040159139542578L; + private final FileSystemTree tree; + private final SearchField searchField; + private boolean regex; + private boolean caseSensitive; + + @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(); + 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; + 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(); + } + + 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 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 = thinButton("+"); + 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 = thinButton("−"); + 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 = thinButton("⋮"); + 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("About File Explorer ..."); + 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 -> collapseAllNodes()); + popup.add(jmi); + 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); + 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 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 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 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:

" // + + "
" // + + " " // + + " " // + + " " // + + " " // + + " " // + + " " // + + " " // + + " " // + + " " // + + " " // + + " " // + + " " // + + "
PatternResult
py$Display filenames ending with py
^DemoDisplay filenames starting with Demo
"; + JOptionPane.showMessageDialog(this, msg, "File Explorer Pane", 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... "; + + 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()); + } + + @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/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 d3063746..2dd85353 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; @@ -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; @@ -82,32 +79,33 @@ 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.Enumeration; import java.util.HashMap; import java.util.HashSet; 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; 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; import javax.script.ScriptException; import javax.swing.AbstractAction; +import javax.swing.AbstractButton; import javax.swing.BorderFactory; import javax.swing.BoxLayout; 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,14 +113,15 @@ 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; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.DocumentEvent; @@ -133,6 +132,7 @@ 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; @@ -181,12 +181,15 @@ * * @author Johannes Schindelin * @author Jonathan Hale + * @author Albert Cardona + * @author Tiago Ferreira */ public class TextEditor extends JFrame implements ActionListener, ChangeListener, CloseConfirmable, DocumentListener { 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"; @@ -195,6 +198,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 { @@ -209,25 +213,28 @@ 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, - clearScreen, nextError, previousError, openHelpWithoutFrames, nextTab, - previousTab, runSelection, extractSourceJar, toggleBookmark, - listBookmarks, openSourceForClass, openSourceForMenuItem, + nextError, previousError, openHelpWithoutFrames, nextTab, + previousTab, runSelection, extractSourceJar, + openSourceForClass, + //openSourceForMenuItem, // this never had an actionListener!?? openMacroFunctions, decreaseFontSize, increaseFontSize, chooseFontSize, chooseTabSize, gitGrep, replaceTabsWithSpaces, - replaceSpacesWithTabs, toggleWhiteSpaceLabeling, zapGremlins, - savePreferences, toggleAutoCompletionMenu, openClassOrPackageHelp; + replaceSpacesWithTabs, zapGremlins,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, + autocompletion, fallbackAutocompletion, keylessAutocompletion, + markOccurences, paintTabs, whiteSpace; + private ButtonGroup themeRadioGroup; private JTextArea errorScreen = new JTextArea(); - + private final FileSystemTree tree; private final JSplitPane body; @@ -236,6 +243,9 @@ public class TextEditor extends JFrame implements ActionListener, private ErrorHandler errorHandler; private boolean respectAutoImports; + private String activeTheme; + private int[] panePositions; + @Parameter private Context context; @@ -285,6 +295,13 @@ public TextEditor(final Context context) { context.inject(this); initializeTokenMakers(); + // NB: All panes must be initialized before menus are assembled! + tabbed = new JTabbedPane(); + tree = new FileSystemTree(log); + body = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, new FileSystemTreePanel(tree, context), tabbed); + // These items are dynamic and need to be initialized before EditorPane creation + initializeDynamicMenuComponents(); + // -- BEGIN MENUS -- // Initialize menu @@ -304,14 +321,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); @@ -329,62 +347,31 @@ 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(); - find = addToMenu(edit, "Find...", KeyEvent.VK_F, ctrl); + addSeparator(edit, "Find:"); + 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); - 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.setMnemonic(KeyEvent.VK_O); - edit.addSeparator(); - - 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); + addSeparator(edit, "Goto:"); + gotoLine = addToMenu(edit, "Goto Line...", KeyEvent.VK_G, ctrl); + gotoLine.setMnemonic(KeyEvent.VK_G); - whiteSpaceMenu = new JMenu("Whitespace"); - whiteSpaceMenu.setMnemonic(KeyEvent.VK_W); - removeTrailingWhitespace = - addToMenu(whiteSpaceMenu, "Remove trailing whitespace", 0, 0); + final JMenuItem toggleBookmark = addToMenu(edit, "Toggle Bookmark", KeyEvent.VK_B, ctrl); + toggleBookmark.setMnemonic(KeyEvent.VK_B); + 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); 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); @@ -455,12 +442,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(); @@ -471,7 +458,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); @@ -481,25 +468,29 @@ public TextEditor(final Context context) { toolsMenu = new JMenu("Tools"); toolsMenu.setMnemonic(KeyEvent.VK_O); - 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); + 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:"); + 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 -- @@ -518,10 +509,17 @@ 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 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); previousTab = @@ -538,14 +536,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(); @@ -571,8 +570,12 @@ public TextEditor(final Context context) { fontSizeMenu.add(chooseFontSize); options.add(fontSizeMenu); - // Add tab size adjusting menu - tabSizeMenu = new JMenu("Tab sizes"); + addSeparator(options, "Indentation:"); + tabsEmulated = new JCheckBoxMenuItem("Indent Using Spaces"); + tabsEmulated.setMnemonic(KeyEvent.VK_S); + tabsEmulated.addItemListener(e -> 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 }) { @@ -591,51 +594,62 @@ 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"); - wrapLines.addChangeListener(e -> getEditorPane().setLineWrap(wrapLines.getState())); + addSeparator(options, "View:"); + options.add(whiteSpace); + options.add(paintTabs); + options.add(markOccurences); 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"); - toggleAutoCompletionMenu.setSelected(prefService.getBoolean(TextEditor.class, "autoComplete", true)); - toggleAutoCompletionMenu.addChangeListener(e -> toggleAutoCompletion()); - options.add(toggleAutoCompletionMenu); + addSeparator(options, "Code Completions:"); + options.add(autocompletion); + options.add(keylessAutocompletion); + options.add(fallbackAutocompletion); options.addSeparator(); - savePreferences = addToMenu(options, "Save Preferences", 0, 0); - + appendPreferences(options); mbar.add(options); + mbar.add(helpMenu()); // -- 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)); - - 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("filter..."); - filter.setForeground(Color.gray); - filter.setToolTipText("Use leading '/' for regular expressions"); - - tree = new FileSystemTree(log); - tree.ignoreExtension("class"); + // Tweaks for JSplitPane + // 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(); + }); + + // Tweaks for FileSystemTree + tree.addTopLevelFoldersFrom(getEditorPane().loadFolders()); // Restore top-level directories 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(); @@ -658,7 +672,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); } @@ -671,123 +685,44 @@ public TextEditor(final Context context) { open(f); } }); - add_directory.addActionListener(e -> { - final JFileChooser c = new JFileChooser("Choose a directory"); - c.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); - c.setFileHidingEnabled(true); // hide hidden files - if (JFileChooser.APPROVE_OPTION == c.showOpenDialog(getContentPane())) { - final File f = c.getSelectedFile(); - if (f.isDirectory()) tree.addRootDirectory(f.getAbsolutePath(), false); - } - }); - remove_directory.addActionListener(e -> { - final TreePath p = tree.getSelectionPath(); - if (null == p) { - JOptionPane.showMessageDialog(TextEditor.this, - "Select a top-level folder first."); - 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."); - } - }); - filter.addFocusListener(new FocusListener() { - @Override - public void focusLost(FocusEvent e) { - if (0 == filter.getText().length()) { - filter.setForeground(Color.gray); - filter.setText("filter..."); - } - } - - @Override - public void focusGained(FocusEvent e) { - if (filter.getForeground() == Color.gray) { - filter.setText(""); - filter.setForeground(Color.black); - } - } - }); - 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(Color.black); - } catch (final PatternSyntaxException pse) { - 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().indexOf(text)); - } - } else { - // Upon re-typing something - if (filter.getForeground() == Color.red) { - filter.setForeground(Color.black); - } + + // 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; } - } - }); - - // 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); - scrolltree.setBackground(Color.white); - scrolltree.setBorder(BorderFactory.createTitledBorder(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(); + }); + 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); @@ -796,7 +731,6 @@ public void keyPressed(final KeyEvent ke) { 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 @@ -823,8 +757,8 @@ public void windowGainedFocus(final WindowEvent e) { } }); - final Font font = new Font("Courier", Font.PLAIN, 12); - errorScreen.setFont(font); + // Tweaks for Console + errorScreen.setFont(getEditorPane().getFont()); errorScreen.setEditable(false); errorScreen.setLineWrap(true); @@ -833,8 +767,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(); }); @@ -865,21 +799,31 @@ 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()); + // 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().getDividerLocation()}; 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(); dragSource.startDrag(dge, DragSource.DefaultCopyDrop, new Transferable() { @Override @@ -893,7 +837,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; @@ -902,12 +846,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) { @@ -918,7 +862,7 @@ public void dragOver(DragSourceDragEvent dsde) { } @Override - public void dropActionChanged(DragSourceDragEvent dsde) {} + public void dropActionChanged(final DragSourceDragEvent dsde) {} } public LogService log() { return log; } @@ -947,6 +891,48 @@ 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.addItemListener(e -> setWrapLines(wrapLines.getState())); + markOccurences = new JCheckBoxMenuItem("Mark Occurences", false); + markOccurences.setToolTipText("Highlights all occurrences of a selected element"); + 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.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"); + 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.addItemListener(e -> setFallbackAutoCompletion(fallbackAutocompletion.getState())); + themeRadioGroup = new ButtonGroup(); + + // 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. @@ -1005,9 +991,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; } @@ -1032,6 +1019,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() { @@ -1362,13 +1350,10 @@ 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(); else if (source == selectAll) { getTextArea().setCaretPosition(0); getTextArea().moveCaretPosition(getTextArea().getDocument().getLength()); @@ -1380,7 +1365,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(); @@ -1392,28 +1378,11 @@ 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 == savePreferences) { - getEditorPane().savePreferences(tree.getTopLevelFoldersString()); - } - 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(); + final String className = getSelectedClassNameOrAsk("Class (Fully qualified name):", "Which Class?"); if (className != null) { try { final String url = new FileFunctions(this).getSourceURL(className); @@ -1460,12 +1429,134 @@ else if (source == increaseFontSize || source == decreaseFontSize) { else if (handleTabsMenu(source)) return; } - private void toggleAutoCompletion() { - for (int i = 0; i < tabbed.getTabCount(); i++) { - final EditorPane editorPane = getEditorPane(i); - editorPane.setAutoCompletionEnabled(toggleAutoCompletionMenu.isSelected()); + private void setAutoCompletionEnabled(final boolean enabled) { + 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++) + getEditorPane(i).setKeylessAutoCompletion(noKeyRequired); + } + + private void setFallbackAutoCompletion(final boolean fallback) { + 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++) + getEditorPane(i).setMarkOccurrences(markOccurrences); + } + + private void setWhiteSpaceVisible(final boolean visible) { + 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++) + getEditorPane(i).setLineWrap(wrap); + } + + private JMenu applyThemeMenu() { + final LinkedHashMap map = new LinkedHashMap<>(); + map.put("Default", "default"); + 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("Visual Studio (Light)", "vs"); + themeRadioGroup = new ButtonGroup(); + final JMenu menu = new JMenu("Theme"); + map.forEach((k, v) -> { + if ("-".equals(k)) { + menu.addSeparator(); + return; + } + final JRadioButtonMenuItem item = new JRadioButtonMenuItem(k); + item.setActionCommand(v); + themeRadioGroup.add(item); + item.addActionListener(e -> { + try { + applyTheme(v, false); + } catch (final 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 { + 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")); + for (int i = 0; i < tabbed.getTabCount(); 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); + ep.updateBookmarkIcon(); // update bookmark icon color + } + } catch (final Exception ex) { + throw new IllegalArgumentException(ex); + } + this.activeTheme = theme; + if (updateUI && themeRadioGroup != null) { + final Enumeration choices = themeRadioGroup.getElements(); + while (choices.hasMoreElements()) { + final AbstractButton choice = choices.nextElement(); + if (theme.equals(choice.getActionCommand())) { + choice.setSelected(true); + break; + } + } + } + } + + private void collapseSplitPane(final int pane, final boolean collapse) { + final JSplitPane jsp = (pane == 0) ? body : getTab(); + if (collapse) { + panePositions[pane] = jsp.getDividerLocation(); + if (pane == 0) { // collapse to left + jsp.setDividerLocation(0.0d); + } else { // collapse to bottom + jsp.setDividerLocation(1.0d); + } + } else { + jsp.setDividerLocation(panePositions[pane]); } - prefService.put(TextEditor.class, "autoComplete", toggleAutoCompletionMenu.isSelected()); } protected boolean handleTabsMenu(final Object source) { @@ -1491,7 +1582,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()); @@ -1516,7 +1607,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 { @@ -1538,16 +1629,34 @@ 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.\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); + } + } - final BookmarkDialog dialog = new BookmarkDialog(this, bookmarks); - dialog.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() { @@ -1637,7 +1746,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; @@ -1645,7 +1754,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) { @@ -1869,58 +1978,73 @@ 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); } + private String lastSupportStatus = null; + void updateLanguageMenu(final ScriptLanguage language) { JMenuItem item = languageMenuItems.get(language); if (item == null) item = noneLanguageItem; if (!item.isSelected()) { item.setSelected(true); } + // print autocompletion status to console + String supportStatus = getEditorPane().getSupportStatus(); + if (supportStatus != null && !Objects.equals(supportStatus, lastSupportStatus)) { + write(supportStatus); + lastSupportStatus = supportStatus; + } final boolean isRunnable = item != noneLanguageItem; 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); - 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; @@ -1965,8 +2089,13 @@ 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()); + whiteSpace.setState(pane.isWhitespaceVisible()); + autocompletion.setState(pane.isAutoCompletionEnabled()); + keylessAutocompletion.setState(pane.isAutoCompletionKeyless()); } public void setEditorPaneFileName(final String baseName) { @@ -2212,8 +2341,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)); @@ -2335,7 +2473,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(); @@ -2343,7 +2481,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); } } @@ -2369,11 +2507,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()); @@ -2389,7 +2527,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); } } @@ -2443,19 +2581,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; } @@ -2466,6 +2604,7 @@ private static void append(final JTextArea textArea, final String text) { textArea.setCaretPosition(length); } + public void markCompileStart() { markCompileStart(true); } @@ -2617,7 +2756,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); @@ -2681,7 +2820,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 } @@ -2701,14 +2840,18 @@ 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(); 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; @@ -2732,13 +2875,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(); } }); } @@ -2746,13 +2887,11 @@ public void actionPerformed(final ActionEvent event) { } 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); }); } } @@ -2807,7 +2946,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) { @@ -2911,7 +3050,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); } } @@ -2944,7 +3083,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(); @@ -2979,7 +3118,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 @@ -3121,4 +3260,105 @@ public void setFontSize(final float size) { private void changeFontSize(final JTextArea a, final float size) { a.setFont(a.getFont().deriveFont(size)); } + + private void appendPreferences(final JMenu menu) { + JMenuItem item = new JMenuItem("Save Preferences"); + menu.add(item); + item.addActionListener(e -> { + getEditorPane().savePreferences(tree.getTopLevelFoldersString(), activeTheme); + write("Script Editor: Preferences 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); + prefService.clear(TextEditor.class); + write("Script Editor: Preferences Reset.\n"); + } + }); + } + + private JMenu helpMenu() { + final JMenu menu = new JMenu("Help"); + addSeparator(menu, "Contextual Help:"); + menu.add(openHelpWithoutFrames); + openHelpWithoutFrames.setMnemonic(KeyEvent.VK_O); + menu.add(openHelp); + openClassOrPackageHelp = addToMenu(menu, "Lookup Class or Package...", 0, 0); + openClassOrPackageHelp.setMnemonic(KeyEvent.VK_S); + 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/")); + //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.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 -> 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); + label.setEnabled(false); + label.setForeground(getDisabledComponentColor()); + if (menu.getMenuComponentCount() > 1) { + menu.addSeparator(); + } + 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"); + } catch (final Exception ignored) { + 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; + } + } 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..2108a5b0 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; @@ -81,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; @@ -89,21 +88,25 @@ 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(); 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; } - Transferable t = e.getTransferable(); + e.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE); // fix for InvalidDnDOperationException: No drop current + final Transferable t = e.getTransferable(); if (!t.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) return; try { final Object o = t.getTransferData(DataFlavor.javaFileListFlavor); @@ -111,7 +114,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; @@ -119,29 +122,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()); @@ -155,40 +159,23 @@ 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 = new JCheckBox("Persistent"); incremental.setEnabled(true); incremental.setSelected(false); bottom.add(incremental, bc); @@ -219,7 +206,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 { @@ -227,7 +214,7 @@ public void actionPerformed(ActionEvent e) { } // Keep prompt collapsed if not in use if (!incremental.isSelected()) { - SwingUtilities.invokeLater(() -> screenAndPromptSplit.setDividerLocation(1.0)); + setREPLVisible(false); } } }); @@ -240,10 +227,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); @@ -277,22 +260,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\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); }); prompt_panel.add(prompt_help, bc); @@ -323,7 +303,6 @@ public void actionPerformed(ActionEvent a) { super.setLeftComponent(editorPane.wrappedInScrollbars()); super.setRightComponent(screenAndPromptSplit); - screenAndPromptSplit.setDividerLocation(600); screenAndPromptSplit.setDividerLocation(1.0); // Persist Script Editor layout whenever split pane divider is adjusted. @@ -338,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); 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() {