diff --git a/pom.xml b/pom.xml index e0b93fac..9acd19cc 100644 --- a/pom.xml +++ b/pom.xml @@ -164,10 +164,12 @@ com.fifesoft rsyntaxtextarea + 3.3.0 com.fifesoft languagesupport + 3.3.0 com.miglayout @@ -199,6 +201,10 @@ scripting-groovy test + + com.formdev + flatlaf + diff --git a/src/main/java/org/scijava/ui/swing/script/CommandPalette.java b/src/main/java/org/scijava/ui/swing/script/CommandPalette.java new file mode 100644 index 00000000..92301eff --- /dev/null +++ b/src/main/java/org/scijava/ui/swing/script/CommandPalette.java @@ -0,0 +1,928 @@ +/*- + * #%L + * Fiji distribution of ImageJ for the life sciences. + * %% + * Copyright (C) 2010 - 2022 Fiji developers. + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +package org.scijava.ui.swing.script; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.Insets; +import java.awt.Rectangle; +import java.awt.event.ActionEvent; +import java.awt.event.InputEvent; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import javax.swing.AbstractAction; +import javax.swing.AbstractButton; +import javax.swing.Action; +import javax.swing.ActionMap; +import javax.swing.InputMap; +import javax.swing.JComponent; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JMenu; +import javax.swing.JMenuBar; +import javax.swing.JMenuItem; +import javax.swing.JPopupMenu; +import javax.swing.JRootPane; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.JTextField; +import javax.swing.KeyStroke; +import javax.swing.ListSelectionModel; +import javax.swing.MenuElement; +import javax.swing.SwingUtilities; +import javax.swing.UIManager; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.table.AbstractTableModel; +import javax.swing.table.DefaultTableCellRenderer; + +import org.fife.ui.rtextarea.RecordableTextAction; +import org.scijava.util.PlatformUtils; + +class CommandPalette { + + private static final String NAME = "Command Palette..."; + + /** Settings. Ought to become adjustable some day */ + private static final KeyStroke ACCELERATOR = KeyStroke.getKeyStroke(KeyEvent.VK_P, + java.awt.Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() | KeyEvent.SHIFT_DOWN_MASK); + private static final int TABLE_ROWS = 6; // no. of commands to be displayed + private static final float OPACITY = 1f; // 0-1 range + private static final boolean IGNORE_WHITESPACE = true; // Ignore white spaces while matching? + private static Palette frame; + + private SearchField searchField; + private CmdTable table; + private final TextEditor textEditor; + private final CmdAction noHitsCmd; + private final CmdScrapper cmdScrapper; + + public CommandPalette(final TextEditor textEditor) { + this.textEditor = textEditor; + noHitsCmd = new SearchWebCmd(); + cmdScrapper = new CmdScrapper(textEditor); + } + + void install(final JMenu toolsMenu) { + final Action action = new AbstractAction(NAME) { + + private static final long serialVersionUID = -7030359886427866104L; + + @Override + public void actionPerformed(final ActionEvent e) { + toggleVisibility(); + } + + }; + action.putValue(Action.ACCELERATOR_KEY, ACCELERATOR); + toolsMenu.add(new JMenuItem(action)); + } + + Map getShortcuts() { + if (!cmdScrapper.scrapeSuccessful()) + cmdScrapper.scrape(); + final TreeMap result = new TreeMap<>(); + cmdScrapper.getCmdMap().forEach((id, cmdAction) -> { + if (cmdAction.hotkey != null && !cmdAction.hotkey.isEmpty()) + result.put(id, cmdAction.hotkey); + + }); + return result; + } + + Map getRecordableActions() { + if (!cmdScrapper.scrapeSuccessful()) + cmdScrapper.scrape(); + final TreeMap result = new TreeMap<>(); + cmdScrapper.getCmdMap().forEach((id, cmdAction) -> { + if (cmdAction.recordable()) + result.put(id, cmdAction.hotkey); + + }); + return result; + } + + void register(final AbstractButton button, final String description) { + cmdScrapper.registerOther(button, description); + } + + void dispose() { + if (frame != null) frame.dispose(); + frame = null; + } + + private void hideWindow() { + if (frame != null) frame.setVisible(false); + } + + private void assemblePalette() { + if (frame != null) + return; + frame = new Palette(); + frame.setLayout(new BorderLayout()); + searchField = new SearchField(); + frame.add(searchField, BorderLayout.NORTH); + searchField.getDocument().addDocumentListener(new PromptDocumentListener()); + final InternalKeyListener keyListener = new InternalKeyListener(); + searchField.addKeyListener(keyListener); + table = new CmdTable(); + table.addKeyListener(keyListener); + table.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(final MouseEvent e) { + if (e.getClickCount() == 2 && table.getSelectedRow() > -1) { + runCmd(table.getInternalModel().getCommand(table.getSelectedRow())); + } + } + }); + populateList(""); + frame.add(table.getScrollPane()); + frame.pack(); + } + + private String[] makeRow(final CmdAction ca) { + return new String[] {ca.id, ca.description()}; + } + + private void populateList(final String matchingSubstring) { + final ArrayList list = new ArrayList<>(); + if (!cmdScrapper.scrapeSuccessful()) + cmdScrapper.scrape(); + cmdScrapper.getCmdMap().forEach((id, cmd) -> { + if (cmd.matches(matchingSubstring)) { + list.add(makeRow(cmd)); + } + }); + if (list.isEmpty()) { + list.add(makeRow(noHitsCmd)); + } + table.getInternalModel().setData(list); + if (searchField != null) + searchField.requestFocus(); + } + + private void runCmd(final String command) { + SwingUtilities.invokeLater(() -> { + if (CmdScrapper.REBUILD_ID.equals(command)) { + cmdScrapper.scrape(); + table.clearSelection(); + searchField.setText(""); + searchField.requestFocus(); + frame.setVisible(true); + return; + } + CmdAction cmd; + if (noHitsCmd != null && command.equals(noHitsCmd.id)) { + cmd = noHitsCmd; + } else { + cmd = cmdScrapper.getCmdMap().get(command); + } + if (cmd != null) { + final boolean hasButton = cmd.button != null; + if (hasButton && !cmd.button.isEnabled()) { + textEditor.error("Command is currently disabled. Either execution requirements " + + "are unmet or it is not supported by current language."); + frame.setVisible(true); + return; + } + hideWindow(); // hide before running, in case command opens a dialog + if (hasButton) { + cmd.button.doClick(); + } else if (cmd.action != null) { + cmd.action.actionPerformed( + new ActionEvent(textEditor.getTextArea(), ActionEvent.ACTION_PERFORMED, cmd.id)); + } + } + }); + } + + private void toggleVisibility() { + if (frame == null) { + assemblePalette(); + } + if (frame.isVisible()) { + hideWindow(); + } else { + frame.center(textEditor); + table.clearSelection(); + frame.setVisible(true); + searchField.requestFocus(); + } + } + + private static class SearchField extends TextEditor.TextFieldWithPlaceholder { + private static final long serialVersionUID = 1L; + private static final int PADDING = 4; + static final Font REF_FONT = refFont(); + + SearchField() { + setPlaceholder(" Search for commands and actions (e.g., Theme)"); + setMargin(new Insets(PADDING, PADDING, 0, 0)); + setFont(REF_FONT.deriveFont(REF_FONT.getSize() * 1.5f)); + } + + @Override + Font getPlaceholderFont() { + return REF_FONT.deriveFont(Font.ITALIC); + } + + static Font refFont() { + try { + return UIManager.getFont("TextField.font"); + } catch (final Exception ignored) { + return new JTextField().getFont(); + } + } + } + + private class PromptDocumentListener implements DocumentListener { + public void insertUpdate(final DocumentEvent e) { + populateList(getQueryFromSearchField()); + } + + public void removeUpdate(final DocumentEvent e) { + populateList(getQueryFromSearchField()); + } + + public void changedUpdate(final DocumentEvent e) { + populateList(getQueryFromSearchField()); + } + + String getQueryFromSearchField() { + final String text = searchField.getText(); + if (text == null) + return ""; + final String query = text.toLowerCase(); + return (IGNORE_WHITESPACE) ? query.replaceAll("\\s+", "") : query; + } + } + + private class InternalKeyListener extends KeyAdapter { + + @Override + public void keyPressed(final KeyEvent ke) { + final int key = ke.getKeyCode(); + final int flags = ke.getModifiersEx(); + final int items = table.getInternalModel().getRowCount(); + final Object source = ke.getSource(); + final boolean meta = ((flags & KeyEvent.META_DOWN_MASK) != 0) || ((flags & KeyEvent.CTRL_DOWN_MASK) != 0); + if (key == KeyEvent.VK_ESCAPE || (key == KeyEvent.VK_W && meta) || (key == KeyEvent.VK_P && meta)) { + hideWindow(); + } else if (source == searchField) { + /* + * If you hit enter in the text field, and there's only one command that + * matches, run that: + */ + if (key == KeyEvent.VK_ENTER) { + if (1 == items) + runCmd(table.getInternalModel().getCommand(0)); + } + /* + * If you hit the up or down arrows in the text field, move the focus to the + * table and select the row at the bottom or top. + */ + int index = -1; + if (key == KeyEvent.VK_UP) { + index = table.getSelectedRow() - 1; + if (index < 0) + index = items - 1; + } else if (key == KeyEvent.VK_DOWN) { + index = table.getSelectedRow() + 1; + if (index >= items) + index = Math.min(items - 1, 0); + } + if (index >= 0) { + table.requestFocus(); + // completions.ensureIndexIsVisible(index); + table.setRowSelectionInterval(index, index); + } + } else if (key == KeyEvent.VK_BACK_SPACE || key == KeyEvent.VK_DELETE) { + /* + * If someone presses backspace or delete they probably want to remove the last + * letter from the search string, so switch the focus back to the prompt: + */ + searchField.requestFocus(); + } else if (source == table) { + /* If you hit enter with the focus in the table, run the selected command */ + if (key == KeyEvent.VK_ENTER) { + ke.consume(); + final int row = table.getSelectedRow(); + if (row >= 0) + runCmd(table.getInternalModel().getCommand(row)); + /* Loop through the list using the arrow keys */ + } else if (key == KeyEvent.VK_UP) { + if (table.getSelectedRow() == 0) + table.setRowSelectionInterval(table.getRowCount() - 1, table.getRowCount() - 1); + } else if (key == KeyEvent.VK_DOWN) { + if (table.getSelectedRow() == table.getRowCount() - 1) + table.setRowSelectionInterval(0, 0); + } + } + } + } + + private class Palette extends JFrame { + private static final long serialVersionUID = 1L; + + Palette() { + super("Command Palette"); + setUndecorated(true); + setAlwaysOnTop(true); + setOpacity(OPACITY); + getRootPane().setWindowDecorationStyle(JRootPane.NONE); + // it should NOT be possible to minimize this frame, but just to + // be safe, we'll ensure the frame is never in an awkward state + addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(final WindowEvent e) { + hideWindow(); + } + + @Override + public void windowIconified(final WindowEvent e) { + hideWindow(); + } + + @Override + public void windowDeactivated(final WindowEvent e) { + hideWindow(); + } + }); + } + + void center(final Container component) { + final Rectangle bounds = component.getBounds(); + final Dimension w = getSize(); + int x = bounds.x + (bounds.width - w.width) / 2; + int y = bounds.y + (bounds.height - w.height) / 2; + if (x < 0) + x = 0; + if (y < 0) + y = 0; + setLocation(x, y); + } + } + + private class CmdTable extends JTable { + private static final long serialVersionUID = 1L; + + CmdTable() { + super(new CmdTableModel()); + setAutoCreateRowSorter(false); + setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + setShowGrid(false); + setRowSelectionAllowed(true); + setColumnSelectionAllowed(false); + setTableHeader(null); + setAutoResizeMode(AUTO_RESIZE_LAST_COLUMN); + final CmdTableRenderer renderer = new CmdTableRenderer(); + final int col0Width = renderer.maxWidh(0); + final int col1Width = renderer.maxWidh(1); + setDefaultRenderer(Object.class, renderer); + getColumnModel().getColumn(0).setMaxWidth(col0Width); + getColumnModel().getColumn(1).setMaxWidth(col1Width); + setRowHeight(renderer.rowHeight()); + int height = TABLE_ROWS * getRowHeight(); + if (getRowMargin() > 0) + height *= getRowMargin(); + setPreferredScrollableViewportSize(new Dimension(col0Width + col1Width, height)); + setFillsViewportHeight(true); + } + + private JScrollPane getScrollPane() { + final JScrollPane scrollPane = new JScrollPane(this, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, + JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); + scrollPane.setWheelScrollingEnabled(true); + return scrollPane; + } + + CmdTableModel getInternalModel() { + return (CmdTableModel) getModel(); + } + + } + + private class CmdTableRenderer extends DefaultTableCellRenderer { + + private static final long serialVersionUID = 1L; + final Font col0Font = SearchField.REF_FONT.deriveFont(SearchField.REF_FONT.getSize() * 1.2f); + final Font col1Font = SearchField.REF_FONT.deriveFont(SearchField.REF_FONT.getSize() * 1.2f); + + public Component getTableCellRendererComponent(final JTable table, final Object value, final boolean isSelected, + final boolean hasFocus, final int row, final int column) { + final Component c = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + if (column == 1) { + setHorizontalAlignment(JLabel.RIGHT); + setEnabled(false); + setFont(col1Font); + } else { + setHorizontalAlignment(JLabel.LEFT); + setEnabled(true); + setFont(col0Font); + } + return c; + } + + int rowHeight() { + return (int) (col0Font.getSize() * 1.75f); + } + + int maxWidh(final int columnIndex) { + if (columnIndex == 1) + return SwingUtilities.computeStringWidth(getFontMetrics(col1Font), "Really+Huge+Key+Combo"); + return SwingUtilities.computeStringWidth(getFontMetrics(col0Font), + "A large filename from the Recents menu.groovy"); + } + + } + + private class CmdTableModel extends AbstractTableModel { + private static final long serialVersionUID = 1L; + private final static int COLUMNS = 2; + List list; + + void setData(final ArrayList list) { + this.list = list; + fireTableDataChanged(); + } + + String getCommand(final int row) { + if (list.size() == 1) + return (String) getValueAt(row, 0); + else if (row < 0 || row >= list.size()) + return ""; + else + return (String) getValueAt(row, 0); + } + + @Override + public int getColumnCount() { + return COLUMNS; + } + + @Override + public Object getValueAt(final int row, final int column) { + if (row >= list.size() || column >= COLUMNS) + return null; + final String[] strings = (String[]) list.get(row); + return strings[column]; + } + + @Override + public int getRowCount() { + return list.size(); + } + + } + + private class CmdAction { + + final String id; + String menuLocation; + String hotkey; + AbstractButton button; + Action action; + + CmdAction(final String cmdName) { + this.id = capitalize(cmdName); + this.menuLocation = ""; + this.hotkey = ""; + } + + CmdAction(final String cmdName, final AbstractButton button) { + this(cmdName); + if (button.getAction() != null && button.getAction() instanceof AbstractAction) + action = (AbstractAction) button.getAction(); + else + this.button = button; + } + + CmdAction(final String cmdName, final Action action) { + this(cmdName); + this.action = action; + } + + boolean recordable() { + return action != null && action instanceof RecordableTextAction; + } + + String description() { + final String rec = (recordable()) ?" \u29BF" :""; + if (!hotkey.isEmpty()) + return hotkey + rec; + if (!menuLocation.isEmpty()) + return "|" + menuLocation + "|" + rec; + return rec; + } + + boolean matches(final String lowercaseQuery) { + if (IGNORE_WHITESPACE) { + return id.toLowerCase().replaceAll("\\s+", "").contains(lowercaseQuery) + || menuLocation.toLowerCase().contains(lowercaseQuery); + } + return id.toLowerCase().contains(lowercaseQuery) || menuLocation.toLowerCase().contains(lowercaseQuery); + } + + void setkeyString(final KeyStroke key) { + if (hotkey.isEmpty()) { + hotkey = prettifiedKey(key); + } else { + final String oldHotkey = hotkey; + final String newHotKey = prettifiedKey(key); + if (!oldHotkey.contains(newHotKey)) { + hotkey = oldHotkey + " or " + newHotKey; + } + } + } + + private String capitalize(final String string) { + return string.substring(0, 1).toUpperCase() + string.substring(1); + } + + private String prettifiedKey(final KeyStroke key) { + if (key == null) + return ""; + final StringBuilder s = new StringBuilder(); + final int m = key.getModifiers(); + if ((m & InputEvent.CTRL_DOWN_MASK) != 0) { + s.append((PlatformUtils.isMac()) ? "⌃ " : "Ctrl "); + } + if ((m & InputEvent.META_DOWN_MASK) != 0) { + s.append((PlatformUtils.isMac()) ? "⌘ " : "Ctrl "); + } + if ((m & InputEvent.ALT_DOWN_MASK) != 0) { + s.append((PlatformUtils.isMac()) ? "⎇ " : "Alt "); + } + if ((m & InputEvent.SHIFT_DOWN_MASK) != 0) { + s.append("⇧ "); + } + if ((m & InputEvent.BUTTON1_DOWN_MASK) != 0) { + s.append("L-click "); + } + if ((m & InputEvent.BUTTON2_DOWN_MASK) != 0) { + s.append("R-click "); + } + if ((m & InputEvent.BUTTON3_DOWN_MASK) != 0) { + s.append("M-click "); + } + switch (key.getKeyEventType()) { + case KeyEvent.KEY_TYPED: + s.append(key.getKeyChar() + " "); + break; + case KeyEvent.KEY_PRESSED: + case KeyEvent.KEY_RELEASED: + s.append(getKeyText(key.getKeyCode()) + " "); + break; + default: + break; + } + return s.toString(); + } + + String getKeyText(final int keyCode) { + if (keyCode >= KeyEvent.VK_0 && keyCode <= KeyEvent.VK_9 + || keyCode >= KeyEvent.VK_A && keyCode <= KeyEvent.VK_Z) { + return String.valueOf((char) keyCode); + } + switch (keyCode) { + case KeyEvent.VK_COMMA: + return ","; + case KeyEvent.VK_PERIOD: + return "."; + case KeyEvent.VK_SLASH: + return "/"; + case KeyEvent.VK_SEMICOLON: + return ";"; + case KeyEvent.VK_EQUALS: + return "="; + case KeyEvent.VK_OPEN_BRACKET: + return "["; + case KeyEvent.VK_BACK_SLASH: + return "\\"; + case KeyEvent.VK_CLOSE_BRACKET: + return "]"; + case KeyEvent.VK_ENTER: + return "↵"; + case KeyEvent.VK_BACK_SPACE: + return "⌫"; + case KeyEvent.VK_TAB: + return "↹"; + case KeyEvent.VK_CANCEL: + return "Cancel"; + case KeyEvent.VK_CLEAR: + return "Clear"; + case KeyEvent.VK_PAUSE: + return "Pause"; + case KeyEvent.VK_CAPS_LOCK: + return "Caps Lock"; + case KeyEvent.VK_ESCAPE: + return "Esc"; + case KeyEvent.VK_SPACE: + return "Space"; + case KeyEvent.VK_PAGE_UP: + return "⇞"; + case KeyEvent.VK_PAGE_DOWN: + return "⇟"; + case KeyEvent.VK_END: + return "END"; + case KeyEvent.VK_HOME: + return "Home"; // "⌂"; + case KeyEvent.VK_LEFT: + return "←"; + case KeyEvent.VK_UP: + return "↑"; + case KeyEvent.VK_RIGHT: + return "→"; + case KeyEvent.VK_DOWN: + return "↓"; + case KeyEvent.VK_MULTIPLY: + return "[Num ×]"; + case KeyEvent.VK_ADD: + return "[Num +]"; + case KeyEvent.VK_SUBTRACT: + return "[Num −]"; + case KeyEvent.VK_DIVIDE: + return "[Num /]"; + case KeyEvent.VK_DELETE: + return "⌦"; + case KeyEvent.VK_INSERT: + return "Ins"; + case KeyEvent.VK_BACK_QUOTE: + return "`"; + case KeyEvent.VK_QUOTE: + return "'"; + case KeyEvent.VK_AMPERSAND: + return "&"; + case KeyEvent.VK_ASTERISK: + return "*"; + case KeyEvent.VK_QUOTEDBL: + return "\""; + case KeyEvent.VK_LESS: + return "<"; + case KeyEvent.VK_GREATER: + return ">"; + case KeyEvent.VK_BRACELEFT: + return "{"; + case KeyEvent.VK_BRACERIGHT: + return "}"; + case KeyEvent.VK_COLON: + return ","; + case KeyEvent.VK_CIRCUMFLEX: + return "^"; + case KeyEvent.VK_DEAD_TILDE: + return "~"; + case KeyEvent.VK_DOLLAR: + return "$"; + case KeyEvent.VK_EXCLAMATION_MARK: + return "!"; + case KeyEvent.VK_LEFT_PARENTHESIS: + return "("; + case KeyEvent.VK_MINUS: + return "-"; + case KeyEvent.VK_PLUS: + return "+"; + case KeyEvent.VK_RIGHT_PARENTHESIS: + return ")"; + case KeyEvent.VK_UNDERSCORE: + return "_"; + default: + return KeyEvent.getKeyText(keyCode); + } + } + } + + private class CmdScrapper { + final TextEditor textEditor; + static final String REBUILD_ID = "Rebuild Actions Index"; + private final TreeMap cmdMap; + private TreeMap otherMap; + + CmdScrapper(final TextEditor textEditor) { + this.textEditor = textEditor; + // It seems that the ScriptEditor has duplicated actions(!?) registered + // in input/action maps. Some Duplicates seem to be defined in lower + // case, so we'll assemble a case-insensitive map to mitigate this + cmdMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + } + + TreeMap getCmdMap() { + if (otherMap != null) + cmdMap.putAll(otherMap); + return cmdMap; + } + + boolean scrapeSuccessful() { + return !cmdMap.isEmpty(); + } + + void scrape() { + cmdMap.clear(); + cmdMap.put(REBUILD_ID, new CmdAction(REBUILD_ID)); + parseActionAndInputMaps(); + final JMenuBar menuBar = textEditor.getJMenuBar(); + final int topLevelMenus = menuBar.getMenuCount(); + for (int i = 0; i < topLevelMenus; ++i) { + final JMenu topLevelMenu = menuBar.getMenu(i); + if (topLevelMenu != null && topLevelMenu.getText() != null) { + parseMenu(topLevelMenu.getText(), topLevelMenu); + } + } + final JPopupMenu popup = textEditor.getEditorPane().getPopupMenu(); + if (popup != null) { + getMenuItems(popup).forEach(mi -> { + registerMenuItem(mi, "Popup Menu"); + }); + } + } + + private void parseActionAndInputMaps() { + final InputMap inputMap = textEditor.getTextArea().getInputMap(JComponent.WHEN_FOCUSED); + final KeyStroke[] keys = inputMap.allKeys(); + if (keys != null) { + for (final KeyStroke key : keys) { + if (key.getModifiers() == 0) { + // ignore 'typed' keystrokes and related single-key actions + continue; + } + final Object obj = inputMap.get(key); + CmdAction cmdAction; + String cmdName; + if (obj instanceof Action) { + cmdName = (String) ((Action) obj).getValue(Action.NAME); + cmdAction = new CmdAction(cleanseActionDescription(cmdName), (AbstractAction) obj); + } else if (obj instanceof AbstractButton) { + cmdName = ((AbstractButton) obj).getText(); + cmdAction = new CmdAction(cmdName, (AbstractButton) obj); + } else { + continue; + } + cmdAction.setkeyString(key); + cmdMap.put(cmdAction.id, cmdAction); + } + } + final ActionMap actionMap = textEditor.getTextArea().getActionMap(); + for (final Object obj : actionMap.keys()) { + if (obj instanceof String && cmdMap.get((String) obj) == null) { + final Action action = actionMap.get((String) obj); + final CmdAction cmdAction = new CmdAction(cleanseActionDescription((String) obj), action); + cmdMap.put(cmdAction.id, cmdAction); + } + } + } + + private void parseMenu(final String componentHostingMenu, final JMenu menu) { + final int n = menu.getItemCount(); + for (int i = 0; i < n; ++i) { + registerMenuItem(menu.getItem(i), componentHostingMenu); + } + } + + private void registerMenuItem(final JMenuItem m, final String hostingComponent) { + if (m != null) { + String label = m.getActionCommand(); + if (label == null) + label = m.getText(); + if (m instanceof JMenu) { + final JMenu subMenu = (JMenu) m; + String hostDesc = subMenu.getText(); + if (hostDesc == null) + hostDesc = hostingComponent; + parseMenu(hostDesc, subMenu); + } else { + registerMain(m, hostingComponent); + } + } + } + + private List getMenuItems(final JPopupMenu popupMenu) { + final List list = new ArrayList<>(); + for (final MenuElement me : popupMenu.getSubElements()) { + if (me == null) { + continue; + } else if (me instanceof JMenuItem) { + list.add((JMenuItem) me); + } else if (me instanceof JMenu) { + getMenuItems((JMenu) me, list); + } + } + return list; + } + + private void getMenuItems(final JMenu menu, final List holdingList) { + for (int j = 0; j < menu.getItemCount(); j++) { + final JMenuItem jmi = menu.getItem(j); + if (jmi == null) + continue; + if (jmi instanceof JMenu) { + getMenuItems((JMenu) jmi, holdingList); + } else { + holdingList.add(jmi); + } + } + } + + private boolean irrelevantCommand(final String label) { + // commands that would only add clutter to the palette + return label == null || label.endsWith(" pt") || label.length() < 2; + } + + void registerMain(final AbstractButton button, final String description) { + register(cmdMap, button, description); + } + + void registerOther(final AbstractButton button, final String description) { + if (otherMap == null) + otherMap = new TreeMap<>(); + register(otherMap, button, description); + } + + private void register(final TreeMap map, final AbstractButton button, + final String descriptionOfComponentHostingButton) { + String label = button.getActionCommand(); + if (NAME.equals(label)) + return; // do not register command palette + if (label == null) + label = button.getText().trim(); + if (irrelevantCommand(label)) + return; + if (label.endsWith("...")) + label = label.substring(0, label.length() - 3); + // If a command has already been registered, we'll include its accelerator + final boolean isMenuItem = button instanceof JMenuItem; + final CmdAction registeredAction = (CmdAction) map.get(label); + final KeyStroke accelerator = (isMenuItem) ? ((JMenuItem) button).getAccelerator() : null; + if (registeredAction != null && accelerator != null) { + registeredAction.setkeyString(accelerator); + } else { + final CmdAction ca = new CmdAction(label, button); + ca.menuLocation = descriptionOfComponentHostingButton; + if (accelerator != null) ca.setkeyString(accelerator); + map.put(ca.id, ca); + } + } + + private String cleanseActionDescription(String actionId) { + if (actionId.startsWith("RTA.")) + actionId = actionId.substring(4); + else if (actionId.startsWith("RSTA.")) + actionId = actionId.substring(5); + if (actionId.endsWith("Action")) + actionId = actionId.substring(0, actionId.length() - 6); + actionId = actionId.replace("-", " "); + return actionId.replaceAll("([A-Z])", " $1").trim(); // CamelCase to Camel Case + } + + } + + private class SearchWebCmd extends CmdAction { + SearchWebCmd() { + super("Search the Web"); + button = new JMenuItem(new AbstractAction(id) { + private static final long serialVersionUID = 1L; + + @Override + public void actionPerformed(final ActionEvent e) { + TextEditor.GuiUtils.runSearchQueryInBrowser(textEditor, textEditor.getPlatformService(), + searchField.getText()); + } + }); + } + + @Override + String description() { + return "|Unmatched action|"; + } + } +} \ No newline at end of file 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 78a39d7f..6e08b809 100644 --- a/src/main/java/org/scijava/ui/swing/script/EditorPane.java +++ b/src/main/java/org/scijava/ui/swing/script/EditorPane.java @@ -56,7 +56,6 @@ import javax.swing.ButtonGroup; import javax.swing.JMenu; import javax.swing.JMenuItem; -import javax.swing.JOptionPane; import javax.swing.JPopupMenu; import javax.swing.JRadioButtonMenuItem; import javax.swing.JScrollPane; @@ -289,15 +288,6 @@ private JMenuItem getMenuItem(final String label, final String actionID, final b final Action action = getActionMap().get(actionID); final JMenuItem jmi = new JMenuItem(action); jmi.setAccelerator(getPaneActions().getAccelerator(actionID)); - jmi.addActionListener(e -> { - if (editingAction && isLocked()) { - UIManager.getLookAndFeel().provideErrorFeedback(this); - } else try { - action.actionPerformed(e); - } catch (final Exception | Error ex) { - log.debug(ex); - } - }); jmi.setText(label); return jmi; } @@ -314,7 +304,9 @@ private JMenu geSyntaxForNoneLang() { menu.add(getSyntaxItem(bg, "Dockerfile", SYNTAX_STYLE_DOCKERFILE)); menu.add(getSyntaxItem(bg, "HTML", SYNTAX_STYLE_HTML)); menu.add(getSyntaxItem(bg, "JSON", SYNTAX_STYLE_JSON)); + menu.add(getSyntaxItem(bg, "Kotlin", SYNTAX_STYLE_KOTLIN)); menu.add(getSyntaxItem(bg, "Makefile", SYNTAX_STYLE_MAKEFILE)); + menu.add(getSyntaxItem(bg, "Markdown", SYNTAX_STYLE_MARKDOWN)); menu.add(getSyntaxItem(bg, "SH", SYNTAX_STYLE_UNIX_SHELL)); menu.add(getSyntaxItem(bg, "XML", SYNTAX_STYLE_XML)); menu.add(getSyntaxItem(bg, "YAML", SYNTAX_STYLE_YAML)); @@ -324,12 +316,11 @@ private JMenu geSyntaxForNoneLang() { private JMenuItem getSyntaxItem(final ButtonGroup bg, final String label, final String syntaxId) { final JRadioButtonMenuItem item = new JRadioButtonMenuItem(label); bg.add(item); - item.setActionCommand(syntaxId); item.addActionListener(e -> { if (getCurrentLanguage() == null) { setSyntaxEditingStyle(syntaxId); } else { - log.error("[BUG] Unknown state: Non-executable syntaxes cannot be applied to valid languages"); + error("Non-executable syntaxes can only be applied when scripting language is 'None'."); bg.getElements().nextElement().setSelected(true); //select none setSyntaxEditingStyle(SYNTAX_STYLE_NONE); } @@ -337,6 +328,10 @@ private JMenuItem getSyntaxItem(final ButtonGroup bg, final String label, final return item; } + void error(final String message) { + TextEditor.GuiUtils.error(this, message); + } + private void updateNoneLangSyntaxMenu(final ScriptLanguage language) { noneLangSyntaxMenu.setEnabled(language == null); if (language == null) { @@ -945,8 +940,7 @@ public void toggleBookmark(final int line) { } catch (final BadLocationException e) { /* ignore */ - JOptionPane.showMessageDialog(this, "Cannot toggle bookmark at this location.", "Error", - JOptionPane.ERROR_MESSAGE); + error("Cannot toggle bookmark at this location."); } } } @@ -1096,7 +1090,7 @@ public void loadPreferences() { */ public void applyTheme(final String theme) throws IllegalArgumentException { try { - applyTheme(getTheme(theme)); + applyTheme(TextEditor.getTheme(theme)); } catch (final Exception ex) { throw new IllegalArgumentException(ex); } @@ -1114,15 +1108,6 @@ private void applyTheme(final Theme th) throws IllegalArgumentException { GutterUtils.updateIcons(gutter); } - private static Theme getTheme(final String theme) throws IllegalArgumentException { - try { - return Theme - .load(TextEditor.class.getResourceAsStream("/org/fife/ui/rsyntaxtextarea/themes/" + theme + ".xml")); - } catch (final Exception ex) { - throw new IllegalArgumentException(ex); - } - } - public String loadFolders() { return prefService.get(getClass(), FOLDERS_PREFS, System.getProperty("user.home")); } @@ -1192,8 +1177,7 @@ public void actionPerformedImpl(final ActionEvent e, final RTextArea textArea) { if (selection == null) { UIManager.getLookAndFeel().provideErrorFeedback(textArea); } else { - final String link = "https://duckduckgo.com/?q=" + selection.trim().replace(" ", "+"); - openLinkInBrowser(link); + TextEditor.GuiUtils.runSearchQueryInBrowser(EditorPane.this, platformService, selection.trim()); } textArea.requestFocusInWindow(); } @@ -1208,8 +1192,6 @@ JMenuItem getMenuItem() { jmi.setText("Search Web for Selection"); return jmi; } - - } class OpenLinkUnderCursor extends RecordableTextAction { diff --git a/src/main/java/org/scijava/ui/swing/script/FileFunctions.java b/src/main/java/org/scijava/ui/swing/script/FileFunctions.java index c71e5524..f631cd7c 100644 --- a/src/main/java/org/scijava/ui/swing/script/FileFunctions.java +++ b/src/main/java/org/scijava/ui/swing/script/FileFunctions.java @@ -93,13 +93,14 @@ public List extractSourceJar(final String path, final File workspace) final File baseDirectory = new File(workspace, baseName); final List result = new ArrayList<>(); - final JarFile jar = new JarFile(path); - for (final JarEntry entry : Collections.list(jar.entries())) { - final String name = entry.getName(); - if (name.endsWith(".class") || name.endsWith("/")) continue; - final String destination = baseDirectory + name; - copyTo(jar.getInputStream(entry), destination); - result.add(destination); + try (JarFile jar = new JarFile(path)) { + for (final JarEntry entry : Collections.list(jar.entries())) { + final String name = entry.getName(); + if (name.endsWith(".class") || name.endsWith("/")) continue; + final String destination = baseDirectory + name; + copyTo(jar.getInputStream(entry), destination); + result.add(destination); + } } return result; } @@ -223,7 +224,7 @@ public String findSourcePath(final String className, final File workspace) { } if (paths.size() == 1) return new File(workspace, paths.get(0)) .getAbsolutePath(); - final String[] names = paths.toArray(new String[paths.size()]); + //final String[] names = paths.toArray(new String[paths.size()]); final JFileChooser chooser = new JFileChooser(workspace); chooser.setDialogTitle("Choose path"); if (chooser.showOpenDialog(parent) != JFileChooser.APPROVE_OPTION) return null; @@ -304,8 +305,7 @@ public List getResourceList(String url) { final String prefix = url.substring(bang + 2); final int prefixLength = prefix.length(); - try { - final JarFile jar = new JarFile(jarURL); + try (JarFile jar = new JarFile(jarURL)) { final Enumeration e = jar.entries(); while (e.hasMoreElements()) { final JarEntry entry = e.nextElement(); @@ -504,11 +504,11 @@ public void openInGitweb(final File file, final File gitDirectory, final int line) { if (file == null || gitDirectory == null) { - error("No file or git directory"); + parent.error("No file or git directory."); return; } final String url = getGitwebURL(file, gitDirectory, line); - if (url == null) error("Could not get gitweb URL for " + file); + if (url == null) parent.error("Could not get gitweb URL for " + file + "."); else try { parent.getPlatformService().open(new URL(url)); } @@ -599,11 +599,6 @@ protected String stripSuffix(final String string, final String suffix) { return string; } - protected boolean error(final String message) { - JOptionPane.showMessageDialog(parent, message); - return false; - } - /** * @deprecated Use {@link FileUtils#findResources(String, String, File)} * instead. diff --git a/src/main/java/org/scijava/ui/swing/script/FileSystemTreePanel.java b/src/main/java/org/scijava/ui/swing/script/FileSystemTreePanel.java index 7c7cc377..8357981c 100644 --- a/src/main/java/org/scijava/ui/swing/script/FileSystemTreePanel.java +++ b/src/main/java/org/scijava/ui/swing/script/FileSystemTreePanel.java @@ -32,13 +32,10 @@ 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; @@ -53,16 +50,13 @@ import java.util.stream.Collectors; import javax.swing.BorderFactory; -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.UIManager; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.TreePath; @@ -124,14 +118,11 @@ class FileSystemTreePanel extends JPanel { 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); + TextEditor.GuiUtils.warn(this, "Only folders can be dropped into the file tree."); 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) { + if (TextEditor.GuiUtils.confirm(this, "Confirm loading of " + dirs.size() + " folders?", "Confirm?", + "Confirm")) { dirs.forEach(dir -> tree.addRootDirectory(dir.getAbsolutePath(), true)); } }); @@ -199,7 +190,7 @@ public void keyPressed(final KeyEvent ke) { return field; } - private JButton thinButton(final String label) { + private JButton thinButton(final String label, final float factor) { final JButton b = new JButton(label); try { if ("com.apple.laf.AquaLookAndFeel".equals(UIManager.getLookAndFeel().getClass().getName())) { @@ -209,10 +200,9 @@ private JButton thinButton(final String label) { b.setBorder(BorderFactory.createEmptyBorder()); b.setMargin(new Insets(0, 2, 0, 2)); } else { - 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.setMargin(new Insets((int) (insets.top * factor), (int) (insets.left * factor), + (int) (insets.bottom * factor), (int) (insets.right * factor))); } } catch (final Exception ignored) { // do nothing @@ -225,7 +215,7 @@ private JButton thinButton(final String label) { } private JButton addDirectoryButton() { - final JButton add_directory = thinButton("+"); + final JButton add_directory = thinButton("+", .25f); add_directory.setToolTipText("Add a directory"); add_directory.addActionListener(e -> { final String folders = tree.getTopLevelFoldersString(); @@ -257,13 +247,12 @@ private JButton addDirectoryButton() { } private JButton removeDirectoryButton() { - final JButton remove_directory = thinButton("−"); + final JButton remove_directory = thinButton("−", .25f); 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); + TextEditor.GuiUtils.error(this, "Select a top-level folder first."); return; } if (2 == p.getPathCount()) { @@ -271,15 +260,14 @@ private JButton removeDirectoryButton() { tree.getModel().removeNodeFromParent(// (FileSystemTree.Node) p.getLastPathComponent()); } else { - JOptionPane.showMessageDialog(this, "Can only remove top-level folders.", "Invalid Folder", - JOptionPane.ERROR_MESSAGE); + TextEditor.GuiUtils.error(this, "Can only remove top-level folders."); } }); return remove_directory; } private JButton searchOptionsButton() { - final JButton options = thinButton("⋮"); + final JButton options = thinButton("⋮", .05f); options.setToolTipText("Filtering options"); final JPopupMenu popup = new JPopupMenu(); final JCheckBoxMenuItem jcbmi1 = new JCheckBoxMenuItem("Case Sensitive", isCaseSensitive()); @@ -332,8 +320,7 @@ private void addContextualMenuToTree() { jmi.addActionListener(e -> { final TreePath path = tree.getSelectionPath(); if (path == null) { - JOptionPane.showMessageDialog(this, "No items are currently selected.", "Invalid Selection", - JOptionPane.INFORMATION_MESSAGE); + TextEditor.GuiUtils.info(this, "No items are currently selected.", "Invalid Selection"); return; } try { @@ -341,8 +328,7 @@ private void addContextualMenuToTree() { final File f = new File(filepath); Desktop.getDesktop().open((f.isDirectory()) ? f : f.getParentFile()); } catch (final Exception | Error ignored) { - JOptionPane.showMessageDialog(this, "Folder of selected item does not seem to be accessible.", "Error", - JOptionPane.ERROR_MESSAGE); + TextEditor.GuiUtils.error(this, "Folder of selected item does not seem to be accessible."); } }); popup.add(jmi); @@ -350,16 +336,14 @@ private void addContextualMenuToTree() { jmi.addActionListener(e -> { final TreePath path = tree.getSelectionPath(); if (path == null) { - JOptionPane.showMessageDialog(this, "No items are currently selected.", "Invalid Selection", - JOptionPane.INFORMATION_MESSAGE); + TextEditor.GuiUtils.info(this, "No items are currently selected.", "Invalid Selection"); return; } try { final String filepath = (String) ((FileSystemTree.Node) path.getLastPathComponent()).getUserObject(); TextEditor.GuiUtils.openTerminal(new File(filepath)); } catch (final Exception ignored) { - JOptionPane.showMessageDialog(this, "Could not open path in Terminal.", "Error", - JOptionPane.ERROR_MESSAGE); + TextEditor.GuiUtils.error(this, "Could not open path in Terminal."); } }); popup.add(jmi); @@ -387,7 +371,7 @@ private void expandImmediateNodes() { } private void showHelpMsg() { - final String msg = "
" // + 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.

