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:
+ *
+ *
+ * - September 2007, Nathan Blomquist -- Linux (KDE/Gnome) support added.
+ * - December 2010, Joshua Gerth
+ * - June 2019, TF, Adjust defaultBorderColor. Code cleanup. Added the ability
+ * to abort drop operation using Esc
+ *
+ *
+ * @author Robert Harder
+ * @version 1.1.1
+ */
+public class FileDrop {
+ private transient javax.swing.border.Border normalBorder;
+ private transient java.awt.dnd.DropTargetListener dropListener;
+
+ /** Discover if the running JVM is modern enough to have drag and drop. */
+ private static Boolean supportsDnD;
+
+ // Default border color
+ private static java.awt.Color defaultBorderColor = UIManager.getColor("Tree.selectionBackground");
+ static {
+ if (defaultBorderColor == null) defaultBorderColor = new java.awt.Color(0f,0f, 1f, 0.25f);
+ }
+
+ /**
+ * Constructs a {@link FileDrop} with a default light-blue border and, if
+ * c is a {@link java.awt.Container}, recursively sets all
+ * elements contained within as drop targets, though only the top level
+ * container will change borders.
+ *
+ * @param c
+ * Component on which files will be dropped.
+ * @param listener
+ * Listens for filesDropped.
+ * @since 1.0
+ */
+ public FileDrop(final java.awt.Component c, final Listener listener) {
+ this(null, // Logging stream
+ c, // Drop target
+ javax.swing.BorderFactory.createMatteBorder(2, 2, 2, 2,
+ defaultBorderColor), // Drag border
+ true, // Recursive
+ listener);
+ } // end constructor
+
+ /**
+ * Constructor with a default border and the option to recursively set drop
+ * targets. If your component is a java.awt.Container, then each of
+ * its children components will also listen for drops, though only the
+ * parent will change borders.
+ *
+ * @param c
+ * Component on which files will be dropped.
+ * @param recursive
+ * Recursively set children as drop targets.
+ * @param listener
+ * Listens for filesDropped.
+ * @since 1.0
+ */
+ protected FileDrop(final java.awt.Component c, final boolean recursive,
+ final Listener listener) {
+ this(null, // Logging stream
+ c, // Drop target
+ javax.swing.BorderFactory.createMatteBorder(2, 2, 2, 2,
+ defaultBorderColor), // Drag border
+ recursive, // Recursive
+ listener);
+ } // end constructor
+
+ /**
+ * Constructor with a default border and debugging optionally turned on.
+ * With Debugging turned on, more status messages will be displayed to
+ * out. A common way to use this constructor is with
+ * System.out or System.err. A null value for the
+ * parameter out will result in no debugging output.
+ *
+ * @param out
+ * PrintStream to record debugging info or null for no debugging.
+ * @param c
+ * Component on which files will be dropped.
+ * @param listener
+ * Listens for filesDropped.
+ * @since 1.0
+ */
+ protected FileDrop(final java.io.PrintStream out, final java.awt.Component c,
+ final Listener listener) {
+ this(out, // Logging stream
+ c, // Drop target
+ javax.swing.BorderFactory.createMatteBorder(2, 2, 2, 2,
+ defaultBorderColor), false, // Recursive
+ listener);
+ } // end constructor
+
+ /**
+ * Constructor with a default border, debugging optionally turned on and the
+ * option to recursively set drop targets. If your component is a
+ * java.awt.Container, then each of its children components will
+ * also listen for drops, though only the parent will change borders. With
+ * Debugging turned on, more status messages will be displayed to
+ * out. A common way to use this constructor is with
+ * System.out or System.err. A null value for the
+ * parameter out will result in no debugging output.
+ *
+ * @param out
+ * PrintStream to record debugging info or null for no debugging.
+ * @param c
+ * Component on which files will be dropped.
+ * @param recursive
+ * Recursively set children as drop targets.
+ * @param listener
+ * Listens for filesDropped.
+ * @since 1.0
+ */
+ protected FileDrop(final java.io.PrintStream out, final java.awt.Component c,
+ final boolean recursive, final Listener listener) {
+ this(out, // Logging stream
+ c, // Drop target
+ javax.swing.BorderFactory.createMatteBorder(2, 2, 2, 2,
+ defaultBorderColor), // Drag border
+ recursive, // Recursive
+ listener);
+ } // end constructor
+
+ /**
+ * Constructor with a specified border
+ *
+ * @param c
+ * Component on which files will be dropped.
+ * @param dragBorder
+ * Border to use on JComponent when dragging occurs.
+ * @param listener
+ * Listens for filesDropped.
+ * @since 1.0
+ */
+ protected FileDrop(final java.awt.Component c,
+ final javax.swing.border.Border dragBorder, final Listener listener) {
+ this(null, // Logging stream
+ c, // Drop target
+ dragBorder, // Drag border
+ false, // Recursive
+ listener);
+ } // end constructor
+
+ /**
+ * Constructor with a specified border and the option to recursively set
+ * drop targets. If your component is a java.awt.Container, then
+ * each of its children components will also listen for drops, though only
+ * the parent will change borders.
+ *
+ * @param c
+ * Component on which files will be dropped.
+ * @param dragBorder
+ * Border to use on JComponent when dragging occurs.
+ * @param recursive
+ * Recursively set children as drop targets.
+ * @param listener
+ * Listens for filesDropped.
+ * @since 1.0
+ */
+ protected FileDrop(final java.awt.Component c,
+ final javax.swing.border.Border dragBorder,
+ final boolean recursive, final Listener listener) {
+ this(null, c, dragBorder, recursive, listener);
+ } // end constructor
+
+ /**
+ * Constructor with a specified border and debugging optionally turned on.
+ * With Debugging turned on, more status messages will be displayed to
+ * out. A common way to use this constructor is with
+ * System.out or System.err. A null value for the
+ * parameter out will result in no debugging output.
+ *
+ * @param out
+ * PrintStream to record debugging info or null for no debugging.
+ * @param c
+ * Component on which files will be dropped.
+ * @param dragBorder
+ * Border to use on JComponent when dragging occurs.
+ * @param listener
+ * Listens for filesDropped.
+ * @since 1.0
+ */
+ protected FileDrop(final java.io.PrintStream out, final java.awt.Component c,
+ final javax.swing.border.Border dragBorder, final Listener listener) {
+ this(out, // Logging stream
+ c, // Drop target
+ dragBorder, // Drag border
+ false, // Recursive
+ listener);
+ } // end constructor
+
+ /**
+ * Full constructor with a specified border and debugging optionally turned
+ * on. With Debugging turned on, more status messages will be displayed to
+ * out. A common way to use this constructor is with
+ * System.out or System.err. A null value for the
+ * parameter out will result in no debugging output.
+ *
+ * @param out
+ * PrintStream to record debugging info or null for no debugging.
+ * @param c
+ * Component on which files will be dropped.
+ * @param dragBorder
+ * Border to use on JComponent when dragging occurs.
+ * @param recursive
+ * Recursively set children as drop targets.
+ * @param listener
+ * Listens for filesDropped.
+ * @since 1.0
+ */
+ protected FileDrop(final java.io.PrintStream out, final java.awt.Component c,
+ final javax.swing.border.Border dragBorder,
+ final boolean recursive, final Listener listener) {
+
+ if (supportsDnD()) { // Make a drop listener
+ dropListener = new java.awt.dnd.DropTargetListener() {
+ @Override
+ public void dragEnter(final java.awt.dnd.DropTargetDragEvent evt) {
+ log(out, "FileDrop: dragEnter event.");
+
+ // Is this an acceptable drag event?
+ if (isDragOk(out, evt) && c.isEnabled()) {
+ // If it's a Swing component, set its border
+ if (c instanceof javax.swing.JComponent) {
+ final javax.swing.JComponent jc = (javax.swing.JComponent) c;
+ if (normalBorder == null) {
+ normalBorder = jc.getBorder();
+ } // end if: border not yet saved
+ log(out, "FileDrop: normal border saved.");
+ jc.setBorder(dragBorder);
+ log(out, "FileDrop: drag border set.");
+ } // end if: JComponent
+
+ // Acknowledge that it's okay to enter
+ // evt.acceptDrag(
+ // java.awt.dnd.DnDConstants.ACTION_COPY_OR_MOVE );
+ evt.acceptDrag(java.awt.dnd.DnDConstants.ACTION_COPY);
+ log(out, "FileDrop: event accepted.");
+ } // end if: drag ok
+ else { // Reject the drag event
+ evt.rejectDrag();
+ log(out, "FileDrop: event rejected.");
+ } // end else: drag not ok
+ } // end dragEnter
+
+ @Override
+ public void dragOver(final java.awt.dnd.DropTargetDragEvent evt) { // This
+ // is
+ // called
+ // continually
+ // as
+ // long
+ // as
+ // the
+ // mouse
+ // is
+ // over
+ // the
+ // drag
+ // target.
+ } // end dragOver
+
+ @Override
+ public void drop(final java.awt.dnd.DropTargetDropEvent evt) {
+ log(out, "FileDrop: drop event.");
+ try { // Get whatever was dropped
+ final java.awt.datatransfer.Transferable tr = evt
+ .getTransferable();
+
+ // Is it a file list?
+ if (tr.isDataFlavorSupported(java.awt.datatransfer.DataFlavor.javaFileListFlavor)) {
+ // Say we'll take it.
+ // evt.acceptDrop (
+ // java.awt.dnd.DnDConstants.ACTION_COPY_OR_MOVE );
+ evt.acceptDrop(java.awt.dnd.DnDConstants.ACTION_COPY);
+ log(out, "FileDrop: file list accepted.");
+
+ // Get a useful list
+ final java.util.List> fileList = (java.util.List>) tr
+ .getTransferData(java.awt.datatransfer.DataFlavor.javaFileListFlavor);
+ //final java.util.Iterator> iterator = fileList.iterator();
+
+ // Convert list to array
+ final java.io.File[] filesTemp = new java.io.File[fileList
+ .size()];
+ fileList.toArray(filesTemp);
+ final java.io.File[] files = filesTemp;
+
+ // Alert listener to drop.
+ if (listener != null)
+ listener.filesDropped(files);
+
+ // Mark that drop is completed.
+ evt.getDropTargetContext().dropComplete(true);
+ log(out, "FileDrop: drop complete.");
+ } // end if: file list
+ else // this section will check for a reader flavor.
+ {
+ // Thanks, Nathan!
+ // BEGIN 2007-09-12 Nathan Blomquist -- Linux
+ // (KDE/Gnome) support added.
+ final DataFlavor[] flavors = tr.getTransferDataFlavors();
+ boolean handled = false;
+ for (int zz = 0; zz < flavors.length; zz++) {
+ if (flavors[zz].isRepresentationClassReader()) {
+ // Say we'll take it.
+ // evt.acceptDrop (
+ // java.awt.dnd.DnDConstants.ACTION_COPY_OR_MOVE
+ // );
+ evt.acceptDrop(java.awt.dnd.DnDConstants.ACTION_COPY);
+ log(out, "FileDrop: reader accepted.");
+
+ final Reader reader = flavors[zz]
+ .getReaderForText(tr);
+
+ final BufferedReader br = new BufferedReader(
+ reader);
+
+ if (listener != null)
+ listener.filesDropped(createFileArray(
+ br, out));
+
+ // Mark that drop is completed.
+ evt.getDropTargetContext().dropComplete(
+ true);
+ log(out, "FileDrop: drop complete.");
+ handled = true;
+ break;
+ }
+ }
+ if (!handled) {
+ log(out,
+ "FileDrop: not a file list or reader - abort.");
+ evt.rejectDrop();
+ }
+ // END 2007-09-12 Nathan Blomquist -- Linux
+ // (KDE/Gnome) support added.
+ } // end else: not a file list
+ } // end try
+ catch (final java.io.IOException io) {
+ log(out, "FileDrop: IOException - abort:");
+ io.printStackTrace(out);
+ evt.rejectDrop();
+ } // end catch IOException
+ catch (final java.awt.datatransfer.UnsupportedFlavorException ufe) {
+ log(out,
+ "FileDrop: UnsupportedFlavorException - abort:");
+ ufe.printStackTrace(out);
+ evt.rejectDrop();
+ } // end catch: UnsupportedFlavorException
+ finally {
+ // If it's a Swing component, reset its border
+ if (c instanceof javax.swing.JComponent) {
+ final javax.swing.JComponent jc = (javax.swing.JComponent) c;
+ jc.setBorder(normalBorder);
+ log(out, "FileDrop: normal border restored.");
+ } // end if: JComponent
+ } // end finally
+ } // end drop
+
+ @Override
+ public void dragExit(final java.awt.dnd.DropTargetEvent evt) {
+ log(out, "FileDrop: dragExit event.");
+ // If it's a Swing component, reset its border
+ if (c instanceof javax.swing.JComponent) {
+ final javax.swing.JComponent jc = (javax.swing.JComponent) c;
+ jc.setBorder(normalBorder);
+ log(out, "FileDrop: normal border restored.");
+ } // end if: JComponent
+ } // end dragExit
+
+ @Override
+ public void dropActionChanged(
+ final java.awt.dnd.DropTargetDragEvent evt) {
+ log(out, "FileDrop: dropActionChanged event.");
+ // Is this an acceptable drag event?
+ if (isDragOk(out, evt)) { // evt.acceptDrag(
+ // java.awt.dnd.DnDConstants.ACTION_COPY_OR_MOVE
+ // );
+ evt.acceptDrag(java.awt.dnd.DnDConstants.ACTION_COPY);
+ log(out, "FileDrop: event accepted.");
+ } // end if: drag ok
+ else {
+ evt.rejectDrag();
+ log(out, "FileDrop: event rejected.");
+ } // end else: drag not ok
+ } // end dropActionChanged
+ }; // end DropTargetListener
+
+ // Make the component (and possibly children) drop targets
+ makeDropTarget(out, c, recursive);
+ } // end if: supports dnd
+ else {
+ log(out, "FileDrop: Drag and drop is not supported with this JVM");
+ } // end else: does not support DnD
+ } // end constructor
+
+ private static boolean supportsDnD() { // Static Boolean
+ if (supportsDnD == null) {
+ boolean support = false;
+ try {
+ Class.forName("java.awt.dnd.DnDConstants");
+ support = true;
+ } // end try
+ catch (final Exception e) {
+ support = false;
+ } // end catch
+ supportsDnD = Boolean.valueOf(support);
+ } // end if: first time through
+ return supportsDnD.booleanValue();
+ } // end supportsDnD
+
+ // BEGIN 2007-09-12 Nathan Blomquist -- Linux (KDE/Gnome) support added.
+ private static String ZERO_CHAR_STRING = "" + (char) 0;
+
+ private static File[] createFileArray(final BufferedReader bReader,
+ final PrintStream out) {
+ try {
+ final java.util.List list = new java.util.ArrayList();
+ java.lang.String line = null;
+ while ((line = bReader.readLine()) != null) {
+ try {
+ // kde seems to append a 0 char to the end of the reader
+ if (ZERO_CHAR_STRING.equals(line))
+ continue;
+
+ final java.io.File file = new java.io.File(new java.net.URI(line));
+ list.add(file);
+ } catch (final Exception ex) {
+ log(out, "Error with " + line + ": " + ex.getMessage());
+ }
+ }
+
+ return list.toArray(new File[list.size()]);
+ } catch (final IOException ex) {
+ log(out, "FileDrop: IOException");
+ }
+ return new File[0];
+ }
+
+ // END 2007-09-12 Nathan Blomquist -- Linux (KDE/Gnome) support added.
+
+ private void makeDropTarget(final java.io.PrintStream out,
+ final java.awt.Component c, final boolean recursive) {
+ // Make drop target
+ final java.awt.dnd.DropTarget dt = new java.awt.dnd.DropTarget();
+ try {
+ dt.addDropTargetListener(dropListener);
+ } // end try
+ catch (final java.util.TooManyListenersException e) {
+ e.printStackTrace();
+ log(out,
+ "FileDrop: Drop will not work due to previous error. Do you have another listener attached?");
+ } // end catch
+
+ // Listen for hierarchy changes and remove the drop target when the
+ // parent gets cleared out.
+ c.addHierarchyListener(new java.awt.event.HierarchyListener() {
+ @Override
+ public void hierarchyChanged(final java.awt.event.HierarchyEvent evt) {
+ log(out, "FileDrop: Hierarchy changed.");
+ final java.awt.Component parent = c.getParent();
+ if (parent == null) {
+ c.setDropTarget(null);
+ log(out, "FileDrop: Drop target cleared from component.");
+ } // end if: null parent
+ else {
+ new java.awt.dnd.DropTarget(c, dropListener);
+ log(out, "FileDrop: Drop target added to component.");
+ } // end else: parent not null
+ } // end hierarchyChanged
+ }); // end hierarchy listener
+ if (c.getParent() != null)
+ new java.awt.dnd.DropTarget(c, dropListener);
+
+ if (recursive && (c instanceof java.awt.Container)) {
+ // Get the container
+ final java.awt.Container cont = (java.awt.Container) c;
+
+ // Get it's components
+ final java.awt.Component[] comps = cont.getComponents();
+
+ // Set it's components as listeners also
+ for (int i = 0; i < comps.length; i++)
+ makeDropTarget(out, comps[i], recursive);
+ } // end if: recursively set components as listener
+ } // end dropListener
+
+ /** Determine if the dragged data is a file list. */
+ private boolean isDragOk(final java.io.PrintStream out,
+ final java.awt.dnd.DropTargetDragEvent evt) {
+ boolean ok = false;
+
+ // Get data flavors being dragged
+ final java.awt.datatransfer.DataFlavor[] flavors = evt
+ .getCurrentDataFlavors();
+
+ // See if any of the flavors are a file list
+ int i = 0;
+ while (!ok && i < flavors.length) {
+ // BEGIN 2007-09-12 Nathan Blomquist -- Linux (KDE/Gnome) support
+ // added.
+ // Is the flavor a file list?
+ final DataFlavor curFlavor = flavors[i];
+ if (curFlavor
+ .equals(java.awt.datatransfer.DataFlavor.javaFileListFlavor)
+ || curFlavor.isRepresentationClassReader()) {
+ ok = true;
+ }
+ // END 2007-09-12 Nathan Blomquist -- Linux (KDE/Gnome) support
+ // added.
+ i++;
+ } // end while: through flavors
+
+ // If logging is enabled, show data flavors
+ if (out != null) {
+ if (flavors.length == 0)
+ log(out, "FileDrop: no data flavors.");
+ for (i = 0; i < flavors.length; i++)
+ log(out, flavors[i].toString());
+ } // end if: logging enabled
+
+ return ok;
+ } // end isDragOk
+
+ /** Outputs message to out if it's not null. */
+ private static void log(final java.io.PrintStream out, final String message) { // Log
+ // message
+ // if
+ // requested
+ if (out != null)
+ out.println(message);
+ } // end log
+
+ /**
+ * Removes the drag-and-drop hooks from the component and optionally from
+ * the all children. You should call this if you add and remove components
+ * after you've set up the drag-and-drop. This will recursively unregister
+ * all components contained within c if c is a
+ * {@link java.awt.Container}.
+ *
+ * @param c
+ * The component to unregister as a drop target
+ * @since 1.0
+ */
+ public static boolean remove(final java.awt.Component c) {
+ return remove(null, c, true);
+ } // end remove
+
+ /**
+ * Removes the drag-and-drop hooks from the component and optionally from
+ * the all children. You should call this if you add and remove components
+ * after you've set up the drag-and-drop.
+ *
+ * @param out
+ * Optional {@link java.io.PrintStream} for logging drag and drop
+ * messages
+ * @param c
+ * The component to unregister
+ * @param recursive
+ * Recursively unregister components within a container
+ * @since 1.0
+ */
+ protected static boolean remove(final java.io.PrintStream out, final java.awt.Component c,
+ final boolean recursive) { // Make sure we support dnd.
+ if (supportsDnD()) {
+ log(out, "FileDrop: Removing drag-and-drop hooks.");
+ c.setDropTarget(null);
+ if (recursive && (c instanceof java.awt.Container)) {
+ final java.awt.Component[] comps = ((java.awt.Container) c)
+ .getComponents();
+ for (int i = 0; i < comps.length; i++)
+ remove(out, comps[i], recursive);
+ return true;
+ } // end if: recursive
+ else
+ return false;
+ } // end if: supports DnD
+ else
+ return false;
+ } // end remove
+
+ /* ******** I N N E R I N T E R F A C E L I S T E N E R ******** */
+
+ /**
+ * Implement this inner interface to listen for when files are dropped. For
+ * example your class declaration may begin like this:
+ *
+ * public class MyClass implements FileDrop.Listener
+ * ...
+ * public void filesDropped( java.io.File[] files )
+ * {
+ * ...
+ * } // end filesDropped
+ * ...
+ *
+ *
+ * @since 1.1
+ */
+ public static interface Listener {
+
+ /**
+ * This method is called when files have been successfully dropped.
+ *
+ * @param files
+ * An array of Files that were dropped.
+ * @since 1.0
+ */
+ public abstract void filesDropped(java.io.File[] files);
+
+ } // end inner-interface Listener
+
+ /* ******** I N N E R C L A S S ******** */
+
+ /**
+ * This is the event that is passed to the
+ * {@link FileDrop.Listener#filesDropped filesDropped(...)} method in your
+ * {@link FileDrop.Listener} when files are dropped onto a registered drop
+ * target.
+ *
+ *
+ * I'm releasing this code into the Public Domain. Enjoy.
+ *
+ *
+ * @author Robert Harder
+ * @author rob@iharder.net
+ * @version 1.2
+ */
+ @SuppressWarnings("serial")
+ static class Event extends java.util.EventObject {
+
+ private final java.io.File[] files;
+
+ /**
+ * Constructs an {@link Event} with the array of files that were dropped
+ * and the {@link FileDrop} that initiated the event.
+ *
+ * @param files
+ * The array of files that were dropped
+ * @param source
+ * The event source
+ * @since 1.1
+ */
+ public Event(final java.io.File[] files, final Object source) {
+ super(source);
+ this.files = files;
+ } // end constructor
+
+ /**
+ * Returns an array of files that were dropped on a registered drop
+ * target.
+ *
+ * @return array of files that were dropped
+ * @since 1.1
+ */
+ public java.io.File[] getFiles() {
+ return files;
+ } // end getFiles
+
+ } // end inner class Event
+
+ /* ******** I N N E R C L A S S ******** */
+
+ /**
+ * At last an easy way to encapsulate your custom objects for dragging and
+ * dropping in your Java programs! When you need to create a
+ * {@link java.awt.datatransfer.Transferable} object, use this class to wrap
+ * your object. For example:
+ *
+ *
+ * ...
+ * MyCoolClass myObj = new MyCoolClass();
+ * Transferable xfer = new TransferableObject( myObj );
+ * ...
+ *
+ *
+ * Or if you need to know when the data was actually dropped, like when
+ * you're moving data out of a list, say, you can use the
+ * {@link TransferableObject.Fetcher} inner class to return your object Just
+ * in Time. For example:
+ *
+ *
+ * ...
+ * final MyCoolClass myObj = new MyCoolClass();
+ *
+ * TransferableObject.Fetcher fetcher = new TransferableObject.Fetcher()
+ * { public Object getObject(){ return myObj; }
+ * }; // end fetcher
+ *
+ * Transferable xfer = new TransferableObject( fetcher );
+ * ...
+ *
+ *
+ * The {@link java.awt.datatransfer.DataFlavor} associated with
+ * {@link TransferableObject} has the representation class
+ * net.iharder.dnd.TransferableObject.class and MIME type
+ * application/x-net.iharder.dnd.TransferableObject. This data
+ * flavor is accessible via the static {@link #DATA_FLAVOR} property.
+ *
+ *
+ *
+ * I'm releasing this code into the Public Domain. Enjoy.
+ *
+ *
+ * @author Robert Harder
+ * @author rob@iharder.net
+ * @version 1.2
+ */
+ static class TransferableObject implements
+ java.awt.datatransfer.Transferable {
+ /**
+ * The MIME type for {@link #DATA_FLAVOR} is
+ * application/x-net.iharder.dnd.TransferableObject.
+ *
+ * @since 1.1
+ */
+ public final static String MIME_TYPE = "application/x-net.iharder.dnd.TransferableObject";
+
+ /**
+ * The default {@link java.awt.datatransfer.DataFlavor} for
+ * {@link TransferableObject} has the representation class
+ * net.iharder.dnd.TransferableObject.class and the MIME type
+ * application/x-net.iharder.dnd.TransferableObject.
+ *
+ * @since 1.1
+ */
+ public final static java.awt.datatransfer.DataFlavor DATA_FLAVOR = new java.awt.datatransfer.DataFlavor(
+ FileDrop.TransferableObject.class, MIME_TYPE);
+
+ private Fetcher fetcher;
+ private Object data;
+
+ private java.awt.datatransfer.DataFlavor customFlavor;
+
+ /**
+ * Creates a new {@link TransferableObject} that wraps data.
+ * Along with the {@link #DATA_FLAVOR} associated with this class, this
+ * creates a custom data flavor with a representation class determined
+ * from data.getClass() and the MIME type
+ * application/x-net.iharder.dnd.TransferableObject.
+ *
+ * @param data
+ * The data to transfer
+ * @since 1.1
+ */
+ public TransferableObject(final Object data) {
+ this.data = data;
+ this.customFlavor = new java.awt.datatransfer.DataFlavor(
+ data.getClass(), MIME_TYPE);
+ } // end constructor
+
+ /**
+ * Creates a new {@link TransferableObject} that will return the object
+ * that is returned by fetcher. No custom data flavor is set
+ * other than the default {@link #DATA_FLAVOR}.
+ *
+ * @see Fetcher
+ * @param fetcher
+ * The {@link Fetcher} that will return the data object
+ * @since 1.1
+ */
+ public TransferableObject(final Fetcher fetcher) {
+ this.fetcher = fetcher;
+ } // end constructor
+
+ /**
+ * Creates a new {@link TransferableObject} that will return the object
+ * that is returned by fetcher. Along with the
+ * {@link #DATA_FLAVOR} associated with this class, this creates a
+ * custom data flavor with a representation class dataClass
+ * and the MIME type
+ * application/x-net.iharder.dnd.TransferableObject.
+ *
+ * @see Fetcher
+ * @param dataClass
+ * The {@link java.lang.Class} to use in the custom data
+ * flavor
+ * @param fetcher
+ * The {@link Fetcher} that will return the data object
+ * @since 1.1
+ */
+ public TransferableObject(final Class> dataClass, final Fetcher fetcher) {
+ this.fetcher = fetcher;
+ this.customFlavor = new java.awt.datatransfer.DataFlavor(dataClass,
+ MIME_TYPE);
+ } // end constructor
+
+ /**
+ * Returns the custom {@link java.awt.datatransfer.DataFlavor}
+ * associated with the encapsulated object or null if the
+ * {@link Fetcher} constructor was used without passing a
+ * {@link java.lang.Class}.
+ *
+ * @return The custom data flavor for the encapsulated object
+ * @since 1.1
+ */
+ public java.awt.datatransfer.DataFlavor getCustomDataFlavor() {
+ return customFlavor;
+ } // end getCustomDataFlavor
+
+ /* ******** T R A N S F E R A B L E M E T H O D S ******** */
+
+ /**
+ * Returns a two- or three-element array containing first the custom
+ * data flavor, if one was created in the constructors, second the
+ * default {@link #DATA_FLAVOR} associated with
+ * {@link TransferableObject}, and third the
+ * {@link java.awt.datatransfer.DataFlavor#stringFlavor}.
+ *
+ * @return An array of supported data flavors
+ * @since 1.1
+ */
+ @Override
+ public java.awt.datatransfer.DataFlavor[] getTransferDataFlavors() {
+ if (customFlavor != null)
+ return new java.awt.datatransfer.DataFlavor[] { customFlavor,
+ DATA_FLAVOR,
+ java.awt.datatransfer.DataFlavor.stringFlavor }; // end
+ // flavors
+ // array
+ else
+ return new java.awt.datatransfer.DataFlavor[] { DATA_FLAVOR,
+ java.awt.datatransfer.DataFlavor.stringFlavor }; // end
+ // flavors
+ // array
+ } // end getTransferDataFlavors
+
+ /**
+ * Returns the data encapsulated in this {@link TransferableObject}. If
+ * the {@link Fetcher} constructor was used, then this is when the
+ * {@link Fetcher#getObject getObject()} method will be called. If the
+ * requested data flavor is not supported, then the
+ * {@link Fetcher#getObject getObject()} method will not be called.
+ *
+ * @param flavor
+ * The data flavor for the data to return
+ * @return The dropped data
+ * @since 1.1
+ */
+ @Override
+ public Object getTransferData(final java.awt.datatransfer.DataFlavor flavor)
+ throws java.awt.datatransfer.UnsupportedFlavorException,
+ java.io.IOException {
+ // Native object
+ if (flavor.equals(DATA_FLAVOR))
+ return fetcher == null ? data : fetcher.getObject();
+
+ // String
+ if (flavor.equals(java.awt.datatransfer.DataFlavor.stringFlavor))
+ return fetcher == null ? data.toString() : fetcher.getObject()
+ .toString();
+
+ // We can't do anything else
+ throw new java.awt.datatransfer.UnsupportedFlavorException(flavor);
+ } // end getTransferData
+
+ /**
+ * Returns true if flavor is one of the supported
+ * flavors. Flavors are supported using the equals(...)
+ * method.
+ *
+ * @param flavor
+ * The data flavor to check
+ * @return Whether or not the flavor is supported
+ * @since 1.1
+ */
+ @Override
+ public boolean isDataFlavorSupported(
+ final java.awt.datatransfer.DataFlavor flavor) {
+ // Native object
+ if (flavor.equals(DATA_FLAVOR))
+ return true;
+
+ // String
+ if (flavor.equals(java.awt.datatransfer.DataFlavor.stringFlavor))
+ return true;
+
+ // We can't do anything else
+ return false;
+ } // end isDataFlavorSupported
+
+ /* ******** I N N E R I N T E R F A C E F E T C H E R ******** */
+
+ /**
+ * Instead of passing your data directly to the
+ * {@link TransferableObject} constructor, you may want to know exactly
+ * when your data was received in case you need to remove it from its
+ * source (or do anyting else to it). When the {@link #getTransferData
+ * getTransferData(...)} method is called on the
+ * {@link TransferableObject}, the {@link Fetcher}'s {@link #getObject
+ * getObject()} method will be called.
+ *
+ * @author Robert Harder
+ * @version 1.1
+ * @since 1.1
+ */
+ public static interface Fetcher {
+ /**
+ * Return the object being encapsulated in the
+ * {@link TransferableObject}.
+ *
+ * @return The dropped object
+ * @since 1.1
+ */
+ public abstract Object getObject();
+ } // end inner interface Fetcher
+
+ } // end class TransferableObject
+
+} // end class FileDrop
diff --git a/src/main/java/org/scijava/ui/swing/script/FileSystemTree.java b/src/main/java/org/scijava/ui/swing/script/FileSystemTree.java
index 25111a68..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:
" //
+ + "
" //
+ + " " //
+ + " | Pattern | " //
+ + " Result | " //
+ + "
" //
+ + " " //
+ + " | py$ | " //
+ + " Display filenames ending with py | " //
+ + "
" //
+ + " " //
+ + " | ^Demo | " //
+ + " Display 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() {