/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */
/*
Part of the Processing project - http://processing.org
Copyright (c) 2012-23 The Processing Foundation
Copyright (c) 2004-12 Ben Fry and Casey Reas
Copyright (c) 2001-04 Massachusetts Institute of Technology
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License version 2
as published by the Free Software Foundation.
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, write to the Free Software Foundation,
Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package processing.app.ui;
import com.formdev.flatlaf.util.SystemInfo;
import processing.app.*;
import processing.app.Formatter;
import processing.app.contrib.ContributionManager;
import processing.app.laf.PdeMenuItemUI;
import processing.app.syntax.*;
import processing.core.PApplet;
import processing.utils.SketchException;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.event.*;
import javax.swing.plaf.basic.BasicSplitPaneDivider;
import javax.swing.plaf.basic.BasicSplitPaneUI;
import javax.swing.text.BadLocationException;
import javax.swing.text.Element;
import javax.swing.text.View;
import javax.swing.text.ViewFactory;
import javax.swing.text.html.HTMLEditorKit;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.CannotUndoException;
import javax.swing.undo.CompoundEdit;
import javax.swing.undo.UndoManager;
import java.awt.*;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.event.*;
import java.awt.print.PageFormat;
import java.awt.print.PrinterException;
import java.awt.print.PrinterJob;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.List;
import java.util.Timer;
import java.util.stream.Collectors;
/**
* Main editor panel for the Processing Development Environment.
*/
public abstract class Editor extends JFrame implements RunnerListener {
protected Base base;
protected EditorState state;
protected Mode mode;
// There may be certain gutter sizes that cause text bounds
// inside the console to be calculated incorrectly.
// 45 seems to work but change with caution. [sampottinger 191107]
static public final int LEFT_GUTTER = Toolkit.zoom(45);
static public final int RIGHT_GUTTER = Toolkit.zoom(12);
static public final int GUTTER_MARGIN = Toolkit.zoom(5);
protected MarkerColumn errorColumn;
// Otherwise, if the window is resized with the message label
// set to blank, its preferredSize() will be fuckered
static protected final String EMPTY =
" " +
" " +
" ";
private PageFormat pageFormat;
private PrinterJob printerJob;
// File and sketch menus for re-inserting items
private JMenu fileMenu;
private JMenu sketchMenu;
protected JPanel spacer = new JPanel();
protected EditorHeader header;
protected EditorToolbar toolbar;
protected JEditTextArea textarea;
protected EditorStatus status;
protected JSplitPane splitPane;
protected EditorFooter footer;
protected EditorConsole console;
protected ErrorTable errorTable;
// currently opened program
protected Sketch sketch;
// runtime information and window placement
private Point sketchWindowLocation;
// undo fellers
private JMenuItem undoItem, redoItem;
protected UndoAction undoAction;
protected RedoAction redoAction;
protected CutAction cutAction;
protected CopyAction copyAction;
protected CopyAsHtmlAction copyAsHtmlAction;
protected PasteAction pasteAction;
/** Menu Actions updated on the opening of the edit menu. */
protected List editMenuUpdatable = new ArrayList();
protected FindNextAction findNextAction;
protected FindPreviousAction findPreviousAction;
/** The currently selected tab's undo manager and caret positions*/
private UndoManager undo;
// maintain caret position during undo operations
private Stack caretUndoStack = new Stack();
private Stack caretRedoStack = new Stack();
// Used internally for every edit. Groups hot key event text manipulations
// and multi-character inputs into a single undo objects.
private CompoundEdit compoundEdit;
// timer to decide when to group characters into an undo
private final Timer timer;
private TimerTask endUndoEvent;
// true if inserting text, false if removing text
private boolean isInserting;
private FindReplace find;
JMenu toolsMenu;
JMenu modePopup;
JMenu developMenu;
protected List problems = Collections.emptyList();
protected Editor(final Base base, String path, final EditorState state,
final Mode mode) throws EditorException {
super("Processing", state.getConfig());
if (Platform.isLinux()) {
// If the frame is already displayable, dispose it to allow undecorated change
if (isDisplayable()) {
dispose();
}
try {
setUndecorated(true);
} catch (IllegalComponentStateException e) {
System.err.println("Could not set undecorated: " + e.getMessage());
}
getRootPane().setWindowDecorationStyle(JRootPane.FRAME);
}
this.base = base;
this.state = state;
this.mode = mode;
// Make sure Base.getActiveEditor() never returns null
base.checkFirstEditor(this);
// This is a Processing window. Get rid of that ugly ass coffee cup.
Toolkit.setIcon(this);
// add listener to handle window close box hit event
addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
base.handleClose(Editor.this, false);
}
});
// don't close the window when clicked, the app will take care
// of that via the handleQuitInternal() methods
// https://download.processing.org/bugzilla/440.html
setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
// When bringing a window to front, let the Base know
addWindowListener(new WindowAdapter() {
public void windowActivated(WindowEvent e) {
base.handleActivated(Editor.this);
fileMenu.insert(Recent.getMenu(), 2);
Toolkit.setMenuMnemsInside(fileMenu);
mode.insertImportMenu(sketchMenu);
Toolkit.setMenuMnemsInside(sketchMenu);
mode.insertToolbarRecentMenu();
}
public void windowDeactivated(WindowEvent e) {
// TODO call handleActivated(null)? or do we run the risk of the
// deactivate call for old window being called after the activate?
fileMenu.remove(Recent.getMenu());
mode.removeImportMenu(sketchMenu);
mode.removeToolbarRecentMenu();
}
});
timer = new Timer();
buildMenuBar();
JPanel contentPain = new JPanel();
setContentPane(contentPain);
contentPain.setLayout(new BorderLayout());
Box box = Box.createVerticalBox();
Box upper = Box.createVerticalBox();
if(Platform.isMacOS() && SystemInfo.isMacFullWindowContentSupported) {
getRootPane().putClientProperty( "apple.awt.fullWindowContent", true );
getRootPane().putClientProperty( "apple.awt.transparentTitleBar", true );
spacer.setPreferredSize(new Dimension(1, Toolkit.zoom(18)));
spacer.setMinimumSize(new Dimension(1, Toolkit.zoom(18)));
spacer.setAlignmentX(Component.LEFT_ALIGNMENT);
box.add(spacer);
}
rebuildModePopup();
toolbar = createToolbar();
upper.add(toolbar);
header = createHeader();
upper.add(header);
textarea = createTextArea();
textarea.setRightClickPopup(new TextAreaPopup());
textarea.setHorizontalOffset(JEditTextArea.leftHandGutter);
{ // Hack: add Numpad Slash as alternative shortcut for Comment/Uncomment
KeyStroke keyStroke =
KeyStroke.getKeyStroke(KeyEvent.VK_DIVIDE, Toolkit.SHORTCUT_KEY_MASK);
final String ACTION_KEY = "COMMENT_UNCOMMENT_ALT";
textarea.getInputMap().put(keyStroke, ACTION_KEY);
textarea.getActionMap().put(ACTION_KEY, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
handleCommentUncomment();
}
});
}
textarea.addCaretListener(e -> updateEditorStatus());
footer = createFooter();
// build the central panel with the text area & error marker column
JPanel editorPanel = new JPanel(new BorderLayout());
errorColumn = new MarkerColumn(this, textarea.getMinimumSize().height);
editorPanel.add(errorColumn, BorderLayout.EAST);
textarea.setBounds(0, 0, errorColumn.getX() - 1, textarea.getHeight());
editorPanel.add(textarea);
upper.add(editorPanel);
splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, upper, footer);
// disable this because it hides the message area
// https://github.com/processing/processing/issues/784
splitPane.setOneTouchExpandable(false);
// repaint child panes while resizing
splitPane.setContinuousLayout(true);
// if window increases in size, give all the increase to
// the textarea in the upper pane
splitPane.setResizeWeight(1D);
// remove any ugly borders added by PLAFs (doesn't fix everything)
splitPane.setBorder(null);
// remove an ugly border around anything in a SplitPane !$*&!%
UIManager.getDefaults().put("SplitPane.border", BorderFactory.createEmptyBorder());
// set the height per our gui design
splitPane.setDividerSize(EditorStatus.HIGH);
// override the look of the SplitPane so that it's identical across OSes
splitPane.setUI(new BasicSplitPaneUI() {
public BasicSplitPaneDivider createDefaultDivider() {
status = new EditorStatus(this, Editor.this);
return status;
}
@Override
public void finishDraggingTo(int location) {
super.finishDraggingTo(location);
// JSplitPane issue: if you only make the lower component visible at
// the last minute, its minimum size is ignored.
if (location > splitPane.getMaximumDividerLocation()) {
splitPane.setDividerLocation(splitPane.getMaximumDividerLocation());
}
}
});
box.add(splitPane);
contentPain.add(box);
// end an undo-chunk any time the caret moves unless it's when text is edited
textarea.addCaretListener(new CaretListener() {
String lastText = textarea.getText();
public void caretUpdate(CaretEvent e) {
String newText = textarea.getText();
if (lastText.equals(newText) && isDirectEdit() && !textarea.isOverwriteEnabled()) {
endTextEditHistory();
}
lastText = newText;
}
});
textarea.addKeyListener(toolbar);
contentPain.setTransferHandler(new FileDropHandler());
// set all fonts and colors
updateTheme();
// Finish preparing Editor
pack();
// Set the window bounds and the divider location before setting it visible
state.apply(this);
// Set the minimum size for the editor window
int minWidth =
Toolkit.zoom(Preferences.getInteger("editor.window.width.min"));
int minHeight =
Toolkit.zoom(Preferences.getInteger("editor.window.height.min"));
setMinimumSize(new Dimension(minWidth, minHeight));
// Bring back the general options for the editor
applyPreferences();
// Make textField get the focus whenever frame is activated.
// http://download.oracle.com/javase/tutorial/uiswing/misc/focus.html
// May not be necessary, but helps avoid random situations with
// the editor not being able to request its own focus.
addWindowFocusListener(new WindowAdapter() {
public void windowGainedFocus(WindowEvent e) {
textarea.requestFocusInWindow();
}
});
// TODO: Subclasses can't initialize anything before Doc Open happens since
// super() has to be the first line in subclass constructor; we might
// want to keep constructor light and call methods later [jv 160318]
// Open the document that was passed in
handleOpenInternal(path);
// Add a window listener to watch for changes to the files in the sketch
addWindowFocusListener(new ChangeDetector(this));
// Enable window resizing (which allows for full screen button)
setResizable(true);
{
// Move Lines Keyboard Shortcut (Alt + Arrow Up/Down)
KeyStroke moveUpKeyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.ALT_DOWN_MASK);
final String MOVE_UP_ACTION_KEY = "moveLinesUp";
textarea.getInputMap(JComponent.WHEN_FOCUSED).put(moveUpKeyStroke, MOVE_UP_ACTION_KEY);
textarea.getActionMap().put(MOVE_UP_ACTION_KEY, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
handleMoveLines(true);
}
});
KeyStroke moveDownKeyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent.ALT_DOWN_MASK);
final String MOVE_DOWN_ACTION_KEY = "moveLinesDown";
textarea.getInputMap(JComponent.WHEN_FOCUSED).put(moveDownKeyStroke, MOVE_DOWN_ACTION_KEY);
textarea.getActionMap().put(MOVE_DOWN_ACTION_KEY, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
handleMoveLines(false);
}
});
}
PreferencesEvents.onUpdated(this::updateTheme);
}
protected JEditTextArea createTextArea() {
return new JEditTextArea(new PdeTextAreaDefaults(),
new PdeInputHandler(this));
}
public EditorFooter createFooter() {
EditorFooter ef = new EditorFooter(this);
console = new EditorConsole(this);
ef.addPanel(console, Language.text("editor.footer.console"), "/lib/footer/console");
return ef;
}
public void addErrorTable(EditorFooter ef) {
JScrollPane scrollPane = new JScrollPane();
errorTable = new ErrorTable(this);
scrollPane.setBorder(BorderFactory.createEmptyBorder());
scrollPane.setViewportView(errorTable);
ef.addPanel(scrollPane, Language.text("editor.footer.errors"), "/lib/footer/error");
}
public EditorState getEditorState() {
return state;
}
/**
* Handles files dragged & dropped from the desktop and into the editor
* window. Dragging files into the editor window is the same as using
* "Sketch → Add File" for each file.
*/
class FileDropHandler extends TransferHandler {
public boolean canImport(TransferHandler.TransferSupport support) {
return !sketch.isReadOnly();
}
public boolean importData(TransferHandler.TransferSupport support) {
int successful = 0;
if (!canImport(support)) {
return false;
}
try {
Transferable transferable = support.getTransferable();
DataFlavor uriListFlavor =
new DataFlavor("text/uri-list;class=java.lang.String");
if (transferable.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
List list = (List)
transferable.getTransferData(DataFlavor.javaFileListFlavor);
for (Object o : list) {
File file = (File) o;
if (sketch.addFile(file)) {
successful++;
}
}
} else if (transferable.isDataFlavorSupported(uriListFlavor)) {
// Some platforms (Mac OS X and Linux, when this began) preferred
// this method of moving files.
String data = (String)transferable.getTransferData(uriListFlavor);
String[] pieces = PApplet.splitTokens(data, "\r\n");
for (String piece : pieces) {
if (piece.startsWith("#")) continue;
String path = null;
if (piece.startsWith("file:///")) {
path = piece.substring(7);
} else if (piece.startsWith("file:/")) {
path = piece.substring(5);
}
if (path != null) {
if (sketch.addFile(new File(path))) {
successful++;
}
} else {
System.err.println("Path not found for: " + data);
}
}
}
} catch (Exception e) {
Messages.showWarning("Drag & Drop Problem",
"An error occurred while trying to add files to the sketch.", e);
return false;
}
statusNotice(Language.pluralize("editor.status.drag_and_drop.files_added", successful));
return true;
}
}
public Base getBase() {
return base;
}
public Mode getMode() {
return mode;
}
public void repaintHeader() {
header.repaint();
}
public void rebuildHeader() {
header.rebuild();
}
public void rebuildModePopup() {
modePopup = new JMenu();
ButtonGroup modeGroup = new ButtonGroup();
for (final Mode m : base.getModeList()) {
JRadioButtonMenuItem item = new JRadioButtonMenuItem(m.getTitle());
item.addActionListener(e -> {
if (!base.changeMode(m)) {
// Returns false if unable to change the mode in this window
// (which will open a new window with the new Mode), in which case
// re-select the menu item b/c Java changes it automatically.
reselectMode();
}
});
modePopup.add(item);
modeGroup.add(item);
if (mode == m) {
item.setSelected(true);
}
}
modePopup.addSeparator();
JMenuItem manageModes = new JMenuItem(Language.text("toolbar.manage_modes"));
manageModes.addActionListener(e -> ContributionManager.openModes());
modePopup.add(manageModes);
Toolkit.setMenuMnemsInside(modePopup);
}
// Re-select the old checkbox, because it was automatically
// updated by Java, even though the Mode could not be changed.
// https://github.com/processing/processing/issues/2615
private void reselectMode() {
for (Component c : getModePopup().getComponents()) {
if (c instanceof JRadioButtonMenuItem) {
if (((JRadioButtonMenuItem) c).getText().equals(mode.getTitle())) {
((JRadioButtonMenuItem)c).setSelected(true);
break;
}
}
}
}
public JPopupMenu getModePopup() {
return modePopup.getPopupMenu();
}
public EditorConsole getConsole() {
return console;
}
public EditorHeader createHeader() {
return new EditorHeader(this);
}
abstract public EditorToolbar createToolbar();
@SuppressWarnings("unused")
public EditorToolbar getToolbar() {
return toolbar;
}
/**
* Rebuild the Toolbar after turning debug on/off.
*/
public void rebuildToolbar() {
toolbar.rebuild();
toolbar.revalidate(); // necessary to handle subcomponents
}
abstract public Formatter createFormatter();
protected void setDividerLocation(int pos) {
splitPane.setDividerLocation(pos);
}
protected int getDividerLocation() {
return splitPane.getDividerLocation();
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
/**
* Read and apply new values from the preferences, either because
* the app is just starting up, or the user just finished messing
* with things in the Preferences window.
*/
public void applyPreferences() {
// Even though this is only updating the theme (colors, icons),
// subclasses use this to apply other preferences.
// For instance, Java Mode applies changes to error checking.
updateTheme();
}
public void updateTheme() {
header.updateTheme();
toolbar.updateTheme();
textarea.updateTheme();
errorColumn.updateTheme();
status.updateTheme();
console.updateTheme();
footer.updateTheme();
// Not all Modes will have an error table (that's why it's addErrorTable()
// and not createErrorTable() and called by default).
// https://github.com/jdf/processing.py/issues/382#issuecomment-892269678
if (errorTable != null) {
errorTable.updateTheme();
}
var color = Theme.getColor("toolbar.gradient.top");
spacer.setBackground(color);
toolTipFont = Toolkit.getSansFont(Toolkit.zoom(9), Font.PLAIN);
toolTipTextColor = Theme.get("errors.selection.fgcolor");
toolTipWarningColor = Theme.get("errors.selection.warning.bgcolor");
toolTipErrorColor = Theme.get("errors.selection.error.bgcolor");
UIManager.put("RootPane.background", color);
UIManager.put("TitlePane.embeddedForeground", Theme.getColor("editor.fgcolor"));
getRootPane().updateUI();
UIManager.put("RootPane.background", null);
JPopupMenu popup = modePopup.getPopupMenu();
// Cannot use instanceof because com.formdev.flatlaf.ui.FlatPopupMenuBorder
// is a subclass of EmptyBorder, so just override each time. Cannot set
// null because that will reset the border to the default, not remove it.
// The top/bottom in FlatLaf is 6px, but feels too large.
popup.setBorder(new EmptyBorder(3, 0, 3, 0));
popup.setBackground(Theme.getColor("mode.popup.enabled.bgcolor"));
for (Component comp : modePopup.getMenuComponents()) {
if (comp instanceof JMenuItem item) {
if (item.getUI() instanceof PdeMenuItemUI) {
((PdeMenuItemUI) item.getUI()).updateTheme();
} else {
item.setUI(new PdeMenuItemUI("mode.popup"));
}
} else if (comp instanceof JPopupMenu.Separator) {
comp.setForeground(Theme.getColor("mode.popup.disabled.fgcolor"));
}
}
repaint(); // for good measure
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
protected void buildMenuBar() {
JMenuBar menubar = new JMenuBar();
fileMenu = buildFileMenu();
menubar.add(fileMenu);
menubar.add(buildEditMenu());
menubar.add(buildSketchMenu());
// For 3.0a4 move mode menu to the left of the Tool menu
JMenu modeMenu = buildModeMenu();
if (modeMenu != null) {
menubar.add(modeMenu);
}
toolsMenu = new JMenu(Language.text("menu.tools"));
base.populateToolsMenu(toolsMenu);
menubar.add(toolsMenu);
JMenu helpMenu = buildHelpMenu();
if (Platform.isMacOS()) {
// There's a bug on macOS since at least 2016 that leaves the
// Help menu disabled after a modal dialog has been shown.
// In 2018, it was closed by Oracle with a claim that it couldn't
// be reproduced: https://bugs.openjdk.org/browse/JDK-8196655
// The workaround is to add a space to the end of the menu name,
// which disables whatever macOS behavior is causing the problem.
// https://github.com/processing/processing4/issues/638
helpMenu.setText(helpMenu.getText() + " ");
}
menubar.add(helpMenu);
updateDevelopMenu(menubar);
Toolkit.setMenuMnemonics(menubar);
setJMenuBar(menubar);
}
abstract public JMenu buildFileMenu();
protected JMenu buildFileMenu(JMenuItem[] exportItems) {
JMenuItem item;
JMenu fileMenu = new JMenu(Language.text("menu.file"));
item = Toolkit.newJMenuItem(Language.text("menu.file.new"), 'N');
item.addActionListener(e -> base.handleNew());
fileMenu.add(item);
item = Toolkit.newJMenuItem(Language.text("menu.file.open"), 'O');
item.addActionListener(e -> base.handleOpenPrompt());
fileMenu.add(item);
item = Toolkit.newJMenuItemShift(Language.text("menu.file.sketchbook"), 'K');
item.addActionListener(e -> base.showSketchbookFrame());
fileMenu.add(item);
item = Toolkit.newJMenuItemShift(Language.text("menu.file.examples"), 'O');
item.addActionListener(e -> mode.showExamplesFrame());
fileMenu.add(item);
item = Toolkit.newJMenuItem(Language.text("menu.file.close"), 'W');
item.addActionListener(e -> base.handleClose(Editor.this, false));
fileMenu.add(item);
item = Toolkit.newJMenuItem(Language.text("menu.file.save"), 'S');
item.addActionListener(e -> handleSave(false));
fileMenu.add(item);
item = Toolkit.newJMenuItemShift(Language.text("menu.file.save_as"), 'S');
item.addActionListener(e -> handleSaveAs());
fileMenu.add(item);
if (exportItems != null) {
for (JMenuItem ei : exportItems) {
fileMenu.add(ei);
}
}
fileMenu.addSeparator();
item = Toolkit.newJMenuItemShift(Language.text("menu.file.page_setup"), 'P');
item.addActionListener(e -> handlePageSetup());
fileMenu.add(item);
item = Toolkit.newJMenuItem(Language.text("menu.file.print"), 'P');
item.addActionListener(e -> handlePrint());
fileMenu.add(item);
/*
fileMenu.addSeparator();
item = new JMenuItem("Restart");
item.addActionListener(e -> base.handleRestart());
fileMenu.add(item);
*/
// macOS already has its own preferences and quit menu.
// That's right! Think different, b*tches!
if (!Platform.isMacOS()) {
fileMenu.addSeparator();
item = Toolkit.newJMenuItem(Language.text("menu.file.preferences"), ',');
item.addActionListener(e -> base.handlePrefs());
fileMenu.add(item);
fileMenu.addSeparator();
item = Toolkit.newJMenuItem(Language.text("menu.file.quit"), 'Q');
item.addActionListener(e -> base.handleQuit());
fileMenu.add(item);
}
return fileMenu;
}
protected JMenu buildEditMenu() {
JMenu menu = new JMenu(Language.text("menu.edit"));
JMenuItem item;
undoItem = Toolkit.newJMenuItem(undoAction = new UndoAction(), 'Z');
menu.add(undoItem);
redoItem = new JMenuItem(redoAction = new RedoAction());
redoItem.setAccelerator(Toolkit.getKeyStrokeExt("menu.edit.redo"));
menu.add(redoItem);
menu.addSeparator();
item = Toolkit.newJMenuItem(cutAction = new CutAction(), 'X');
editMenuUpdatable.add(cutAction);
menu.add(item);
item = Toolkit.newJMenuItem(copyAction = new CopyAction(), 'C');
editMenuUpdatable.add(copyAction);
menu.add(item);
item = Toolkit.newJMenuItemShift(copyAsHtmlAction = new CopyAsHtmlAction(), 'C');
editMenuUpdatable.add(copyAsHtmlAction);
menu.add(item);
item = Toolkit.newJMenuItem(pasteAction = new PasteAction(), 'V');
editMenuUpdatable.add(pasteAction);
menu.add(item);
item = Toolkit.newJMenuItem(Language.text("menu.edit.select_all"), 'A');
item.addActionListener(e -> textarea.selectAll());
menu.add(item);
menu.addSeparator();
item = Toolkit.newJMenuItem(Language.text("menu.edit.auto_format"), 'T');
item.addActionListener(e -> handleAutoFormat());
menu.add(item);
item = Toolkit.newJMenuItemExt("menu.edit.comment_uncomment");
item.addActionListener(e -> handleCommentUncomment());
menu.add(item);
item = Toolkit.newJMenuItemExt("menu.edit.increase_indent");
item.addActionListener(e -> handleIndentOutdent(true));
menu.add(item);
item = Toolkit.newJMenuItemExt("menu.edit.decrease_indent");
item.addActionListener(e -> handleIndentOutdent(false));
menu.add(item);
item = Toolkit.newJMenuItemExt("menu.edit.increase_font");
item.addActionListener(e -> {
modifyFontSize(true);
});
menu.add(item);
item = Toolkit.newJMenuItemExt("menu.edit.decrease_font");
item.addActionListener(e -> {
modifyFontSize(false);
});
menu.add(item);
menu.addSeparator();
item = Toolkit.newJMenuItem(Language.text("menu.edit.find"), 'F');
item.addActionListener(e -> {
if (find == null) {
find = new FindReplace(Editor.this);
}
// https://github.com/processing/processing/issues/3457
String selection = getSelectedText();
if (selection != null && selection.length() != 0 &&
!selection.contains("\n")) {
find.setFindText(selection);
}
find.setVisible(true);
});
menu.add(item);
item = Toolkit.newJMenuItem(findNextAction = new FindNextAction(), 'G');
editMenuUpdatable.add(findNextAction);
menu.add(item);
item = Toolkit.newJMenuItemShift(findPreviousAction = new FindPreviousAction(), 'G');
editMenuUpdatable.add(findPreviousAction);
menu.add(item);
UpdatableAction action;
item = Toolkit.newJMenuItem(action = new SelectionForFindAction(), 'E');
editMenuUpdatable.add(action);
menu.add(item);
// Update copy/cut state on selection/de-selection
menu.addMenuListener(new MenuListener() {
// UndoAction and RedoAction do this for themselves.
@Override
public void menuCanceled(MenuEvent e) {
for (UpdatableAction a : editMenuUpdatable) {
a.setEnabled(true);
}
}
@Override
public void menuDeselected(MenuEvent e) {
for (UpdatableAction a : editMenuUpdatable) {
a.setEnabled(true);
}
}
@Override
public void menuSelected(MenuEvent e) {
for (UpdatableAction a : editMenuUpdatable) {
a.updateState();
}
}
});
return menu;
}
protected void modifyFontSize(boolean increase){
var fontSize = Preferences.getInteger("editor.font.size");
fontSize += increase ? 1 : -1;
fontSize = Math.max(5, Math.min(72, fontSize));
Preferences.setInteger("editor.font.size", fontSize);
for (Editor editor : base.getEditors()) {
editor.applyPreferences();
}
Preferences.save();
}
abstract public JMenu buildSketchMenu();
protected JMenu buildSketchMenu(JMenuItem[] runItems) {
JMenuItem item;
sketchMenu = new JMenu(Language.text("menu.sketch"));
for (JMenuItem mi : runItems) {
sketchMenu.add(mi);
}
sketchMenu.addSeparator();
sketchMenu.add(mode.getImportMenu());
item = Toolkit.newJMenuItem(Language.text("menu.sketch.show_sketch_folder"), 'K');
item.addActionListener(e -> {
if (sketch.isUntitled() || sketch.isReadOnly()) {
// Too weird to show the sketch folder when it's buried somewhere in an
// OS-specific temp directory. TODO a better, and localized, message.
Messages.showMessage("Save First", "Please first save the sketch.");
} else {
Platform.openFolder(sketch.getFolder());
}
});
sketchMenu.add(item);
item.setEnabled(Platform.openFolderAvailable());
item = new JMenuItem(Language.text("menu.sketch.add_file"));
item.addActionListener(e -> {
if (sketch.isUntitled() || sketch.isReadOnly()) {
// Technically, this sketch either doesn't exist (it's untitled and
// lives in a temp folder) or it shouldn't be overwritten/modified
// (it's an example). Just ask the user to save. TODO same as above.
Messages.showMessage("Save First", "Please first save the sketch.");
} else {
sketch.handleAddFile();
}
});
sketchMenu.add(item);
//noinspection ConstantConditions
if (runItems != null && runItems.length != 0) {
sketchMenu.addSeparator();
}
sketchMenu.addMenuListener(new MenuListener() {
// Menu Listener that populates the menu only when the menu is opened
final Map itemMap = new HashMap();
@Override
public void menuSelected(MenuEvent event) {
Set unseen = new HashSet(itemMap.values());
for (final Editor editor : base.getEditors()) {
Sketch sketch = editor.getSketch();
JMenuItem item = itemMap.get(sketch);
if (item != null) {
unseen.remove(item);
} else { // it's a new item
item = new JCheckBoxMenuItem();
sketchMenu.add(item);
itemMap.put(sketch, item);
}
// set selected if the current sketch, deselect if not
item.setSelected(sketch.equals(getSketch()));
// name may have changed while Sketch object stayed the same
String name = sketch.getName();
if (!editor.getMode().equals(base.getDefaultMode())) {
name += " (" + editor.getMode().getTitle() + ")";
}
item.setText(name);
// Action listener to bring the appropriate sketch in front
item.addActionListener(e -> {
editor.setState(Frame.NORMAL);
editor.setVisible(true);
editor.toFront();
});
// Disabling for now, might be problematic [fry 200117]
//Toolkit.setMenuMnemsInside(sketchMenu);
}
for (JMenuItem item : unseen) {
sketchMenu.remove(item);
Sketch s = findSketch(item);
if (s != null) {
itemMap.remove(s);
}
}
}
Sketch findSketch(JMenuItem item) {
for (Map.Entry e : itemMap.entrySet()) {
if (item == e.getValue()) {
return e.getKey();
}
}
return null;
}
@Override
public void menuDeselected(MenuEvent event) { }
@Override
public void menuCanceled(MenuEvent event) { }
});
return sketchMenu;
}
abstract public void handleImportLibrary(String name);
public void librariesChanged() { }
public void codeFolderChanged() { }
public void sketchChanged() { }
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
public JMenu getToolMenu() {
return toolsMenu;
}
/**
* Clears the Tool menu and runs the gc so that contributions can be updated
* without classes still being in use.
*/
public void clearToolMenu() {
toolsMenu.removeAll();
System.gc();
}
/**
* Updates update count in the UI. Called on EDT.
* @param count number of available updates
*/
public void setUpdatesAvailable(int count) {
footer.setUpdateCount(count);
}
/**
* Override this if you want a special menu for a Mode.
* You only get one menu, use it wisely!
* Note that this is called from the Editor constructor,
* so your Editor object may not be completely initialized yet.
*/
public JMenu buildModeMenu() {
return null;
}
abstract public JMenu buildHelpMenu();
public void buildDevelopMenu(){
developMenu = new JMenu(Language.text("menu.develop"));
var updateTrigger = new JMenuItem(Language.text("menu.develop.check_for_updates"));
updateTrigger.addActionListener(e -> {
Preferences.unset("update.last");
Preferences.setInteger("update.beta_welcome", 0);
new UpdateCheck(base);
});
developMenu.add(updateTrigger);
}
public void updateDevelopMenu(){
updateDevelopMenu(null);
}
void updateDevelopMenu(JMenuBar menu){
if(menu == null){
menu = getJMenuBar();
}
if(developMenu == null){
buildDevelopMenu();
}
if(Base.DEBUG){
menu.add(developMenu);
}else{
menu.remove(developMenu);
}
}
public void showReference(String filename) {
File file = new File(mode.getReferenceFolder(), filename);
showReferenceFile(file);
}
/**
* Given the .html file, displays it in the default browser.
*/
public void showReferenceFile(File file) {
try {
file = file.getCanonicalFile();
} catch (IOException e) {
e.printStackTrace();
}
// Prepend with file:// and also encode spaces & other characters
Platform.openURL(file.toURI().toString());
}
static public void showChanges() {
// https://github.com/processing/processing/issues/1558
if (!Base.isCommandLine()) {
Platform.openURL("https://github.com/processing/processing4/wiki/Changes-in-4.0");
}
}
static public int getProblemEditorLineStop(Problem problem, int lineStart, int lineStop) {
int stopOffset = problem.getStopOffset();
if (stopOffset == -1) {
stopOffset = lineStop - lineStart;
}
return stopOffset;
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
/**
* Subclass if you want to have setEnabled(canDo()); called when your menu
* is opened.
*/
static abstract class UpdatableAction extends AbstractAction {
public UpdatableAction(String name) {
super(name);
}
abstract public boolean canDo();
public void updateState() {
setEnabled(canDo());
}
}
class CutAction extends UpdatableAction {
public CutAction() {
super(Language.text("menu.edit.cut"));
}
@Override
public void actionPerformed(ActionEvent e) {
handleCut();
}
public boolean canDo() {
return textarea.isSelectionActive();
}
}
class CopyAction extends UpdatableAction {
public CopyAction() {
super(Language.text("menu.edit.copy"));
}
@Override
public void actionPerformed(ActionEvent e) {
handleCopy();
}
public boolean canDo() {
return textarea.isSelectionActive();
}
}
class CopyAsHtmlAction extends UpdatableAction {
public CopyAsHtmlAction() {
super(Language.text("menu.edit.copy_as_html"));
}
@Override
public void actionPerformed(ActionEvent e) {
handleCopyAsHTML();
}
public boolean canDo() {
return textarea.isSelectionActive();
}
}
class PasteAction extends UpdatableAction {
public PasteAction() {
super(Language.text("menu.edit.paste"));
}
@Override
public void actionPerformed(ActionEvent e) {
textarea.paste();
sketch.setModified(true);
}
public boolean canDo() {
return getToolkit().getSystemClipboard()
.isDataFlavorAvailable(DataFlavor.stringFlavor);
}
}
class UndoAction extends AbstractAction {
public UndoAction() {
super(Language.text("menu.edit.undo"));
this.setEnabled(false);
}
public void actionPerformed(ActionEvent e) {
stopCompoundEdit();
try {
final Integer caret = caretUndoStack.pop();
caretRedoStack.push(caret);
textarea.setCaretPosition(caret);
textarea.scrollToCaret();
} catch (Exception ignore) { }
try {
undo.undo();
} catch (CannotUndoException ignored) { }
updateUndoState();
redoAction.updateRedoState();
if (sketch != null) {
sketch.setModified(!getText().equals(sketch.getCurrentCode().getSavedProgram()));
// Go through all tabs; Replace All, Rename or Undo could have changed them
for (SketchCode sc : sketch.getCode()) {
if (sc.getDocument() != null) {
try {
sc.setModified(!sc.getDocumentText().equals(sc.getSavedProgram()));
} catch (BadLocationException ignore) { }
}
}
repaintHeader();
}
}
protected void updateUndoState() {
if (undo.canUndo() || compoundEdit != null && compoundEdit.isInProgress()) {
this.setEnabled(true);
undoItem.setEnabled(true);
String newUndoPresentationName = Language.text("menu.edit.undo");
if (undo.getUndoPresentationName().equals("Undo addition")) {
newUndoPresentationName += " "+Language.text("menu.edit.action.addition");
} else if (undo.getUndoPresentationName().equals("Undo deletion")) {
newUndoPresentationName += " "+Language.text("menu.edit.action.deletion");
}
undoItem.setText(newUndoPresentationName);
putValue(Action.NAME, newUndoPresentationName);
// if (sketch != null) {
// sketch.setModified(true); // 0107, removed for 0196
// }
} else {
this.setEnabled(false);
undoItem.setEnabled(false);
undoItem.setText(Language.text("menu.edit.undo"));
putValue(Action.NAME, Language.text("menu.edit.undo"));
// if (sketch != null) {
// sketch.setModified(false); // 0107
// }
}
}
}
class RedoAction extends AbstractAction {
public RedoAction() {
super(Language.text("menu.edit.redo"));
this.setEnabled(false);
}
public void actionPerformed(ActionEvent e) {
stopCompoundEdit();
try {
undo.redo();
} catch (CannotRedoException ex) {
//System.out.println("Unable to redo: " + ex);
//ex.printStackTrace();
}
try {
final Integer caret = caretRedoStack.pop();
caretUndoStack.push(caret);
textarea.setCaretPosition(caret);
} catch (Exception ignore) {
}
updateRedoState();
undoAction.updateUndoState();
if (sketch != null) {
sketch.setModified(!getText().equals(sketch.getCurrentCode().getSavedProgram()));
// Go through all tabs; Replace All, Rename or Undo could have changed them
for (SketchCode sc : sketch.getCode()) {
if (sc.getDocument() != null) {
try {
sc.setModified(!sc.getDocumentText().equals(sc.getSavedProgram()));
} catch (BadLocationException ignore) {
}
}
}
repaintHeader();
}
}
protected void updateRedoState() {
if (undo.canRedo()) {
redoItem.setEnabled(true);
String newRedoPresentationName = Language.text("menu.edit.redo");
if (undo.getRedoPresentationName().equals("Redo addition")) {
newRedoPresentationName += " " + Language.text("menu.edit.action.addition");
} else if (undo.getRedoPresentationName().equals("Redo deletion")) {
newRedoPresentationName += " " + Language.text("menu.edit.action.deletion");
}
redoItem.setText(newRedoPresentationName);
putValue(Action.NAME, newRedoPresentationName);
} else {
this.setEnabled(false);
redoItem.setEnabled(false);
redoItem.setText(Language.text("menu.edit.redo"));
putValue(Action.NAME, Language.text("menu.edit.redo"));
}
}
}
class FindNextAction extends UpdatableAction {
public FindNextAction() {
super(Language.text("menu.edit.find_next"));
}
@Override
public void actionPerformed(ActionEvent e) {
if (find != null) find.findNext();
}
public boolean canDo() {
return find != null && find.canFindNext();
}
}
class FindPreviousAction extends UpdatableAction {
public FindPreviousAction() {
super(Language.text("menu.edit.find_previous"));
}
@Override
public void actionPerformed(ActionEvent e) {
if (find != null) find.findPrevious();
}
public boolean canDo() {
return find != null && find.canFindNext();
}
}
class SelectionForFindAction extends UpdatableAction {
public SelectionForFindAction() {
super(Language.text("menu.edit.use_selection_for_find"));
}
@Override
public void actionPerformed(ActionEvent e) {
if (find == null) {
find = new FindReplace(Editor.this);
}
find.setFindText(getSelectedText());
}
public boolean canDo() {
return textarea.isSelectionActive();
}
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
// these will be done in a more generic way soon, more like:
// setHandler("action name", Runnable);
// but for the time being, working out the kinks of how many things to
// abstract from the editor in this fashion.
// public void setHandlers(Runnable runHandler, Runnable presentHandler,
// Runnable stopHandler,
// Runnable exportHandler, Runnable exportAppHandler) {
// this.runHandler = runHandler;
// this.presentHandler = presentHandler;
// this.stopHandler = stopHandler;
// this.exportHandler = exportHandler;
// this.exportAppHandler = exportAppHandler;
// }
// public void resetHandlers() {
// runHandler = new DefaultRunHandler();
// presentHandler = new DefaultPresentHandler();
// stopHandler = new DefaultStopHandler();
// exportHandler = new DefaultExportHandler();
// exportAppHandler = new DefaultExportAppHandler();
// }
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
/**
* Gets the current sketch object.
*/
public Sketch getSketch() {
return sketch;
}
/**
* Get the JEditTextArea object for use (not recommended). This should only
* be used in obscure cases that really need to hack the internals of the
* JEditTextArea. Most tools should only interface via the get/set functions
* found in this class. This will maintain compatibility with future releases,
* which will not use JEditTextArea.
*/
public JEditTextArea getTextArea() {
return textarea;
}
public PdeTextArea getPdeTextArea() {
return (textarea instanceof PdeTextArea) ? (PdeTextArea) textarea : null;
}
/**
* Get the contents of the current buffer. Used by the Sketch class.
*/
public String getText() {
return textarea.getText();
}
/**
* Get a range of text from the current buffer.
*/
public String getText(int start, int stop) {
return textarea.getText(start, stop - start);
}
/**
* Replace the entire contents of the front-most tab. Note that this does
* a compound edit, so internal callers may want to use textarea.setText()
* if this is part of a larger compound edit.
*/
public void setText(String what) {
startCompoundEdit();
textarea.setText(what);
stopCompoundEdit();
}
@SuppressWarnings("unused")
public void insertText(String what) {
startCompoundEdit();
int caret = getCaretOffset();
setSelection(caret, caret);
textarea.setSelectedText(what);
stopCompoundEdit();
}
public String getSelectedText() {
return textarea.getSelectedText();
}
@SuppressWarnings("unused")
public void setSelectedText(String what) {
textarea.setSelectedText(what);
}
public void setSelectedText(String what, boolean ever) {
textarea.setSelectedText(what, ever);
}
public void setSelection(int start, int stop) {
// make sure that a tool isn't asking for a bad location
start = PApplet.constrain(start, 0, textarea.getDocumentLength());
stop = PApplet.constrain(stop, 0, textarea.getDocumentLength());
textarea.select(start, stop);
}
/**
* Get the position (character offset) of the caret. With text selected,
* this will be the last character actually selected, no matter the direction
* of the selection. That is, if the user clicks and drags to select lines
* 7 up to 4, then the caret position will be somewhere on line four.
*/
public int getCaretOffset() {
return textarea.getCaretPosition();
}
/**
* True if some text is currently selected.
*/
public boolean isSelectionActive() {
return textarea.isSelectionActive();
}
/**
* Get the beginning point of the current selection.
*/
public int getSelectionStart() {
return textarea.getSelectionStart();
}
/**
* Get the end point of the current selection.
*/
public int getSelectionStop() {
return textarea.getSelectionStop();
}
/**
* Get text for a specified line.
*/
public String getLineText(int line) {
return textarea.getLineText(line);
}
/**
* Replace the text on a specified line.
*/
@SuppressWarnings("unused")
public void setLineText(int line, String what) {
startCompoundEdit();
textarea.select(getLineStartOffset(line), getLineStopOffset(line));
textarea.setSelectedText(what);
stopCompoundEdit();
}
/**
* Get character offset for the start of a given line of text.
*/
public int getLineStartOffset(int line) {
return textarea.getLineStartOffset(line);
}
/**
* Get character offset for end of a given line of text.
*/
public int getLineStopOffset(int line) {
return textarea.getLineStopOffset(line);
}
/**
* Get the number of lines in the currently displayed buffer.
*/
public int getLineCount() {
return textarea.getLineCount();
}
/**
* Use before a manipulating text to group editing operations together
* as a single undo. Use stopCompoundEdit() once finished.
*/
public void startCompoundEdit() {
// Call endTextEditHistory() before starting a new CompoundEdit,
// because there's a timer that's possibly set off for 3 seconds after
// which endTextEditHistory() is called, which means that things get
// messed up. Hence, manually call this method so that auto-format gets
// undone in one fell swoop if the user calls auto-formats within 3
// seconds of typing in the last character. Then start a new compound
// edit so that the auto-format can be undone in one go.
// https://github.com/processing/processing/issues/3003
endTextEditHistory(); // also calls stopCompoundEdit()
//stopCompoundEdit();
compoundEdit = new CompoundEdit();
caretUndoStack.push(textarea.getCaretPosition());
caretRedoStack.clear();
}
/**
* Use with startCompoundEdit() to group edit operations in a single undo.
*/
public void stopCompoundEdit() {
if (compoundEdit != null) {
compoundEdit.end();
undo.addEdit(compoundEdit);
undoAction.updateUndoState();
redoAction.updateRedoState();
compoundEdit = null;
}
}
public int getScrollPosition() {
return textarea.getVerticalScrollPosition();
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
/**
* Switch between tabs, this swaps out the Document object
* that's currently being manipulated.
*/
public void setCode(SketchCode code) {
SyntaxDocument document = (SyntaxDocument) code.getDocument();
if (document == null) { // this document not yet inited
document = new SyntaxDocument() {
@Override
public void beginCompoundEdit() {
if (compoundEdit == null)
startCompoundEdit();
super.beginCompoundEdit();
}
@Override
public void endCompoundEdit() {
stopCompoundEdit();
super.endCompoundEdit();
}
};
code.setDocument(document);
// turn on syntax highlighting
document.setTokenMarker(mode.getTokenMarker(code));
// insert the program text into the document object
try {
document.insertString(0, code.getProgram(), null);
} catch (BadLocationException bl) {
bl.printStackTrace();
}
// set up this guy's own undo manager
// code.undo = new UndoManager();
document.addDocumentListener(new DocumentListener() {
public void removeUpdate(DocumentEvent e) {
if (isInserting && isDirectEdit() && !textarea.isOverwriteEnabled()) {
endTextEditHistory();
}
isInserting = false;
}
public void insertUpdate(DocumentEvent e) {
if (!isInserting && !textarea.isOverwriteEnabled() && isDirectEdit()) {
endTextEditHistory();
}
if (!textarea.isOverwriteEnabled()) {
isInserting = true;
}
}
public void changedUpdate(DocumentEvent e) {
endTextEditHistory();
}
});
// connect the undo listener to the editor
document.addUndoableEditListener(e -> {
// if an edit is in progress, reset the timer
if (endUndoEvent != null) {
endUndoEvent.cancel();
endUndoEvent = null;
startTimerEvent();
}
// if this edit is just getting started, create a compound edit
if (compoundEdit == null) {
startCompoundEdit();
startTimerEvent();
}
compoundEdit.addEdit(e.getEdit());
undoAction.updateUndoState();
redoAction.updateRedoState();
});
}
// update the document object that's in use
textarea.setDocument(document,
code.getSelectionStart(), code.getSelectionStop(),
code.getScrollPosition());
// textarea.requestFocus(); // get the caret blinking
textarea.requestFocusInWindow(); // required for caret blinking
// end edits in the previous tab
endTextEditHistory();
// update the UndoManager and caret positions to the selected tab
this.undo = code.getUndo();
caretUndoStack = code.getCaretUndoStack();
caretRedoStack = code.getCaretRedoStack();
undoAction.updateUndoState();
redoAction.updateRedoState();
}
/**
* @return true if the text is being edited from direct input from typing and
* not shortcuts that manipulate text
*/
boolean isDirectEdit() {
return endUndoEvent != null;
}
void startTimerEvent() {
endUndoEvent = new TimerTask() {
public void run() {
EventQueue.invokeLater(Editor.this::endTextEditHistory);
}
};
timer.schedule(endUndoEvent, 3000);
// let the gc eat the cancelled events
timer.purge();
}
void endTextEditHistory() {
if (endUndoEvent != null) {
endUndoEvent.cancel();
endUndoEvent = null;
}
stopCompoundEdit();
}
public void removeNotify() {
timer.cancel();
super.removeNotify();
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
/**
* Implements Edit → Cut.
*/
public void handleCut() {
textarea.cut();
sketch.setModified(true);
}
/**
* Implements Edit → Copy.
*/
public void handleCopy() {
textarea.copy();
}
/**
* Implements Edit → Copy as HTML.
*/
public void handleCopyAsHTML() {
textarea.copyAsHTML();
statusNotice(Language.text("editor.status.copy_as_html"));
}
/**
* Implements Edit → Paste.
*/
public void handlePaste() {
textarea.paste();
sketch.setModified(true);
}
/**
* Implements Edit → Select All.
*/
public void handleSelectAll() {
textarea.selectAll();
}
public void handleAutoFormat() {
final String source = getText();
try {
final String formattedText = createFormatter().format(source);
// save current (rough) selection point
int selectionEnd = getSelectionStop();
// boolean wasVisible =
// textarea.getSelectionStopLine() >= textarea.getFirstLine() &&
// textarea.getSelectionStopLine() < textarea.getLastLine();
// make sure the caret would be past the end of the text
if (formattedText.length() < selectionEnd - 1) {
selectionEnd = formattedText.length() - 1;
}
if (formattedText.equals(source)) {
statusNotice(Language.text("editor.status.autoformat.no_changes"));
} else { // replace with new bootiful text
startCompoundEdit();
// selectionEnd hopefully at least in the neighborhood
int scrollPos = textarea.getVerticalScrollPosition();
textarea.setText(formattedText);
setSelection(selectionEnd, selectionEnd);
// Put the scrollbar position back, otherwise it jumps on each format.
// Since we're not doing a good job of maintaining position anyway,
// a more complicated workaround here is fairly pointless.
// https://github.com/processing/processing/issues/1571
if (scrollPos != textarea.getVerticalScrollPosition()) {
textarea.setVerticalScrollPosition(scrollPos);
}
stopCompoundEdit();
sketch.setModified(true);
statusNotice(Language.text("editor.status.autoformat.finished"));
}
} catch (final Exception e) {
statusError(e);
}
}
abstract public String getCommentPrefix();
protected void handleCommentUncomment() {
// log("Entering handleCommentUncomment()");
startCompoundEdit();
String prefix = getCommentPrefix();
int prefixLen = prefix.length();
int startLine = textarea.getSelectionStartLine();
int stopLine = textarea.getSelectionStopLine();
int lastLineStart = textarea.getLineStartOffset(stopLine);
int selectionStop = textarea.getSelectionStop();
// If the selection ends at the beginning of the last line,
// then don't (un)comment that line.
if (selectionStop == lastLineStart) {
// Though if there's no selection, don't do that
if (textarea.isSelectionActive()) {
stopLine--;
}
}
// If the text is empty, ignore the user.
// Also ensure that all lines are commented (not just the first)
// when determining whether to comment or uncomment.
boolean commented = true;
for (int i = startLine; commented && (i = textarea.getLineCount()) {
stopCompoundEdit();
return;
}
final String source = textarea.getText(); // Get full text from textarea
int replaceStart = textarea.getLineStartOffset(replacedLine);
int replaceEnd = textarea.getLineStopOffset(replacedLine);
if (replaceEnd > source.length()) {
replaceEnd = source.length();
}
int selectionStart = textarea.getLineStartOffset(startLine);
int selectionEnd = textarea.getLineStopOffset(stopLine);
if (selectionEnd > source.length()) {
selectionEnd = source.length();
}
String replacedText = source.substring(replaceStart, replaceEnd);
String selectedText = source.substring(selectionStart, selectionEnd);
if (replacedLine == textarea.getLineCount() - 1) {
replacedText += "\n";
selectedText = selectedText.substring(0, Math.max(0, selectedText.length() - 1));
} else if (stopLine == textarea.getLineCount() - 1) {
selectedText += "\n";
replacedText = replacedText.substring(0, Math.max(0, replacedText.length() - 1));
}
int newSelectionStart, newSelectionEnd;
if (moveUp) {
textarea.select(selectionStart, selectionEnd);
textarea.setSelectedText(replacedText); // Use setSelectedText()
textarea.select(replaceStart, replaceEnd);
textarea.setSelectedText(selectedText);
newSelectionStart = textarea.getLineStartOffset(startLine - 1);
newSelectionEnd = textarea.getLineStopOffset(stopLine - 1);
} else {
textarea.select(replaceStart, replaceEnd);
textarea.setSelectedText(selectedText);
textarea.select(selectionStart, selectionEnd);
textarea.setSelectedText(replacedText);
newSelectionStart = textarea.getLineStartOffset(startLine + 1);
newSelectionEnd = stopLine + 1 < textarea.getLineCount()
? Math.min(textarea.getLineStopOffset(stopLine + 1), source.length())
: textarea.getLineStopOffset(stopLine); // Prevent out-of-bounds
}
stopCompoundEdit();
if (isSelected)
SwingUtilities.invokeLater(() -> {
textarea.select(newSelectionStart, newSelectionEnd-1);
});
else if (replacedLine >= 0 && replacedLine < textarea.getLineCount()) {
int replacedLineStart = textarea.getLineStartOffset(replacedLine);
int replacedLineEnd = textarea.getLineStopOffset(replacedLine);
// Ensure caret stays within bounds of the new line
int newCaretPos = Math.min(replacedLineStart + column, replacedLineEnd - 1);
SwingUtilities.invokeLater(() -> textarea.setCaretPosition(newCaretPos));
}
}
static public boolean checkParen(char[] array, int index, int stop) {
while (index < stop) {
switch (array[index]) {
case '(' -> {
return true;
}
case ' ', '\t', '\n', '\r' -> index++;
default -> {
return false;
}
}
}
return false;
}
protected boolean functionable(char c) {
return (c == '_') || (c >= 'a' && c = 'A' && c 0 && functionable(c[start - 1])) {
start--;
}
while (stop < c.length && functionable(c[stop])) {
stop++;
}
// System.out.println("start is stop");
}
String text = new String(c, start, stop - start).trim();
// System.out.println(" reference piece is '" + text + "'");
if (checkParen(c, stop, c.length)) {
text += "_";
}
String ref = mode.lookupReference(text);
if (selectIfFound) {
textarea.select(start, stop);
}
return ref;
}
protected void handleFindReference() {
String ref = referenceCheck(true);
if (ref != null) {
showReference(ref + ".html");
} else {
String text = textarea.getSelectedText();
if (text == null) {
statusNotice(Language.text("editor.status.find_reference.select_word_first"));
} else {
statusNotice(Language.interpolate("editor.status.find_reference.not_available", text.trim()));
}
}
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
/**
* Set the location of the sketch run window. Used by Runner to update the
* Editor about window drag events while the sketch is running.
*/
public void setSketchLocation(Point p) {
sketchWindowLocation = p;
}
/**
* Get the last location of the sketch's run window. Used by Runner to make
* the window show up in the same location as when it was last closed.
*/
public Point getSketchLocation() {
return sketchWindowLocation;
}
// public void internalCloseRunner() {
// mode.internalCloseRunner(this);
// }
public boolean isDebuggerEnabled() {
return false;
}
public void toggleBreakpoint(int lineIndex) { }
/**
* Check if the sketch is modified and ask user to save changes.
* @return false if canceling the close/quit operation
*/
@SuppressWarnings({"BooleanMethodIsAlwaysInverted", "RedundantIfStatement"})
public boolean checkModified() {
if (!sketch.isModified()) return true;
// As of Processing 1.0.10, this always happens immediately.
// https://download.processing.org/bugzilla/1456.html
// With Java 7u40 on OS X, need to bring the window forward.
toFront();
if (!Platform.isMacOS()) {
String prompt =
Language.interpolate("close.unsaved_changes", sketch.getName());
int result =
JOptionPane.showConfirmDialog(this, prompt,
Language.text("menu.file.close"),
JOptionPane.YES_NO_CANCEL_OPTION,
JOptionPane.QUESTION_MESSAGE);
if (result == JOptionPane.YES_OPTION) {
return handleSave(true);
} else if (result == JOptionPane.NO_OPTION) {
return true; // ok to continue
} else if (result == JOptionPane.CANCEL_OPTION ||
result == JOptionPane.CLOSED_OPTION) {
return false;
} else {
throw new IllegalStateException();
}
} else {
String tier1 = Language.interpolate("save.title", sketch.getName());
String tier2 = Language.text("save.hint");
JOptionPane pane =
new JOptionPane(Toolkit.formatMessage(tier1, tier2), JOptionPane.QUESTION_MESSAGE);
String[] options = new String[] {
Language.text("save.btn.save"),
Language.text("prompt.cancel"),
Language.text("save.btn.dont_save")
};
pane.setOptions(options);
// highlight the safest option ala apple hig
pane.setInitialValue(options[0]);
// On macOS, setting the destructive property places
// this option away from the others at the left-hand side.
pane.putClientProperty("Quaqua.OptionPane.destructiveOption", 2);
JDialog dialog = pane.createDialog(this, null);
dialog.setVisible(true);
Object result = pane.getValue();
if (result == options[0]) { // save (and close/quit)
return handleSave(true);
} else if (result == options[2]) { // don't save (still close/quit)
return true;
} else { // cancel?
return false;
}
}
}
/**
* Second stage of open, occurs after having checked to see if the
* modifications (if any) to the previous sketch need to be saved.
* Because this method is called in Editor's constructor, a subclass
* shouldn't rely on any of its variables being initialized already.
*/
protected void handleOpenInternal(String path) throws EditorException {
// Prior to 4.0 beta 6, a lot of logic happened here that was
// instead moved into Base. Probably was here so that other Modes
// could override the behavior, but that was too messy. [fry 220206]
try {
sketch = new Sketch(path, this);
} catch (IOException e) {
throw new EditorException("Could not create the sketch.", e);
}
header.rebuild();
updateTitle();
}
/**
* Set the title of the PDE window based on the current sketch, i.e.
* something like "sketch_070752a - Processing 0126"
*/
public void updateTitle() {
setTitle(sketch.getName());
if (!sketch.isUntitled()) {
// Set current file for macOS so that cmd-click in title bar works.
// For 4.0 beta 6 changing this to the sketch folder, rather than the
// .pde for the main tab. (Otherwise, we should have it update when
// the tab changes, which seems like overkill for how this is used.)
getRootPane().putClientProperty("Window.documentFile", sketch.getFolder());
} else {
// per other applications, don't set this until the file has been saved
getRootPane().putClientProperty("Window.documentFile", null);
}
}
/**
* Actually handle the save command. If 'immediately' is set to false,
* this will happen in another thread so that the message area
* will update and the save button will stay highlighted while the
* save is happening. If 'immediately' is true, then it will happen
* immediately. This is used during a quit, because invokeLater()
* won't run properly while a quit is happening. This fixes
* Bug 276.
*/
public boolean handleSave(boolean immediately) {
// This was a mistake (rectified in 0136) that would cause long-running
// sketches to be interrupted, causing much sadness.
//handleStop();
if (sketch.isUntitled()) {
return handleSaveAs();
// need to get the name, user might also cancel here
} else if (immediately) {
handleSaveImpl();
} else {
EventQueue.invokeLater(this::handleSaveImpl);
}
return true;
}
protected void handleSaveImpl() {
statusNotice(Language.text("editor.status.saving"));
try {
if (sketch.save()) {
statusNotice(Language.text("editor.status.saving.done"));
} else {
statusEmpty();
}
} catch (Exception e) {
// show the error as a message in the window
statusError(e);
// zero out the current action,
// so that checkModified2 will just do nothing
//checkModifiedMode = 0;
// this is used when another operation calls a save
}
}
public boolean handleSaveAs() {
statusNotice(Language.text("editor.status.saving"));
try {
if (sketch.saveAs()) {
// No longer showing "Done" message except in cases where a
// progress bar is necessary. Message will come from Sketch.
//statusNotice(Language.text("editor.status.saving.done"));
return true;
} else {
statusNotice(Language.text("editor.status.saving.canceled"));
}
} catch (Exception e) {
// show the error as a message in the window
statusError(e);
}
return false;
}
/**
* Handler for File → Page Setup.
*/
public void handlePageSetup() {
//printerJob = null;
if (printerJob == null) {
printerJob = PrinterJob.getPrinterJob();
}
if (pageFormat == null) {
pageFormat = printerJob.defaultPage();
}
pageFormat = printerJob.pageDialog(pageFormat);
//System.out.println("page format is " + pageFormat);
}
/**
* Handler for File → Print.
*/
public void handlePrint() {
statusNotice(Language.text("editor.status.printing"));
StringBuilder html = new StringBuilder("");
for (SketchCode tab : sketch.getCode()) {
html.append("");
html.append(tab.getPrettyName());
html.append("
");
html.append(textarea.getTextAsHtml((SyntaxDocument)tab.getDocument()));
html.append("
");
}
html.setLength(html.length() - 4); // Don't want last
.
html.append("");
JTextPane jtp = new JTextPane();
// Needed for good line wrapping; otherwise one very long word breaks
// wrapping for the whole document.
jtp.setEditorKit(new HTMLEditorKit() {
public ViewFactory getViewFactory() {
return new HTMLFactory() {
public View create(Element e) {
View v = super.create(e);
if (!(v instanceof javax.swing.text.html.ParagraphView))
return v;
else
return new javax.swing.text.html.ParagraphView(e) {
protected SizeRequirements calculateMinorAxisRequirements(
int axis, SizeRequirements r) {
r = super.calculateMinorAxisRequirements(axis, r);
r.minimum = 1;
return r;
}
};
}
};
}
});
jtp.setFont(new Font(Preferences.get("editor.font.family"), Font.PLAIN, 10));
jtp.setText(html.toString().replace("\n", "
") // Not in a .
.replaceAll("(? " + message + "
";
comp.setToolTipText(content);
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
/**
* Returns the edit popup menu.
*/
class TextAreaPopup extends JPopupMenu {
JMenuItem cutItem, copyItem, discourseItem, pasteItem,
referenceItem;
public TextAreaPopup() {
JMenuItem item;
cutItem = new JMenuItem(Language.text("menu.edit.cut"));
cutItem.addActionListener(e -> handleCut());
this.add(cutItem);
copyItem = new JMenuItem(Language.text("menu.edit.copy"));
copyItem.addActionListener(e -> handleCopy());
this.add(copyItem);
discourseItem = new JMenuItem(Language.text("menu.edit.copy_as_html"));
discourseItem.addActionListener(e -> handleCopyAsHTML());
this.add(discourseItem);
pasteItem = new JMenuItem(Language.text("menu.edit.paste"));
pasteItem.addActionListener(e -> handlePaste());
this.add(pasteItem);
item = new JMenuItem(Language.text("menu.edit.select_all"));
item.addActionListener(e -> handleSelectAll());
this.add(item);
this.addSeparator();
item = new JMenuItem(Language.text("menu.edit.comment_uncomment"));
item.addActionListener(e -> handleCommentUncomment());
this.add(item);
item = new JMenuItem(" " + Language.text("menu.edit.increase_indent"));
item.addActionListener(e -> handleIndentOutdent(true));
this.add(item);
item = new JMenuItem(" " + Language.text("menu.edit.decrease_indent"));
item.addActionListener(e -> handleIndentOutdent(false));
this.add(item);
this.addSeparator();
referenceItem = new JMenuItem(Language.text("find_in_reference"));
referenceItem.addActionListener(e -> handleFindReference());
this.add(referenceItem);
Toolkit.setMenuMnemonics(this);
}
// if no text is selected, disable copy and cut menu items
public void show(Component component, int x, int y) {
// Centralize the checks for each item at the Action.
cutItem.setEnabled(cutAction.canDo());
copyItem.setEnabled(copyAction.canDo());
discourseItem.setEnabled(copyAsHtmlAction.canDo());
pasteItem.setEnabled(pasteAction.canDo());
referenceItem.setEnabled(referenceCheck(false) != null);
super.show(component, x, y);
}
}
/**
* Called when clicking on the version number in the footer.
* Return a string with diagnostic info from the sketch,
* or empty string (or null) if not implemented/available.
*/
public String getSketchDiagnostics() {
return "";
}
}