" // @@ -416,7 +400,7 @@ private void showHelpMsg() { + " Display filenames starting with Demo" // + " " // + ""; - JOptionPane.showMessageDialog(this, msg, "File Explorer Pane", JOptionPane.PLAIN_MESSAGE); + TextEditor.GuiUtils.showHTMLDialog(this.getRootPane(), "File Explorer Pane", msg); } private boolean isCaseSensitive() { @@ -437,7 +421,7 @@ private void setRegexEnabled(final boolean b) { searchField.update(); } - private class SearchField extends JTextField { + private class SearchField extends TextEditor.TextFieldWithPlaceholder { private static final long serialVersionUID = 7004232238240585434L; private static final String REGEX_HOLDER = "[?*]"; @@ -445,7 +429,6 @@ private class SearchField extends JTextField { 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()); @@ -453,7 +436,7 @@ private class SearchField extends JTextField { 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()); + setColumns(prefWidth / getColumnWidth()); } catch (final Exception ignored) { // do nothing } @@ -464,21 +447,13 @@ void update() { } @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(); - } + String getPlaceholder() { + final StringBuilder sb = new StringBuilder(DEF_HOLDER); + if (isCaseSensitive()) + sb.append(CASE_HOLDER); + if (isRegexEnabled()) + sb.append(REGEX_HOLDER); + return sb.toString(); } } } 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 02abdac4..83caa620 100644 --- a/src/main/java/org/scijava/ui/swing/script/FindAndReplaceDialog.java +++ b/src/main/java/org/scijava/ui/swing/script/FindAndReplaceDialog.java @@ -151,6 +151,7 @@ final public RSyntaxTextArea getTextArea() { return getTextAreaAsEditorPane(); } + @SuppressWarnings("deprecation") @Override public void show(final boolean replace) { if (replace && restrictToConsole) @@ -166,7 +167,7 @@ public void show(final boolean replace) { searchField.selectAll(); replaceField.selectAll(); getRootPane().setDefaultButton(findNext); - show(); + show(); // cannot call setVisible } private JTextField createField(final String name, final Container container, diff --git a/src/main/java/org/scijava/ui/swing/script/GutterUtils.java b/src/main/java/org/scijava/ui/swing/script/GutterUtils.java index ebbbe022..fe40acea 100644 --- a/src/main/java/org/scijava/ui/swing/script/GutterUtils.java +++ b/src/main/java/org/scijava/ui/swing/script/GutterUtils.java @@ -49,9 +49,12 @@ public class GutterUtils { GutterUtils(final Gutter gutter) { this.gutter = gutter; gutter.setSpacingBetweenLineNumbersAndFoldIndicator(0); + //gutter.setFoldIndicatorStyle(org.fife.ui.rtextarea.FoldIndicatorStyle.MODERN); // DEFAULT + gutter.setShowCollapsedRegionToolTips(true); } - private void updateFoldIcons() { + @SuppressWarnings("unused") + private void updateFoldIcons() { // no longer needed since v3.3.0 int size; try { size = (int) new FoldIndicator(null).getPreferredSize().getWidth(); @@ -63,7 +66,7 @@ private void updateFoldIcons() { final int fontSize = gutter.getLineNumberFont().getSize(); if (size > fontSize) size = fontSize; - gutter.setFoldIcons(new FoldIcon(true, size), new FoldIcon(false, size)); + //gutter.setFoldIcons(new FoldIcon(true, size), new FoldIcon(false, size)); } private ImageIcon getBookmarkIcon() { @@ -94,10 +97,11 @@ private void updateBookmarkIcon() { public static void updateIcons(final Gutter gutter) { final GutterUtils utils = new GutterUtils(gutter); - utils.updateFoldIcons(); + //utils.updateFoldIcons(); utils.updateBookmarkIcon(); } + @SuppressWarnings("unused") private class FoldIcon implements Icon { private final boolean collapsed; diff --git a/src/main/java/org/scijava/ui/swing/script/Main.java b/src/main/java/org/scijava/ui/swing/script/Main.java index 06443c4e..06429baa 100644 --- a/src/main/java/org/scijava/ui/swing/script/Main.java +++ b/src/main/java/org/scijava/ui/swing/script/Main.java @@ -38,6 +38,9 @@ import org.scijava.script.ScriptLanguage; import org.scijava.script.ScriptService; +import com.formdev.flatlaf.FlatDarkLaf; +import com.formdev.flatlaf.FlatLightLaf; + /** * Main entry point for launching the script editor standalone. * @@ -65,6 +68,7 @@ public void windowClosed(final WindowEvent e) { editor.setVisible(true); } public static void main(String[] args) throws Exception { + FlatDarkLaf.setup(); String lang = args.length == 0 ? "Java" : args[0]; launch(lang); } diff --git a/src/main/java/org/scijava/ui/swing/script/OutlineTreePanel.java b/src/main/java/org/scijava/ui/swing/script/OutlineTreePanel.java index 8f54ac76..4fff25fe 100644 --- a/src/main/java/org/scijava/ui/swing/script/OutlineTreePanel.java +++ b/src/main/java/org/scijava/ui/swing/script/OutlineTreePanel.java @@ -67,7 +67,7 @@ class OutlineTreePanel extends JScrollPane { private boolean sorted; private boolean major; - OutlineTreePanel(final TextEditor editor) { + OutlineTreePanel() { super(); fontSize = getFont().getSize(); setViewportView(new UnsupportedLangTree()); 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 551adb31..c7b52cb8 100644 --- a/src/main/java/org/scijava/ui/swing/script/TextEditor.java +++ b/src/main/java/org/scijava/ui/swing/script/TextEditor.java @@ -30,12 +30,15 @@ package org.scijava.ui.swing.script; import java.awt.Color; +import java.awt.Component; import java.awt.Cursor; import java.awt.Desktop; import java.awt.Dimension; import java.awt.Font; +import java.awt.Graphics2D; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; +import java.awt.RenderingHints; import java.awt.Toolkit; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.Transferable; @@ -75,7 +78,9 @@ import java.io.StringReader; import java.io.Writer; import java.net.URL; +import java.net.URLEncoder; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; @@ -103,14 +108,10 @@ import javax.script.ScriptException; import javax.swing.AbstractAction; import javax.swing.AbstractButton; -import javax.swing.Action; -import javax.swing.ActionMap; import javax.swing.BoxLayout; import javax.swing.ButtonGroup; -import javax.swing.InputMap; import javax.swing.JButton; import javax.swing.JCheckBoxMenuItem; -import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JFrame; import javax.swing.JLabel; @@ -125,6 +126,7 @@ import javax.swing.JSplitPane; import javax.swing.JTabbedPane; import javax.swing.JTextArea; +import javax.swing.JTextField; import javax.swing.JTextPane; import javax.swing.JTree; import javax.swing.KeyStroke; @@ -142,6 +144,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.fife.ui.rtextarea.ClipboardHistory; import org.fife.ui.rtextarea.Macro; @@ -149,7 +152,6 @@ import org.fife.ui.rtextarea.RTextAreaEditorKit; import org.fife.ui.rtextarea.RTextAreaEditorKit.SetReadOnlyAction; import org.fife.ui.rtextarea.RTextAreaEditorKit.SetWritableAction; -import org.fife.ui.rtextarea.RecordableTextAction; import org.scijava.Context; import org.scijava.app.AppService; import org.scijava.batch.BatchService; @@ -186,6 +188,8 @@ import org.scijava.util.Types; import org.scijava.widget.FileWidget; +import com.formdev.flatlaf.FlatLaf; + /** * A versatile script editor for SciJava applications. *

