Java tutorial
/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ /* Part of the Arduino project - http://www.arduino.cc Copyright (c) 2015 Matthijs Kooijman Copyright (c) 2004-09 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; import static processing.app.I18n.tr; import static processing.app.Theme.scale; import java.awt.BorderLayout; import java.awt.Font; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseWheelListener; import java.awt.event.MouseWheelEvent; import java.io.IOException; import javax.swing.Action; import javax.swing.BorderFactory; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.ToolTipManager; import javax.swing.border.MatteBorder; import javax.swing.event.PopupMenuEvent; import javax.swing.event.PopupMenuListener; import javax.swing.text.BadLocationException; import javax.swing.text.Element; import javax.swing.text.PlainDocument; import javax.swing.text.DefaultCaret; import javax.swing.text.Document; import org.apache.commons.lang3.StringUtils; import org.fife.ui.rsyntaxtextarea.RSyntaxDocument; import org.fife.ui.rsyntaxtextarea.RSyntaxTextAreaEditorKit; import org.fife.ui.rsyntaxtextarea.RSyntaxUtilities; import org.fife.ui.rtextarea.Gutter; import org.fife.ui.rtextarea.RTextScrollPane; import cc.arduino.UpdatableBoardsLibsFakeURLsHandler; import processing.app.helpers.DocumentTextChangeListener; import processing.app.syntax.ArduinoTokenMakerFactory; import processing.app.syntax.PdeKeywords; import processing.app.syntax.SketchTextArea; import processing.app.syntax.SketchTextAreaEditorKit; import processing.app.tools.DiscourseFormat; /** * Single tab, editing a single file, in the main window. */ public class EditorTab extends JPanel implements SketchFile.TextStorage, MouseWheelListener { protected Editor editor; protected SketchTextArea textarea; protected RTextScrollPane scrollPane; protected SketchFile file; protected boolean modified; /** Is external editing mode currently enabled? */ protected boolean external; /** * Create a new EditorTab * * @param editor * The Editor this tab runs in * @param file * The file to display in this tab * @param contents * Initial contents to display in this tab. Can be used when editing * a file that doesn't exist yet. If null is passed, code.load() is * called and displayed instead. * @throws IOException */ public EditorTab(Editor editor, SketchFile file, String contents) throws IOException { super(new BorderLayout()); // Load initial contents contents from file if nothing was specified. if (contents == null) { contents = file.load(); modified = false; } else { modified = true; } this.editor = editor; this.file = file; RSyntaxDocument document = createDocument(contents); textarea = createTextArea(document); scrollPane = createScrollPane(textarea); file.setStorage(this); applyPreferences(); add(scrollPane, BorderLayout.CENTER); textarea.addMouseWheelListener(this); } private RSyntaxDocument createDocument(String contents) { RSyntaxDocument document = new RSyntaxDocument(new ArduinoTokenMakerFactory(editor.base.getPdeKeywords()), RSyntaxDocument.SYNTAX_STYLE_CPLUSPLUS); document.putProperty(PlainDocument.tabSizeAttribute, PreferencesData.getInteger("editor.tabs.size")); // insert the program text into the document object try { document.insertString(0, contents, null); } catch (BadLocationException bl) { bl.printStackTrace(); } document.addDocumentListener(new DocumentTextChangeListener(() -> setModified(true))); return document; } private RTextScrollPane createScrollPane(SketchTextArea textArea) throws IOException { RTextScrollPane scrollPane = new RTextScrollPane(textArea, true); scrollPane.setBorder(new MatteBorder(0, 6, 0, 0, Theme.getColor("editor.bgcolor"))); scrollPane.setViewportBorder(BorderFactory.createEmptyBorder()); scrollPane.setLineNumbersEnabled(PreferencesData.getBoolean("editor.linenumbers")); scrollPane.setIconRowHeaderEnabled(false); Gutter gutter = scrollPane.getGutter(); gutter.setBookmarkingEnabled(false); //gutter.setBookmarkIcon(CompletionsRenderer.getIcon(CompletionType.TEMPLATE)); gutter.setIconRowHeaderInheritsGutterBackground(true); return scrollPane; } private SketchTextArea createTextArea(RSyntaxDocument document) throws IOException { final SketchTextArea textArea = new SketchTextArea(document, editor.base.getPdeKeywords()); textArea.setName("editor"); textArea.setFocusTraversalKeysEnabled(false); //textArea.requestFocusInWindow(); textArea.setMarkOccurrences(PreferencesData.getBoolean("editor.advanced")); textArea.setMarginLineEnabled(false); textArea.setCodeFoldingEnabled(PreferencesData.getBoolean("editor.code_folding")); textArea.setAutoIndentEnabled(PreferencesData.getBoolean("editor.indent")); textArea.setCloseCurlyBraces(PreferencesData.getBoolean("editor.auto_close_braces", true)); textArea.setAntiAliasingEnabled(PreferencesData.getBoolean("editor.antialias")); textArea.setTabsEmulated(PreferencesData.getBoolean("editor.tabs.expand")); textArea.setTabSize(PreferencesData.getInteger("editor.tabs.size")); textArea.addHyperlinkListener(evt -> { try { UpdatableBoardsLibsFakeURLsHandler boardLibHandler = new UpdatableBoardsLibsFakeURLsHandler( editor.base); boardLibHandler.openBoardLibManager(evt.getURL()); } catch (Exception e) { try { editor.platform.openURL(editor.getSketch().getFolder(), evt.getURL().toExternalForm()); } catch (Exception f) { Base.showWarning(f.getMessage(), f.getMessage(), f); } } }); textArea.addCaretListener(e -> { Element root = textArea.getDocument().getDefaultRootElement(); int lineStart = root.getElementIndex(e.getMark()); int lineEnd = root.getElementIndex(e.getDot()); editor.lineStatus.set(lineStart, lineEnd); }); ToolTipManager.sharedInstance().registerComponent(textArea); configurePopupMenu(textArea); return textArea; } public void mouseWheelMoved(MouseWheelEvent e) { if (e.isControlDown()) { if (e.getWheelRotation() < 0) { editor.base.handleFontSizeChange(1); } else { editor.base.handleFontSizeChange(-1); } } else { e.getComponent().getParent().dispatchEvent(e); } } private void configurePopupMenu(final SketchTextArea textarea) { JPopupMenu menu = textarea.getPopupMenu(); menu.addSeparator(); JMenuItem item = editor.createToolMenuItem("cc.arduino.packages.formatter.AStyle"); if (item == null) { throw new NullPointerException("Tool cc.arduino.packages.formatter.AStyle unavailable"); } item.setName("menuToolsAutoFormat"); menu.add(item); item = new JMenuItem(tr("Comment/Uncomment"), '/'); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { handleCommentUncomment(); } }); menu.add(item); item = new JMenuItem(tr("Increase Indent"), ']'); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { handleIndentOutdent(true); } }); menu.add(item); item = new JMenuItem(tr("Decrease Indent"), '['); item.setName("menuDecreaseIndent"); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { handleIndentOutdent(false); } }); menu.add(item); item = new JMenuItem(tr("Copy for Forum")); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { handleDiscourseCopy(); } }); menu.add(item); item = new JMenuItem(tr("Copy as HTML")); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { handleHTMLCopy(); } }); menu.add(item); final JMenuItem referenceItem = new JMenuItem(tr("Find in Reference")); referenceItem.addActionListener(editor::handleFindReference); menu.add(referenceItem); final JMenuItem openURLItem = new JMenuItem(tr("Open URL")); openURLItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { Base.openURL(e.getActionCommand()); } }); menu.add(openURLItem); menu.addPopupMenuListener(new PopupMenuListener() { @Override public void popupMenuWillBecomeVisible(PopupMenuEvent e) { String referenceFile = editor.base.getPdeKeywords().getReference(getCurrentKeyword()); referenceItem.setEnabled(referenceFile != null); int offset = textarea.getCaretPosition(); org.fife.ui.rsyntaxtextarea.Token token = RSyntaxUtilities.getTokenAtOffset(textarea, offset); if (token != null && token.isHyperlink()) { openURLItem.setEnabled(true); openURLItem.setActionCommand(token.getLexeme()); } else { openURLItem.setEnabled(false); } } @Override public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { } @Override public void popupMenuCanceled(PopupMenuEvent e) { } }); } public void applyPreferences() { textarea.setCodeFoldingEnabled(PreferencesData.getBoolean("editor.code_folding")); scrollPane.setFoldIndicatorEnabled(PreferencesData.getBoolean("editor.code_folding")); scrollPane.setLineNumbersEnabled(PreferencesData.getBoolean("editor.linenumbers")); // apply the setting for 'use external editor', but only if it changed if (external != PreferencesData.getBoolean("editor.external")) { external = !external; if (external) { // disable line highlight and turn off the caret when disabling textarea.setBackground(Theme.getColor("editor.external.bgcolor")); textarea.setHighlightCurrentLine(false); textarea.setEditable(false); // Detach from the code, since we are no longer the authoritative source // for file contents. file.setStorage(null); // Reload, in case the file contents already changed. reload(); } else { textarea.setBackground(Theme.getColor("editor.bgcolor")); textarea.setHighlightCurrentLine(Theme.getBoolean("editor.linehighlight")); textarea.setEditable(true); file.setStorage(this); // Reload once just before disabling external mode, to ensure we have // the latest contents. reload(); } } // apply changes to the font size for the editor Font editorFont = scale(PreferencesData.getFont("editor.font")); // check whether a theme-defined editor font is available Font themeFont = Theme.getFont("editor.font"); if (themeFont != null) { // Apply theme font if the editor font has *not* been changed by the user, // This allows themes to specify an editor font which will only be applied // if the user hasn't already changed their editor font via preferences.txt String defaultFontName = StringUtils.defaultIfEmpty(PreferencesData.getDefault("editor.font"), "") .split(",")[0]; if (defaultFontName.equals(editorFont.getName())) { editorFont = new Font(themeFont.getName(), themeFont.getStyle(), editorFont.getSize()); } } textarea.setFont(editorFont); scrollPane.getGutter().setLineNumberFont(editorFont); } public void updateKeywords(PdeKeywords keywords) { // update GUI for "Find In Reference" textarea.setKeywords(keywords); // update document for syntax highlighting RSyntaxDocument document = (RSyntaxDocument) textarea.getDocument(); document.setTokenMakerFactory(new ArduinoTokenMakerFactory(keywords)); document.setSyntaxStyle(RSyntaxDocument.SYNTAX_STYLE_CPLUSPLUS); } /** * Called when this tab is made the current one, or when it is the current one * and the window is activated. */ public void activated() { // When external editing is enabled, reload the text whenever we get activated. if (external) { reload(); } } /** * Reload the contents of our file. */ public void reload() { String text; try { text = file.load(); } catch (IOException e) { System.err.println(I18n.format("Warning: Failed to reload file: \"{0}\"", file.getFileName())); return; } setText(text); setModified(false); } /** * Get the TextArea 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 TextArea. */ public SketchTextArea getTextArea() { return textarea; } /** * Get the sketch this tab is editing a file from. */ public SketchController getSketch() { return editor.getSketchController(); } /** * Get the SketchFile that is being edited in this tab. */ public SketchFile getSketchFile() { return this.file; } /** * Get the contents of the text area. */ public String getText() { return textarea.getText(); } /** * Replace the entire contents of this tab. */ public void setText(String what) { // Remove all highlights, since these will all end up at the start of the // text otherwise. Preserving them is tricky, so better just remove them. textarea.removeAllLineHighlights(); // Set the caret update policy to NEVER_UPDATE while completely replacing // the current text. Normally, the caret tracks inserts and deletions, but // replacing the entire text will always make the caret end up at the end, // which isn't really useful. With NEVER_UPDATE, the caret will just keep // its absolute position (number of characters from the start), which isn't // always perfect, but the best we can do without making a diff of the old // and new text and some guesswork. // Note that we cannot use textarea.setText() here, since that first removes // text and then inserts the new text. Even with NEVER_UPDATE, the caret // always makes sure to stay valid, so first removing all text makes it // reset to 0. Also note that simply saving and restoring the caret position // will work, but then the scroll position might change in response to the // caret position. DefaultCaret caret = (DefaultCaret) textarea.getCaret(); int policy = caret.getUpdatePolicy(); caret.setUpdatePolicy(DefaultCaret.NEVER_UPDATE); try { Document doc = textarea.getDocument(); int oldLength = doc.getLength(); // The undo manager already seems to group the insert and remove together // automatically, but better be explicit about it. textarea.beginAtomicEdit(); try { doc.insertString(oldLength, what, null); doc.remove(0, oldLength); } catch (BadLocationException e) { System.err.println("Unexpected failure replacing text"); } finally { textarea.endAtomicEdit(); } } finally { caret.setUpdatePolicy(policy); } } /** * Is the text modified since the last save / load? */ public boolean isModified() { return modified; } /** * Clear modified status. Should only be called by SketchFile through the * TextStorage interface. */ public void clearModified() { setModified(false); } private void setModified(boolean value) { if (value != modified) { modified = value; // TODO: Improve decoupling editor.getSketchController().calcModified(); } } public String getSelectedText() { return textarea.getSelectedText(); } public void setSelectedText(String what) { textarea.replaceSelection(what); } public void setSelection(int start, int stop) { textarea.select(start, stop); } public int getScrollPosition() { return scrollPane.getVerticalScrollBar().getValue(); } public void setScrollPosition(int pos) { scrollPane.getVerticalScrollBar().setValue(pos); } /** * 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.getSelectionEnd(); } /** * Get text for a specified line. */ public String getLineText(int line) { try { return textarea.getText(textarea.getLineStartOffset(line), textarea.getLineEndOffset(line)); } catch (BadLocationException e) { return ""; } } /** * Jump to the given line * @param line The line number to jump to, 1-based. */ public void goToLine(int line) { if (line <= 0) { return; } try { textarea.setCaretPosition(textarea.getLineStartOffset(line - 1)); } catch (BadLocationException e) { //ignore } } void handleCut() { textarea.cut(); } void handleCopy() { textarea.copy(); } void handlePaste() { textarea.paste(); } void handleSelectAll() { textarea.selectAll(); } void handleCommentUncomment() { Action action = textarea.getActionMap().get(RSyntaxTextAreaEditorKit.rstaToggleCommentAction); action.actionPerformed(null); // XXX: RSyntaxDocument doesn't fire DocumentListener events, it should be fixed in RSyntaxTextArea? editor.updateUndoRedoState(); } void handleDiscourseCopy() { new DiscourseFormat(editor, this, false).show(); } void handleHTMLCopy() { new DiscourseFormat(editor, this, true).show(); } void handleIndentOutdent(boolean indent) { if (indent) { Action action = textarea.getActionMap().get(SketchTextAreaEditorKit.rtaIncreaseIndentAction); action.actionPerformed(null); } else { Action action = textarea.getActionMap().get(RSyntaxTextAreaEditorKit.rstaDecreaseIndentAction); action.actionPerformed(null); } // XXX: RSyntaxDocument doesn't fire DocumentListener events, it should be fixed in RSyntaxTextArea? editor.updateUndoRedoState(); } void handleUndo() { textarea.undoLastAction(); } void handleRedo() { textarea.redoLastAction(); } public String getCurrentKeyword() { String text = ""; if (textarea.getSelectedText() != null) text = textarea.getSelectedText().trim(); try { int current = textarea.getCaretPosition(); int startOffset = 0; int endIndex = current; String tmp = textarea.getDocument().getText(current, 1); // TODO probably a regexp that matches Arduino lang special chars // already exists. String regexp = "[\\s\\n();\\\\.!='\\[\\]{}]"; while (!tmp.matches(regexp)) { endIndex++; tmp = textarea.getDocument().getText(endIndex, 1); } // For some reason document index start at 2. // if( current - start < 2 ) return; tmp = ""; while (!tmp.matches(regexp)) { startOffset++; if (current - startOffset < 0) { tmp = textarea.getDocument().getText(0, 1); break; } else tmp = textarea.getDocument().getText(current - startOffset, 1); } startOffset--; int length = endIndex - current + startOffset; text = textarea.getDocument().getText(current - startOffset, length); } catch (BadLocationException bl) { bl.printStackTrace(); } return text; } @Override public boolean requestFocusInWindow() { /** If focus is requested, focus the textarea instead. */ return textarea.requestFocusInWindow(); } }