Skip to content

Commit a76d41d

Browse files
committed
Implement Outline Panel and minor fixes
Implement Outline Panel. for Java, JS, and XML. Fixes: - Fix spurious 'file has changed' prompts, see https://forum.image.sc/t/shiny-new-script-editor/64160/33 - Disable AquaLaF hack on main split pane. No longer needed - Revert GotoType shortcut as custom override was never successful - Ensure editing shortcuts in Help> and contextual menus are consistent with menubar
1 parent 2de8cfb commit a76d41d

File tree

3 files changed

+190
-20
lines changed

3 files changed

+190
-20
lines changed

src/main/java/org/scijava/ui/swing/script/EditorPaneActions.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,13 @@ public EditorPaneActions(final EditorPane editorPane) {
9090
map.put(KeyStroke.getKeyStroke(KeyEvent.VK_I, defaultMod), epaIncreaseIndentAction);
9191
map.put(KeyStroke.getKeyStroke(KeyEvent.VK_I, defaultMod + shift), rstaDecreaseIndentAction);
9292

93+
// editing: override defaults for undo/redo/copy/cut/paste for consistency with menubar
94+
map.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, defaultMod), copyAction);
95+
map.put(KeyStroke.getKeyStroke(KeyEvent.VK_V, defaultMod), pasteAction);
96+
map.put(KeyStroke.getKeyStroke(KeyEvent.VK_X, defaultMod), cutAction);
97+
map.put(KeyStroke.getKeyStroke(KeyEvent.VK_Z, defaultMod), rtaUndoAction);
98+
map.put(KeyStroke.getKeyStroke(KeyEvent.VK_Y, defaultMod), rtaRedoAction); // this should be ctrl+shift+z!?
99+
93100
/*
94101
* see RSyntaxTextAreaDefaultInputMap and RTADefaultInputMap for other bindings.
95102
* Note that some of those bindings must be overridden in map.getParent()
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/*
2+
* #%L
3+
* Script Editor and Interpreter for SciJava script languages.
4+
* %%
5+
* Copyright (C) 2009 - 2022 SciJava developers.
6+
* %%
7+
* Redistribution and use in source and binary forms, with or without
8+
* modification, are permitted provided that the following conditions are met:
9+
*
10+
* 1. Redistributions of source code must retain the above copyright notice,
11+
* this list of conditions and the following disclaimer.
12+
* 2. Redistributions in binary form must reproduce the above copyright notice,
13+
* this list of conditions and the following disclaimer in the documentation
14+
* and/or other materials provided with the distribution.
15+
*
16+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19+
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
20+
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21+
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26+
* POSSIBILITY OF SUCH DAMAGE.
27+
* #L%
28+
*/
29+
30+
package org.scijava.ui.swing.script;
31+
32+
import java.awt.Color;
33+
import java.awt.Graphics2D;
34+
import java.awt.Insets;
35+
import java.awt.RenderingHints;
36+
import java.awt.font.FontRenderContext;
37+
import java.awt.font.LineBreakMeasurer;
38+
import java.awt.font.TextAttribute;
39+
import java.awt.font.TextLayout;
40+
import java.text.AttributedCharacterIterator;
41+
import java.text.AttributedString;
42+
43+
import javax.swing.JMenuItem;
44+
import javax.swing.JPopupMenu;
45+
import javax.swing.JScrollPane;
46+
import javax.swing.JTree;
47+
import javax.swing.tree.TreeNode;
48+
49+
import org.fife.rsta.ac.AbstractSourceTree;
50+
import org.fife.rsta.ac.java.tree.JavaOutlineTree;
51+
import org.fife.rsta.ac.js.tree.JavaScriptOutlineTree;
52+
import org.fife.rsta.ac.xml.tree.XmlOutlineTree;
53+
import org.scijava.script.ScriptLanguage;
54+
55+
/**
56+
* Convenience class for displaying a {@link AbstractSourceTree}
57+
*
58+
* @author Tiago Ferreira
59+
*/
60+
class OutlineTreePanel extends JScrollPane {
61+
62+
private static final long serialVersionUID = -710040159139542578L;
63+
private AbstractSourceTree sourceTree;
64+
private final Color placeholdColor;
65+
private float fontSize;
66+
67+
OutlineTreePanel(final TextEditor editor) {
68+
super();
69+
fontSize = getFont().getSize();
70+
setViewportView(new UnsupportedLangTree());
71+
placeholdColor = TextEditor.getDisabledComponentColor();
72+
73+
}
74+
75+
protected void refreshSourceTree(final EditorPane pane) {
76+
if (!isVisible())
77+
return;
78+
if (sourceTree != null) {
79+
sourceTree.uninstall();
80+
}
81+
final ScriptLanguage sLanguage = pane.getCurrentLanguage();
82+
final String language = (sLanguage == null) ? "" : sLanguage.getLanguageName();
83+
switch (language) {
84+
case "Java":
85+
sourceTree = new JavaOutlineTree();
86+
break;
87+
case "JavaScript":
88+
sourceTree = new JavaScriptOutlineTree();
89+
break;
90+
default:
91+
if (EditorPane.SYNTAX_STYLE_XML.equals(pane.getSyntaxEditingStyle())) {
92+
sourceTree = new XmlOutlineTree();
93+
} else
94+
sourceTree = null;
95+
break;
96+
}
97+
fontSize = pane.getFontSize();
98+
if (sourceTree == null) {
99+
setViewportView(new UnsupportedLangTree(pane));
100+
} else {
101+
sourceTree.setFont(sourceTree.getFont().deriveFont(fontSize));
102+
sourceTree.listenTo(pane);
103+
setViewportView(sourceTree);
104+
setPopupMenu(sourceTree, pane);
105+
}
106+
revalidate();
107+
}
108+
109+
private void setPopupMenu(final JTree tree, final EditorPane pane) {
110+
final JPopupMenu popup = new JPopupMenu();
111+
final JMenuItem jmi = new JMenuItem("Refresh");
112+
jmi.addActionListener(e -> refreshSourceTree(pane));
113+
popup.add(jmi);
114+
setComponentPopupMenu(popup);
115+
}
116+
117+
private class UnsupportedLangTree extends JTree {
118+
119+
private static final long serialVersionUID = 1L;
120+
private static final String HOLDER = "Outline not available... "
121+
+ "(Currently, only Java, JS, & XML are supported)";
122+
123+
public UnsupportedLangTree() {
124+
super((TreeNode) null);
125+
}
126+
127+
public UnsupportedLangTree(EditorPane pane) {
128+
this();
129+
setPopupMenu(this, pane);
130+
}
131+
132+
@Override
133+
protected void paintComponent(final java.awt.Graphics g) {
134+
super.paintComponent(g);
135+
final Graphics2D g2 = (Graphics2D) g.create();
136+
g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
137+
g2.setColor(placeholdColor);
138+
final Insets i = getInsets();
139+
float yTop = i.top;
140+
final int w = getWidth() - i.left - i.right;
141+
final int lastIndex = HOLDER.length();
142+
final AttributedString ac = new AttributedString(HOLDER);
143+
ac.addAttribute(TextAttribute.SIZE, fontSize, 0, lastIndex);
144+
ac.addAttribute(TextAttribute.POSTURE, TextAttribute.POSTURE_OBLIQUE, 0, lastIndex);
145+
final AttributedCharacterIterator aci = ac.getIterator();
146+
final FontRenderContext frc = g2.getFontRenderContext();
147+
final LineBreakMeasurer lbm = new LineBreakMeasurer(aci, frc);
148+
while (lbm.getPosition() < aci.getEndIndex()) {
149+
// see https://stackoverflow.com/a/41118280s
150+
final TextLayout tl = lbm.nextLayout(w);
151+
final float xPos = (float) (i.left + ((getWidth() - tl.getBounds().getWidth()) / 2));
152+
final float yPos = (float) (yTop + ((getHeight() - tl.getBounds().getHeight()) / 2));
153+
tl.draw(g2, xPos, yPos + tl.getAscent());
154+
yTop += tl.getDescent() + tl.getLeading() + tl.getAscent();
155+
}
156+
g2.dispose();
157+
}
158+
159+
}
160+
}