@@ -302,6 +306,7 @@ public class TextEditor extends JFrame implements ActionListener, private DragSource dragSource; private boolean layoutLoading = true; private OutlineTreePanel sourceTreePanel; + protected final CommandPalette cmdPalette; public static final ArrayList instances = new ArrayList<>(); public static final ArrayList contexts = new ArrayList<>(); @@ -319,7 +324,7 @@ public TextEditor(final Context context) { tree = new FileSystemTree(log); final JTabbedPane sideTabs = GuiUtils.getJTabbedPane(); sideTabs.addTab("File Explorer", new FileSystemTreePanel(tree, context)); - sideTabs.addTab("Outline", sourceTreePanel = new OutlineTreePanel(this)); + sideTabs.addTab("Outline", sourceTreePanel = new OutlineTreePanel()); body = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, sideTabs, tabbed); // These items are dynamic and need to be initialized before EditorPane creation @@ -498,6 +503,9 @@ public TextEditor(final Context context) { toolsMenu = new JMenu("Tools"); toolsMenu.setMnemonic(KeyEvent.VK_O); + cmdPalette = new CommandPalette(this); + cmdPalette.install(toolsMenu); + GuiUtils.addMenubarSeparator(toolsMenu, "Imports:"); addImport = addToMenu(toolsMenu, "Add Import...", 0, 0); addImport.setMnemonic(KeyEvent.VK_I); @@ -562,16 +570,8 @@ public TextEditor(final Context context) { tabsMenu.add(jcmi2); final JMenuItem mi = new JMenuItem("Reset Layout..."); mi.addActionListener(e -> { - final int choice = JOptionPane.showConfirmDialog(TextEditor.this,// - "Reset Location of Console and File Explorer?", - "Reset Layout?", JOptionPane.OK_CANCEL_OPTION); - if (JOptionPane.OK_OPTION == choice) { - body.setDividerLocation(.2d); - getTab().setOrientation(JSplitPane.VERTICAL_SPLIT); - getTab().setDividerLocation((incremental) ? .7d : .75d); - if (incremental) - getTab().getScreenAndPromptSplit().setDividerLocation(.5d); - getTab().setREPLVisible(incremental); + if (confirm("Reset Location of Console and File Explorer?", "Reset Layout?", "Reset")) { + resetLayout(); jcmi1.setSelected(true); jcmi2.setSelected(true); } @@ -690,10 +690,8 @@ public TextEditor(final Context context) { warn("None of the dropped file(s) seems parseable."); 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) { + if (filteredFiles.size() < 10 + || confirm("Confirm loading of " + filteredFiles.size() + " items?", "Confirm?", "Load")) { filteredFiles.forEach(f -> open(f)); } }); @@ -738,10 +736,7 @@ public TextEditor(final Context context) { } } // Ask: - final int choice = JOptionPane.showConfirmDialog(TextEditor.this, - "Really try to open file " + name + " in a tab?", "Confirm", - JOptionPane.OK_CANCEL_OPTION); - if (JOptionPane.OK_OPTION == choice) { + if (confirm("Really try to open file " + name + " in a tab?", "Confirm", "Open")) { open(f); } }); @@ -767,6 +762,7 @@ public void windowClosing(final WindowEvent e) { } dragSource = null; getTab().destroy(); + cmdPalette.dispose(); dispose(); } }); @@ -823,7 +819,9 @@ public void componentResized(final ComponentEvent e) { final EditorPane editorPane = getEditorPane(); // If dark L&F and using the default theme, assume 'dark' theme - applyTheme((GuiUtils.isDarkLaF() && "default".equals(editorPane.themeName())) ? "dark" : editorPane.themeName()); + applyTheme((GuiUtils.isDarkLaF() && "default".equals(editorPane.themeName())) ? "dark" : editorPane.themeName(), + true); + // Ensure font sizes are consistent across all panels setFontSize(getEditorPane().getFontSize()); // Ensure menu commands are up-to-date @@ -833,6 +831,16 @@ public void componentResized(final ComponentEvent e) { editorPane.requestFocus(); } + private void resetLayout() { + body.setDividerLocation(.2d); + getTab().setOrientation(JSplitPane.VERTICAL_SPLIT); + getTab().setDividerLocation((incremental) ? .7d : .75d); + if (incremental) + getTab().getScreenAndPromptSplit().setDividerLocation(.5d); + getTab().setREPLVisible(incremental); + pack(); + } + private void assembleEditMenu() { // requires an existing instance of an EditorPane final int ctrl = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(); @@ -845,7 +853,7 @@ private void assembleEditMenu() { copy = addToMenu(editMenu, "Copy", KeyEvent.VK_C, ctrl); addMappedActionToMenu(editMenu, "Copy as Styled Text", EditorPaneActions.rstaCopyAsStyledTextAction, false); paste = addToMenu(editMenu, "Paste", KeyEvent.VK_V, ctrl); - addMappedActionToMenu(editMenu, "Paste from History...", EditorPaneActions.clipboardHistoryAction, true); + addMappedActionToMenu(editMenu, "Paste History...", EditorPaneActions.clipboardHistoryAction, true); GuiUtils.addMenubarSeparator(editMenu, "Find:"); find = addToMenu(editMenu, "Find/Replace...", KeyEvent.VK_F, ctrl); find.setMnemonic(KeyEvent.VK_F); @@ -854,12 +862,12 @@ private void assembleEditMenu() { findPrevious = addToMenu(editMenu, "Find Previous", KeyEvent.VK_F3, shift); findPrevious.setMnemonic(KeyEvent.VK_P); - GuiUtils.addMenubarSeparator(editMenu, "Goto:"); - gotoLine = addToMenu(editMenu, "Goto Line...", KeyEvent.VK_G, ctrl); + GuiUtils.addMenubarSeparator(editMenu, "Go To:"); + gotoLine = addToMenu(editMenu, "Go to Line...", KeyEvent.VK_G, ctrl); gotoLine.setMnemonic(KeyEvent.VK_G); - addMappedActionToMenu(editMenu, "Goto Matching Bracket", EditorPaneActions.rstaGoToMatchingBracketAction, false); + addMappedActionToMenu(editMenu, "Go to Matching Bracket", EditorPaneActions.rstaGoToMatchingBracketAction, false); - final JMenuItem gotoType = new JMenuItem("Goto Type..."); + final JMenuItem gotoType = new JMenuItem("Go to Type..."); // we could retrieve the accelerator from paneactions but this may not work, if e.g., an // unsupported syntax (such IJM) has been opened at startup, so we'll just specify it manually //gotoType.setAccelerator(getEditorPane().getPaneActions().getAccelerator("GoToType")); @@ -1010,74 +1018,27 @@ private File getMacroFile(final boolean openOtherwiseSave) { } private void displayRecordableMap() { - final ActionMap inputMap = getTextArea().getActionMap(); - Object[] keys = inputMap.allKeys(); - final ArrayList lines = new ArrayList<>(); - if (keys != null) { - for (int i = 0; i < keys.length; i++) { - final Object obj = inputMap.get(keys[i]); - if (!(obj instanceof RecordableTextAction)) - continue; - String objString = (String) ((RecordableTextAction) obj).getValue(Action.NAME); - objString = cleanseActionDescription(objString); - lines.add("

  • " + capitalize(objString) + "
  • "); - } - Collections.sort(lines, String.CASE_INSENSITIVE_ORDER); - } - showHTMLDialog("Script Editor Recordable Actions/Events", "
      " + String.join("", lines) + ""); + displayMap(cmdPalette.getRecordableActions(), "Script Editor Recordable Actions/Events"); } private void displayKeyMap() { - final InputMap inputMap = getTextArea().getInputMap(JComponent.WHEN_FOCUSED); - final KeyStroke[] keys = inputMap.allKeys(); + displayMap(cmdPalette.getShortcuts(), "Script Editor Shortcuts"); + } + + private void displayMap(final Map map, final String windowTitle) { final ArrayList lines = new ArrayList<>(); - if (keys != null) { - for (int i = 0; i < keys.length; i++) { - final KeyStroke key = keys[i]; - String keyString = key.toString().replace("pressed", ""); - if (keyString.startsWith("typed ")) - continue; // ignore 'regular keystrokes' - final Object obj = inputMap.get(key); - String objString; - if (obj instanceof AbstractAction) { - objString = (String) ((AbstractAction) obj).getValue(Action.NAME); - } else if (obj instanceof AbstractButton) { - objString = ((AbstractButton) obj).getText(); - } else { - objString = obj.toString(); - } - objString = cleanseActionDescription(objString); - keyString = keyString.replace("ctrl", "Ctrl"); - keyString = keyString.replace("shift", "Shift"); - keyString = keyString.replace("alt", "Alt"); - lines.add("" + capitalize(objString) - + "" + keyString + ""); - } - Collections.sort(lines, String.CASE_INSENSITIVE_ORDER); - } - final String prefix = "" // + map.forEach( (cmd, key) -> { + lines.add(""); + }); + final String prefix = "
      " + cmd + + "" + key + "
      " // + "" // + "" // - + "" // - + "" // + + "" // + + "" // + ""; // - final String suffix = "
      ActionShortcutActionShortcut
      "; - showHTMLDialog("Script Editor Shortcuts", prefix + String.join("", lines) + suffix); - } - - private String cleanseActionDescription(String actionId) { - if (actionId.startsWith("RTA.")) - actionId = actionId.substring(4); - else if (actionId.startsWith("RSTA.")) - actionId = actionId.substring(5); - if (actionId.endsWith("Action")) - actionId = actionId.substring(0, actionId.length() - 6); - actionId = actionId.replace("-", " "); - return actionId.replaceAll("([A-Z])", " $1"); // CamelCase to Camel Case - } - - private String capitalize(final String string) { - return string.substring(0, 1).toUpperCase() + string.substring(1); + final String suffix = ""; + showHTMLDialog(windowTitle, prefix + String.join("", lines) + suffix); } private class DragAndDrop implements DragSourceListener, DragGestureListener { @@ -1600,16 +1561,12 @@ public boolean handleUnsavedChanges(final boolean beforeCompiling) { save(); return true; } - - switch (JOptionPane.showConfirmDialog(this, "Do you want to save changes?")) { - case JOptionPane.NO_OPTION: - // Compiled languages should not progress if their source is unsaved - return !beforeCompiling; - case JOptionPane.YES_OPTION: - if (save()) return true; + if (GuiUtils.confirm(this, "Do you want to save changes?", "Save Changes?", "Save")) { + return save(); + } else { + // Compiled languages should not progress if their source is unsaved + return !beforeCompiling; } - - return false; } private boolean isJava(final ScriptLanguage language) { @@ -1832,7 +1789,7 @@ private JMenu applyThemeMenu() { return; } final JRadioButtonMenuItem item = new JRadioButtonMenuItem(k); - item.setActionCommand(v); + item.setActionCommand(v); // needed for #updateThemeControls() themeRadioGroup.add(item); item.addActionListener(e -> { try { @@ -1861,8 +1818,13 @@ public void applyTheme(final String theme) throws IllegalArgumentException { applyTheme(theme, true); } - private void applyTheme(final String theme, final boolean updateUI) throws IllegalArgumentException { + private void applyTheme(final String theme, final boolean updateMenus) throws IllegalArgumentException { try { + final Theme th = getTheme(theme); + if (th == null) { + writeError("Unrecognized theme ignored: '" + theme + "'"); + return; + } for (int i = 0; i < tabbed.getTabCount(); i++) { getEditorPane(i).applyTheme(theme); } @@ -1874,7 +1836,16 @@ private void applyTheme(final String theme, final boolean updateUI) throws Illeg throw new IllegalArgumentException(ex); } activeTheme = theme; - if (updateUI) updateThemeControls(theme); + if (updateMenus) updateThemeControls(theme); + } + + static Theme getTheme(final String theme) throws IllegalArgumentException { + try { + return Theme + .load(TextEditor.class.getResourceAsStream("/org/fife/ui/rsyntaxtextarea/themes/" + theme + ".xml")); + } catch (final Exception ex) { + throw new IllegalArgumentException(ex); + } } private void updateThemeControls(final String theme) { @@ -1972,9 +1943,7 @@ public void findOrReplace(final boolean doReplace) { } public void gotoLine() { - final String line = - JOptionPane.showInputDialog(this, "Enter line number:", "Goto Line", - JOptionPane.QUESTION_MESSAGE); + final String line = GuiUtils.getString(this, "Enter line number:", "Goto Line"); if (line == null) return; try { gotoLine(Integer.parseInt(line)); @@ -2019,8 +1988,8 @@ 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) { + ; + if (confirm("Delete all bookmarks?", "Confirm Deletion?", "Delete")) { bookmarks.forEach(bk -> bk.tab.editorPane.toggleBookmark(bk.getLineNumber())); } } @@ -2209,11 +2178,11 @@ public void saveAs(final String path) { public boolean saveAs(final String path, final boolean askBeforeReplacing) { final File file = new File(path); - if (file.exists() && - askBeforeReplacing && - JOptionPane.showConfirmDialog(this, "Do you want to replace " + path + - "?", "Replace " + path + "?", JOptionPane.YES_NO_OPTION) != JOptionPane.YES_OPTION) return false; - if (!write(file)) return false; + if (file.exists() && askBeforeReplacing + && confirm("Do you want to replace " + path + "?", "Replace " + path + "?", "Replace")) + return false; + if (!write(file)) + return false; setEditorPaneFileName(file); openRecent.add(path); return true; @@ -2259,10 +2228,9 @@ public boolean makeJar(final boolean includeSources) { final File selectedFile = uiService.chooseFile(file, FileWidget.SAVE_STYLE); if (selectedFile == null) return false; - if (selectedFile.exists() && - JOptionPane.showConfirmDialog(this, "Do you want to replace " + - selectedFile + "?", "Replace " + selectedFile + "?", - JOptionPane.YES_NO_OPTION) != JOptionPane.YES_OPTION) return false; + if (selectedFile.exists() + && confirm("Do you want to replace " + selectedFile + "?", "Replace " + selectedFile + "?", "Replace")) + return false; try { makeJar(selectedFile, includeSources); return true; @@ -2970,11 +2938,7 @@ public void compile() { private String getSelectedTextOrAsk(final String msg, final String title) { String selection = getTextArea().getSelectedText(); if (selection == null || selection.indexOf('\n') >= 0) { - selection = JOptionPane.showInputDialog(this, - msg + "\nAlternatively, select a class declaration and re-run.", title, - JOptionPane.QUESTION_MESSAGE); - if (selection == null) - return null; + return GuiUtils.getString(this, msg + "\nAlternatively, select a class declaration and re-run.", title); } return selection; } @@ -2990,7 +2954,6 @@ private static void append(final JTextArea textArea, final String text) { textArea.insert(text, length); textArea.setCaretPosition(length); } - public void markCompileStart() { markCompileStart(true); @@ -3236,7 +3199,7 @@ public void run() { if (matches.isEmpty()) { if (confirm("No info found for: '" + text + "'.\nSearch for it on the web?", "Search the Web?", "Search")) { - openURL("https://duckduckgo.com/?q=" + text.trim().replace(" ", "+")); + GuiUtils.runSearchQueryInBrowser(TextEditor.this, getPlatformService(), text.trim()); } return; } @@ -3269,11 +3232,7 @@ public void run() { gridbag.setConstraints(link, c); panel.add(link); link.addActionListener(event -> { - try { - platformService.open(new URL(url)); - } catch (final Exception e) { - e.printStackTrace(); - } + GuiUtils.openURL(TextEditor.this, platformService, url); }); } c.gridy += 1; @@ -3340,49 +3299,23 @@ public void writeError(String message) { } void error(final String message) { - JOptionPane.showMessageDialog(this, message, "Error", JOptionPane.ERROR_MESSAGE); + GuiUtils.error(this, message); } void warn(final String message) { - JOptionPane.showMessageDialog(this, message, "Warning", JOptionPane.WARNING_MESSAGE); + GuiUtils.warn(this, message); } void info(final String message, final String title) { - JOptionPane.showMessageDialog(this, message, title, JOptionPane.INFORMATION_MESSAGE); + GuiUtils.info(this, message, title); } boolean confirm(final String message, final String title, final String yesButtonLabel) { - return JOptionPane.showOptionDialog(this, message, title, JOptionPane.YES_NO_OPTION, - JOptionPane.QUESTION_MESSAGE, null, // no custom Icon - new String[] { yesButtonLabel, "Cancel" }, // titles of buttons - yesButtonLabel) == JOptionPane.OK_OPTION; + return GuiUtils.confirm(this, message, title, yesButtonLabel); } void showHTMLDialog(final String title, final String htmlContents) { - final JTextPane f = new JTextPane(); - f.setContentType("text/html"); - f.setEditable(false); - f.setBackground(null); - f.setBorder(null); - f.setText(htmlContents); - f.setCaretPosition(0); - final JScrollPane sp = new JScrollPane(f); - final JOptionPane pane = new JOptionPane(sp); - final JDialog dialog = pane.createDialog(this, title); - dialog.setResizable(true); - dialog.pack(); - pane.setPreferredSize( - new Dimension( - (int) Math.min(getWidth() * .5, sp.getPreferredSize().getWidth() + (3 * sp.getVerticalScrollBar().getWidth())), - (int) Math.min(getHeight() * .6, pane.getPreferredSize().getHeight()))); - dialog.pack(); - pane.addPropertyChangeListener(JOptionPane.VALUE_PROPERTY, ignored -> { - dialog.dispose(); - }); - dialog.pack(); - dialog.setModal(false); - dialog.setLocationRelativeTo(this); - dialog.setVisible(true); + GuiUtils.showHTMLDialog(this, title, htmlContents); } public void handleException(final Throwable e) { @@ -3718,6 +3651,7 @@ private void appendPreferences(final JMenu menu) { menu.add(item); item.addActionListener(e -> { if (confirm("Reset preferences to defaults? (a restart may be required)", "Reset?", "Reset")) { + resetLayout(); prefService.clear(EditorPane.class); prefService.clear(TextEditor.class); write("Script Editor: Preferences Reset. Restart is recommended\n"); @@ -3777,30 +3711,30 @@ private JMenu helpMenu() { private JMenuItem helpMenuItem(final String label, final String url) { final JMenuItem item = new JMenuItem(label); - item.addActionListener(e -> openURL(url)); + item.addActionListener(e -> GuiUtils.openURL(TextEditor.this, platformService, 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."); - } - } - protected void applyConsolePopupMenu(final JTextArea textArea) { final JPopupMenu popup = new JPopupMenu(); textArea.setComponentPopupMenu(popup); - JMenuItem jmi = new JMenuItem("Search " + ((textArea == errorScreen) ? "Erros..." : "Outputs...")); + final String scope = ((textArea == errorScreen) ? "Errors..." : "Outputs..."); + JMenuItem jmi = new JMenuItem("Search " + scope); popup.add(jmi); jmi.addActionListener(e -> { findDialog.setLocationRelativeTo(this); findDialog.setRestrictToConsole(true); final String text = textArea.getSelectedText(); if (text != null) findDialog.setSearchPattern(text); + // Ensure the right pane is visible in case search is being + // triggered by CmdPalette + if (textArea == errorScreen && !getTab().showingErrors) + getTab().showErrors(); + else if (getTab().showingErrors) + getTab().showOutput(); findDialog.show(false); }); + cmdPalette.register(jmi, scope); jmi = new JMenuItem("Search Script for Selected Text..."); popup.add(jmi); jmi.addActionListener(e -> { @@ -3843,10 +3777,12 @@ protected void applyConsolePopupMenu(final JTextArea textArea) { jmi = new JMenuItem("Clear Highlights"); popup.add(jmi); jmi.addActionListener(e -> textArea.getHighlighter().removeAllHighlights()); + cmdPalette.register(jmi, scope); popup.addSeparator(); final JCheckBoxMenuItem jmc = new JCheckBoxMenuItem("Wrap Lines"); popup.add(jmc); jmc.addActionListener( e -> textArea.setLineWrap(jmc.isSelected())); + cmdPalette.register(jmc, scope); } private static Collection assembleFlatFileCollection(final Collection collection, final File[] files) { @@ -3863,9 +3799,84 @@ else if (file.isDirectory()) } protected static class GuiUtils { + private GuiUtils() { } + static void error(final Component parent, final String message) { + JOptionPane.showMessageDialog(parent, message, "Error", JOptionPane.ERROR_MESSAGE); + } + + static void warn(final Component parent, final String message) { + JOptionPane.showMessageDialog(parent, message, "Warning", JOptionPane.WARNING_MESSAGE); + } + + static void info(final Component parent, final String message, final String title) { + JOptionPane.showMessageDialog(parent, message, title, JOptionPane.INFORMATION_MESSAGE); + } + + static boolean confirm(final Component parent, final String message, final String title, + final String yesButtonLabel) { + return JOptionPane.showConfirmDialog(parent, message, title, JOptionPane.YES_NO_OPTION) == + JOptionPane.YES_OPTION; + } + + static void showHTMLDialog(final Component parent, final String title, final String htmlContents) { + final JTextPane f = new JTextPane(); + f.setContentType("text/html"); + f.setEditable(false); + f.setBackground(null); + f.setBorder(null); + f.setText(htmlContents); + f.setCaretPosition(0); + final JScrollPane sp = new JScrollPane(f); + final JOptionPane pane = new JOptionPane(sp); + final JDialog dialog = pane.createDialog(parent, title); + dialog.setResizable(true); + dialog.pack(); + dialog.setPreferredSize( + new Dimension( + (int) Math.min(parent.getWidth() * .5, pane.getPreferredSize().getWidth() + (3 * sp.getVerticalScrollBar().getWidth())), + (int) Math.min(parent.getHeight() * .8, pane.getPreferredSize().getHeight()))); + dialog.pack(); + pane.addPropertyChangeListener(JOptionPane.VALUE_PROPERTY, ignored -> { + dialog.dispose(); + }); + dialog.setModal(false); + dialog.setLocationRelativeTo(parent); + dialog.setVisible(true); + } + + static String getString(final Component parent, final String message, final String title) { + return (String) JOptionPane.showInputDialog(parent, message, title, JOptionPane.QUESTION_MESSAGE); + } + + static void runSearchQueryInBrowser(final Component parentComponent, final PlatformService platformService, + final String query) { + String url; + try { + url = "https://forum.image.sc/search?q=" + URLEncoder.encode(query, StandardCharsets.UTF_8.toString()); + } catch (final Exception ignored) { + url = query.trim().replace(" ", "%20"); + } + openURL(parentComponent, platformService, url); + } + + static void openURL(final Component parentComponent, final PlatformService platformService, final String url) { + try { + platformService.open(new URL(url)); + } catch (final Exception ignored) { + // Error message with selectable text + final JTextPane f = new JTextPane(); + f.setContentType("text/html"); + f.setText("Web page could not be open. Please visit
      " + url + "
      using your web browser."); + f.setEditable(false); + f.setBackground(null); + f.setBorder(null); + JOptionPane.showMessageDialog(parentComponent, f, "Error", JOptionPane.ERROR_MESSAGE); + } + } + static void openTerminal(final File pwd) throws IOException, InterruptedException { final String[] wrappedCommand; final File dir = (pwd.isDirectory()) ? pwd : pwd.getParentFile(); @@ -3888,26 +3899,14 @@ static void openTerminal(final File pwd) throws IOException, InterruptedExceptio } static void addMenubarSeparator(final JMenu menu, final String header) { - if (menu.getMenuComponentCount() > 1) { + if (menu.getMenuComponentCount() > 0) { menu.addSeparator(); } try { - // on Aqua L&F (and other L&Fs on the Mac!?) the label is never rendered. It - // seems - // only menu items with an actual actionlistener are registered on the menubar!? - if (PlatformUtils.isMac()) { - final JMenuItem label = new JMenuItem("↓ " + header); - label.setEnabled(false); - label.setFont(label.getFont().deriveFont(Font.ITALIC)); // ignored - label.addActionListener(e -> label.setActionCommand("dummy-menu-bar-separator")); - menu.add(label); - } else { - final JLabel label = new JLabel(header); - // label.setHorizontalAlignment(SwingConstants.LEFT); - label.setEnabled(false); - label.setForeground(getDisabledComponentColor()); - menu.add(label); - } + final JLabel label = new JLabel(" "+ header); + label.setEnabled(false); + label.setForeground(getDisabledComponentColor()); + menu.add(label); } catch (final Exception ignored) { // do nothing } @@ -3934,9 +3933,12 @@ static Color getDisabledComponentColor() { } static boolean isDarkLaF() { + return FlatLaf.isLafDark() || isDark(new JLabel().getBackground()); + } + + static boolean isDark(final Color c) { // 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; + return (c.getRed() * 0.299 + c.getGreen() * 0.587 + c.getBlue() * 0.114) < 186; } static void collapseAllTreeNodes(final JTree tree) { @@ -3994,4 +3996,38 @@ else if (newIndex >= pane.getTabCount()) } } + static class TextFieldWithPlaceholder extends JTextField { + + private static final long serialVersionUID = 1L; + private String placeholder; + + void setPlaceholder(final String placeholder) { + this.placeholder = placeholder; + update(getGraphics()); + } + + Font getPlaceholderFont() { + return getFont().deriveFont(Font.ITALIC); + } + + String getPlaceholder() { + return placeholder; + } + + @Override + protected void paintComponent(final java.awt.Graphics g) { + super.paintComponent(g); + if (getText().isEmpty()) { + final Graphics2D g2 = (Graphics2D) g.create(); + g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + g2.setColor(getDisabledTextColor()); + g2.setFont(getPlaceholderFont()); + g2.drawString(getPlaceholder(), getInsets().left, + g2.getFontMetrics().getHeight() + getInsets().top - getInsets().bottom); + g2.dispose(); + } + } + + } + } 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 2e89e609..a3cc8069 100644 --- a/src/main/java/org/scijava/ui/swing/script/TextEditorTab.java +++ b/src/main/java/org/scijava/ui/swing/script/TextEditorTab.java @@ -158,6 +158,7 @@ public void dragEnter(final DropTargetDragEvent e) { bc.fill = GridBagConstraints.HORIZONTAL; runit = new JButton("Run"); runit.setToolTipText("Control+R, F5, or F11"); + textEditor.cmdPalette.register(runit, "Interpreter"); runit.addActionListener(ae -> textEditor.runText()); bottom.add(runit, bc); @@ -175,6 +176,7 @@ public void dragEnter(final DropTargetDragEvent e) { bc.gridx = 3; incremental = new JCheckBox("REPL"); + textEditor.cmdPalette.register(incremental, "Interpreter"); incremental.setEnabled(true); incremental.setSelected(false); bottom.add(incremental, bc); @@ -202,7 +204,8 @@ public void dragEnter(final DropTargetDragEvent e) { if (showingErrors) editorPane.getErrorHighlighter().reset(); }); bottom.add(clear, bc); - + textEditor.cmdPalette.register(clear, "Console"); + bc.gridx = 7; switchSplit = new JButton(RIGHT_ARROW); switchSplit.setToolTipText("Switch location");