src/main/java/org/scijava/ui/swing/script/TextEditor.java

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ public class TextEditor extends JFrame implements ActionListener,
246246
private FindAndReplaceDialog findDialog;
247247
private JCheckBoxMenuItem autoSave, wrapLines, tabsEmulated, autoImport,
248248
autocompletion, fallbackAutocompletion, keylessAutocompletion,
249-
markOccurences, paintTabs, whiteSpace, marginLine;
249+
markOccurences, paintTabs, whiteSpace, marginLine, lockPane;
250250
private ButtonGroup themeRadioGroup;
251251
private JTextArea errorScreen = new JTextArea();
252252

@@ -299,11 +299,13 @@ public class TextEditor extends JFrame implements ActionListener,
299299
private boolean incremental = false;
300300
private DragSource dragSource;
301301
private boolean layoutLoading = true;
302+
private OutlineTreePanel sourceTreePanel;
302303

303304
public static final ArrayList<TextEditor> instances = new ArrayList<>();
304305
public static final ArrayList<Context> contexts = new ArrayList<>();
305306

306307
public TextEditor(final Context context) {
308+
307309
super("Script Editor");
308310
instances.add(this);
309311
contexts.add(context);
@@ -313,15 +315,10 @@ public TextEditor(final Context context) {
313315
// NB: All panes must be initialized before menus are assembled!
314316
tabbed = new JTabbedPane();
315317
tree = new FileSystemTree(log);
316-
body = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, new FileSystemTreePanel(tree, context), tabbed);
317-
try {// on Aqua L&F, 'one touch arrows' collide with borderless options button, which in turn are
318-
// needed for proper resize of the search panel. Grrrrr....
319-
if ("com.apple.laf.AquaLookAndFeel".equals(UIManager.getLookAndFeel().getClass().getName())) {
320-
body.setOneTouchExpandable(false);
321-
}
322-
} catch (final Exception ignored) {
323-
// do nothing
324-
}
318+
final JTabbedPane sideTabs = new JTabbedPane();
319+
sideTabs.addTab("File Explorer", new FileSystemTreePanel(tree, context));
320+
sideTabs.addTab("Outline", sourceTreePanel = new OutlineTreePanel(this));
321+
body = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, sideTabs, tabbed);
325322

326323
// These items are dynamic and need to be initialized before EditorPane creation
327324
initializeDynamicMenuComponents();
@@ -356,7 +353,7 @@ public TextEditor(final Context context) {
356353
makeJarWithSource = addToMenu(file, "Export as JAR (With Source)...", 0, 0);
357354
makeJarWithSource.setMnemonic(KeyEvent.VK_X);
358355
file.addSeparator();
359-
final JCheckBoxMenuItem lockPane = new JCheckBoxMenuItem("Lock (Make Read Only)");
356+
lockPane = new JCheckBoxMenuItem("Lock (Make Read Only)");
360357
lockPane.setToolTipText("Protects file from accidental editing");
361358
file.add(lockPane);
362359
lockPane.addActionListener(e -> {
@@ -366,7 +363,6 @@ public TextEditor(final Context context) {
366363
new SetWritableAction().actionPerformedImpl(e, getEditorPane());
367364
}
368365
});
369-
tabbed.addChangeListener(e -> lockPane.setSelected(getTab(tabbed.getSelectedIndex()).editorPane.isLocked()));
370366
JMenuItem jmi = new JMenuItem("Revert...");
371367
jmi.addActionListener(e -> {
372368
if (getEditorPane().isLocked()) {
@@ -552,7 +548,7 @@ public TextEditor(final Context context) {
552548
tabsMenu.setMnemonic(KeyEvent.VK_W);
553549
addMenubarSeparator(tabsMenu, "Panes:");
554550
// Assume initial status from prefs or panel visibility
555-
final JCheckBoxMenuItem jcmi1 = new JCheckBoxMenuItem("File Explorer",
551+
final JCheckBoxMenuItem jcmi1 = new JCheckBoxMenuItem("Side Pane",
556552
prefService.getInt(getClass(), MAIN_DIV_LOCATION, body.getDividerLocation()) > 0
557553
|| isLeftPaneExpanded(body));
558554
jcmi1.addItemListener(e -> collapseSplitPane(0, !jcmi1.isSelected()));
@@ -677,8 +673,14 @@ public TextEditor(final Context context) {
677673

678674
// -- END MENUS --
679675

680-
// Add the editor and output area
676+
// add tab-related listeners
681677
tabbed.addChangeListener(this);
678+
sideTabs.addChangeListener(e -> {
679+
if (sideTabs.getSelectedIndex() == 1)
680+
sourceTreePanel.refreshSourceTree(getTab(tabbed.getSelectedIndex()).editorPane);
681+
});
682+
683+
// Add the editor and output area
682684
new FileDrop(tabbed, files -> {
683685
final ArrayList<File> filteredFiles = new ArrayList<>();
684686
assembleFlatFileCollection(filteredFiles, files);
@@ -895,9 +897,7 @@ private void assembleEditMenu() {
895897
addMappedActionToMenu(editMenu, "Goto Matching Bracket", EditorPaneActions.rstaGoToMatchingBracketAction, false);
896898

897899
final JMenuItem gotoType = new JMenuItem("Goto Type...");
898-
gotoType.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_G, ctrl + shift)); // default is Ctrl+Shift+O
899-
gotoType.setToolTipText(
900-
"Alternative shortcut: " + getEditorPane().getPaneActions().getAcceleratorLabel("GoToType"));
900+
gotoType.setAccelerator(getEditorPane().getPaneActions().getAccelerator("GoToType"));
901901
gotoType.addActionListener(e -> {
902902
try {
903903
getTextArea().getActionMap().get("GoToType").actionPerformed(e);
@@ -1976,6 +1976,7 @@ public void stateChanged(final ChangeEvent e) {
19761976
return;
19771977
}
19781978
final EditorPane editorPane = getEditorPane(index);
1979+
lockPane.setSelected(editorPane.isLocked());
19791980
editorPane.requestFocus();
19801981
checkForOutsideChanges();
19811982

@@ -2209,6 +2210,7 @@ public TextEditorTab open(final File file) {
22092210

22102211
updateLanguageMenu(tab.editorPane.getCurrentLanguage());
22112212
tab.editorPane.getDocument().addDocumentListener(this);
2213+
tab.editorPane.requestFocusInWindow();
22122214
return tab;
22132215
}
22142216
catch (final FileNotFoundException e) {
@@ -2460,9 +2462,9 @@ public void updateTabAndFontSize(final boolean setByLanguage) {
24602462

24612463
public void updateUI(final boolean setByLanguage) {
24622464
final EditorPane pane = getEditorPane();
2463-
if (pane.getCurrentLanguage() == null) return;
2465+
//if (pane.getCurrentLanguage() == null) return;
24642466

2465-
if (setByLanguage) {
2467+
if (setByLanguage && pane.getCurrentLanguage() != null) {
24662468
if (pane.getCurrentLanguage().getLanguageName().equals("Python")) {
24672469
pane.setTabSize(4);
24682470
}
@@ -2513,6 +2515,7 @@ else if (tabSize == Integer.parseInt(item.getText())) {
25132515
autocompletion.setState(pane.isAutoCompletionEnabled());
25142516
fallbackAutocompletion.setState(pane.isAutoCompletionFallbackEnabled());
25152517
keylessAutocompletion.setState(pane.isAutoCompletionKeyless());
2518+
sourceTreePanel.refreshSourceTree(pane);
25162519
}
25172520

25182521
public void setEditorPaneFileName(final String baseName) {
@@ -3928,7 +3931,7 @@ else if (file.isDirectory())
39283931
return collection;
39293932
}
39303933

3931-
private static Color getDisabledComponentColor() {
3934+
protected static Color getDisabledComponentColor() {
39323935
try {
39333936
return UIManager.getColor("MenuItem.disabledForeground");
39343937
} catch (final Exception ignored) {

0 commit comments

Comments
 (0)