diff --git a/app/src/cc/arduino/packages/formatter/AStyle.java b/app/src/cc/arduino/packages/formatter/AStyle.java index 7f7c244d6f6..70b6717ff66 100644 --- a/app/src/cc/arduino/packages/formatter/AStyle.java +++ b/app/src/cc/arduino/packages/formatter/AStyle.java @@ -33,10 +33,8 @@ import processing.app.BaseNoGui; import processing.app.Editor; import processing.app.helpers.FileUtils; -import processing.app.syntax.SketchTextArea; import processing.app.tools.Tool; -import javax.swing.text.BadLocationException; import java.io.File; import java.io.IOException; @@ -78,7 +76,7 @@ public void init(Editor editor) { @Override public void run() { - String originalText = editor.getText(); + String originalText = editor.getCurrentTab().getText(); String formattedText = aStyleInterface.AStyleMain(originalText, formatterConfiguration); if (formattedText.equals(originalText)) { @@ -86,57 +84,12 @@ public void run() { return; } - SketchTextArea textArea = editor.getTextArea(); - - int line = getLineOfOffset(textArea); - int lineOffset = getLineOffset(textArea, line); - - textArea.getUndoManager().beginInternalAtomicEdit(); - editor.removeAllLineHighlights(); - editor.setText(formattedText); - editor.getSketch().setModified(true); - textArea.getUndoManager().endInternalAtomicEdit(); - - if (line != -1 && lineOffset != -1) { - try { - setCaretPosition(textArea, line, lineOffset); - } catch (BadLocationException e) { - e.printStackTrace(); - } - } + editor.getCurrentTab().setText(formattedText); // mark as finished editor.statusNotice(tr("Auto Format finished.")); } - private void setCaretPosition(SketchTextArea textArea, int line, int lineOffset) throws BadLocationException { - int caretPosition; - if (line < textArea.getLineCount()) { - caretPosition = Math.min(textArea.getLineStartOffset(line) + lineOffset, textArea.getLineEndOffset(line) - 1); - } else { - caretPosition = textArea.getText().length() - 1; - } - textArea.setCaretPosition(caretPosition); - } - - private int getLineOffset(SketchTextArea textArea, int line) { - try { - return textArea.getCaretPosition() - textArea.getLineStartOffset(line); - } catch (BadLocationException e) { - e.printStackTrace(); - } - return -1; - } - - private int getLineOfOffset(SketchTextArea textArea) { - try { - return textArea.getLineOfOffset(textArea.getCaretPosition()); - } catch (BadLocationException e) { - e.printStackTrace(); - } - return -1; - } - @Override public String getMenuTitle() { return tr("Auto Format"); diff --git a/app/src/cc/arduino/view/GoToLineNumber.java b/app/src/cc/arduino/view/GoToLineNumber.java index 3a3bc6fcaa9..475b0bbe502 100644 --- a/app/src/cc/arduino/view/GoToLineNumber.java +++ b/app/src/cc/arduino/view/GoToLineNumber.java @@ -127,7 +127,7 @@ public void actionPerformed(java.awt.event.ActionEvent evt) { private void okActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_okActionPerformed try { int line = Integer.parseInt(lineNumber.getText()); - editor.goToLine(line); + editor.getCurrentTab().goToLine(line); cancelActionPerformed(evt); } catch (Exception e) { // ignore diff --git a/app/src/cc/arduino/view/findreplace/FindReplace.java b/app/src/cc/arduino/view/findreplace/FindReplace.java index cffb6099c71..1b40e1b12a3 100644 --- a/app/src/cc/arduino/view/findreplace/FindReplace.java +++ b/app/src/cc/arduino/view/findreplace/FindReplace.java @@ -31,7 +31,6 @@ import processing.app.Base; import processing.app.Editor; -import processing.app.Sketch; import processing.app.helpers.OSUtils; import java.awt.*; @@ -292,7 +291,7 @@ private boolean find(boolean wrap, boolean backwards, boolean searchTabs, int or return false; } - String text = editor.getText(); + String text = editor.getCurrentTab().getText(); if (ignoreCaseBox.isSelected()) { search = search.toLowerCase(); @@ -302,7 +301,7 @@ private boolean find(boolean wrap, boolean backwards, boolean searchTabs, int or int nextIndex; if (!backwards) { // int selectionStart = editor.textarea.getSelectionStart(); - int selectionEnd = editor.getSelectionStop(); + int selectionEnd = editor.getCurrentTab().getSelectionStop(); nextIndex = text.indexOf(search, selectionEnd); if (wrap && nextIndex == -1) { @@ -311,7 +310,7 @@ private boolean find(boolean wrap, boolean backwards, boolean searchTabs, int or } } else { // int selectionStart = editor.textarea.getSelectionStart(); - int selectionStart = editor.getSelectionStart() - 1; + int selectionStart = editor.getCurrentTab().getSelectionStart() - 1; if (selectionStart >= 0) { nextIndex = text.lastIndexOf(search, selectionStart); @@ -327,10 +326,9 @@ private boolean find(boolean wrap, boolean backwards, boolean searchTabs, int or if (nextIndex == -1) { // Nothing found on this tab: Search other tabs if required if (searchTabs) { - // editor. - Sketch sketch = editor.getSketch(); - if (sketch.getCodeCount() > 1) { - int realCurrentTab = sketch.getCodeIndex(sketch.getCurrentCode()); + int numTabs = editor.getTabs().size(); + if (numTabs > 1) { + int realCurrentTab = editor.getCurrentTabIndex(); if (originTab != realCurrentTab) { if (originTab < 0) { @@ -338,20 +336,21 @@ private boolean find(boolean wrap, boolean backwards, boolean searchTabs, int or } if (!wrap) { - if ((!backwards && realCurrentTab + 1 >= sketch.getCodeCount()) || (backwards && realCurrentTab - 1 < 0)) { + if ((!backwards && realCurrentTab + 1 >= numTabs) + || (backwards && realCurrentTab - 1 < 0)) { return false; // Can't continue without wrap } } if (backwards) { - sketch.handlePrevCode(); + editor.selectNextTab(); this.setVisible(true); - int l = editor.getText().length() - 1; - editor.setSelection(l, l); + int l = editor.getCurrentTab().getText().length() - 1; + editor.getCurrentTab().setSelection(l, l); } else { - sketch.handleNextCode(); + editor.selectPrevTab(); this.setVisible(true); - editor.setSelection(0, 0); + editor.getCurrentTab().setSelection(0, 0); } return find(wrap, backwards, true, originTab); @@ -365,7 +364,7 @@ private boolean find(boolean wrap, boolean backwards, boolean searchTabs, int or } if (nextIndex != -1) { - editor.setSelection(nextIndex, nextIndex + search.length()); + editor.getCurrentTab().setSelection(nextIndex, nextIndex + search.length()); return true; } @@ -381,18 +380,17 @@ private void replace() { return; } - int newpos = editor.getSelectionStart() - findField.getText().length(); + int newpos = editor.getCurrentTab().getSelectionStart() - findField.getText().length(); if (newpos < 0) { newpos = 0; } - editor.setSelection(newpos, newpos); + editor.getCurrentTab().setSelection(newpos, newpos); boolean foundAtLeastOne = false; if (find(false, false, searchAllFilesBox.isSelected(), -1)) { foundAtLeastOne = true; - editor.setSelectedText(replaceField.getText()); - editor.getSketch().setModified(true); // TODO is this necessary? + editor.getCurrentTab().setSelectedText(replaceField.getText()); } if (!foundAtLeastOne) { @@ -420,17 +418,16 @@ private void replaceAll() { } if (searchAllFilesBox.isSelected()) { - editor.getSketch().setCurrentCode(0); // select the first tab + editor.selectTab(0); // select the first tab } - editor.setSelection(0, 0); // move to the beginning + editor.getCurrentTab().setSelection(0, 0); // move to the beginning boolean foundAtLeastOne = false; while (true) { if (find(false, false, searchAllFilesBox.isSelected(), -1)) { foundAtLeastOne = true; - editor.setSelectedText(replaceField.getText()); - editor.getSketch().setModified(true); // TODO is this necessary? + editor.getCurrentTab().setSelectedText(replaceField.getText()); } else { break; } diff --git a/app/src/processing/app/Base.java b/app/src/processing/app/Base.java index f7af1aa3359..f74f454bee5 100644 --- a/app/src/processing/app/Base.java +++ b/app/src/processing/app/Base.java @@ -583,13 +583,13 @@ protected void storeSketches() { PreferencesData.setInteger("last.sketch.count", index); } - protected void storeRecentSketches(Sketch sketch) { + protected void storeRecentSketches(SketchController sketch) { if (sketch.isUntitled()) { return; } Set sketches = new LinkedHashSet(); - sketches.add(sketch.getMainFilePath()); + sketches.add(sketch.getSketch().getMainFilePath()); sketches.addAll(PreferencesData.getCollection("recent.sketches")); PreferencesData.setCollection("recent.sketches", sketches); @@ -610,13 +610,13 @@ protected void handleActivated(Editor whichEditor) { activeEditor.rebuildRecentSketchesMenu(); if (PreferencesData.getBoolean("editor.external")) { try { - int previousCaretPosition = activeEditor.getTextArea().getCaretPosition(); - activeEditor.getSketch().load(true); - if (previousCaretPosition < activeEditor.getText().length()) { - activeEditor.getTextArea().setCaretPosition(previousCaretPosition); - } + // If the list of files on disk changed, recreate the tabs for them + if (activeEditor.getSketch().reload()) + activeEditor.createTabs(); + else // Let the current tab know it was activated, so it can reload + activeEditor.getCurrentTab().activated(); } catch (IOException e) { - // noop + System.err.println(e); } } @@ -760,50 +760,6 @@ public void handleNew() throws Exception { } - /** - * Replace the sketch in the current window with a new untitled document. - */ - public void handleNewReplace() { - if (!activeEditor.checkModified()) { - return; // sketch was modified, and user canceled - } - // Close the running window, avoid window boogers with multiple sketches - activeEditor.internalCloseRunner(); - - // Actually replace things - handleNewReplaceImpl(); - } - - - protected void handleNewReplaceImpl() { - try { - File file = createNewUntitled(); - if (file != null) { - activeEditor.handleOpenInternal(file); - activeEditor.untitled = true; - } - - } catch (IOException e) { - activeEditor.statusError(e); - } - } - - - public void handleOpenReplace(File file) { - if (!activeEditor.checkModified()) { - return; // sketch was modified, and user canceled - } - // Close the running window, avoid window boogers with multiple sketches - activeEditor.internalCloseRunner(); - - boolean loaded = activeEditor.handleOpenInternal(file); - if (!loaded) { - // replace the document without checking if that's ok - handleNewReplaceImpl(); - } - } - - /** * Prompt for a sketch to open, and open it in a new window. * @@ -865,9 +821,8 @@ protected Editor handleOpen(File file, int[] storedLocation, int[] defaultLocati if (!file.exists()) return null; // Cycle through open windows to make sure that it's not already open. - String path = file.getAbsolutePath(); for (Editor editor : editors) { - if (editor.getSketch().getMainFilePath().equals(path)) { + if (editor.getSketch().getPrimaryFile().getFile().equals(file)) { editor.toFront(); return editor; } @@ -876,7 +831,7 @@ protected Editor handleOpen(File file, int[] storedLocation, int[] defaultLocati Editor editor = new Editor(this, file, storedLocation, defaultLocation, BaseNoGui.getPlatform()); // Make sure that the sketch actually loaded - if (editor.getSketch() == null) { + if (editor.getSketchController() == null) { return null; // Just walk away quietly } @@ -888,7 +843,7 @@ protected Editor handleOpen(File file, int[] storedLocation, int[] defaultLocati // Store information on who's open and running // (in case there's a crash or something that can't be recovered) storeSketches(); - storeRecentSketches(editor.getSketch()); + storeRecentSketches(editor.getSketchController()); rebuildRecentSketchesMenuItems(); PreferencesData.save(); } @@ -957,9 +912,6 @@ public boolean handleClose(Editor editor) { return false; } - // Close the running window, avoid window boogers with multiple sketches - editor.internalCloseRunner(); - if (editors.size() == 1) { // This will store the sketch count as zero editors.remove(editor); @@ -1014,10 +966,6 @@ public boolean handleQuit() { } if (handleQuitEach()) { - // make sure running sketches close before quitting - for (Editor editor : editors) { - editor.internalCloseRunner(); - } // Save out the current prefs state PreferencesData.save(); @@ -1184,7 +1132,7 @@ public void actionPerformed(ActionEvent e) { public void actionPerformed(ActionEvent event) { UserLibrary l = (UserLibrary) getValue("library"); try { - activeEditor.getSketch().importLibrary(l); + activeEditor.getSketchController().importLibrary(l); } catch (IOException e) { showWarning(tr("Error"), I18n.format("Unable to list header files in {0}", l.getSrcFolder()), e); } @@ -1719,7 +1667,7 @@ protected void addLibraries(JMenu menu, LibraryList libs) throws IOException { public void actionPerformed(ActionEvent event) { UserLibrary l = (UserLibrary) getValue("library"); try { - activeEditor.getSketch().importLibrary(l); + activeEditor.getSketchController().importLibrary(l); } catch (IOException e) { showWarning(tr("Error"), I18n.format("Unable to list header files in {0}", l.getSrcFolder()), e); } @@ -2174,54 +2122,6 @@ static public void saveFile(String str, File file) throws IOException { } - /** - * Copy a folder from one place to another. This ignores all dot files and - * folders found in the source directory, to avoid copying silly .DS_Store - * files and potentially troublesome .svn folders. - */ - static public void copyDir(File sourceDir, - File targetDir) throws IOException { - targetDir.mkdirs(); - String files[] = sourceDir.list(); - if (files == null) { - throw new IOException("Unable to list files from " + sourceDir); - } - for (String file : files) { - // Ignore dot files (.DS_Store), dot folders (.svn) while copying - if (file.charAt(0) == '.') continue; - //if (files[i].equals(".") || files[i].equals("..")) continue; - File source = new File(sourceDir, file); - File target = new File(targetDir, file); - if (source.isDirectory()) { - //target.mkdirs(); - copyDir(source, target); - target.setLastModified(source.lastModified()); - } else { - copyFile(source, target); - } - } - } - - - /** - * Remove all files in a directory and the directory itself. - */ - static public void removeDir(File dir) { - BaseNoGui.removeDir(dir); - } - - - /** - * Recursively remove all files within a directory, - * used with removeDir(), or when the contents of a dir - * should be removed, but not the directory itself. - * (i.e. when cleaning temp files from lib/build) - */ - static public void removeDescendants(File dir) { - BaseNoGui.removeDescendants(dir); - } - - /** * Calculate the size of the contents of a folder. * Used to determine whether sketches are empty or not. @@ -2247,48 +2147,6 @@ static public int calcFolderSize(File folder) { return size; } - - /** - * Recursively creates a list of all files within the specified folder, - * and returns a list of their relative paths. - * Ignores any files/folders prefixed with a dot. - */ - static public String[] listFiles(String path, boolean relative) { - return listFiles(new File(path), relative); - } - - - static public String[] listFiles(File folder, boolean relative) { - String path = folder.getAbsolutePath(); - Vector vector = new Vector(); - listFiles(relative ? (path + File.separator) : "", path, vector); - String outgoing[] = new String[vector.size()]; - vector.copyInto(outgoing); - return outgoing; - } - - - static protected void listFiles(String basePath, - String path, Vector vector) { - File folder = new File(path); - String list[] = folder.list(); - if (list == null) return; - - for (int i = 0; i < list.length; i++) { - if (list[i].charAt(0) == '.') continue; - - File file = new File(path, list[i]); - String newPath = file.getAbsolutePath(); - if (newPath.startsWith(basePath)) { - newPath = newPath.substring(basePath.length()); - } - vector.add(newPath); - if (file.isDirectory()) { - listFiles(basePath, newPath, vector); - } - } - } - public void handleAddLibrary() { JFileChooser fileChooser = new JFileChooser(System.getProperty("user.home")); fileChooser.setDialogTitle(tr("Select a zip file or a folder containing the library you'd like to add")); diff --git a/app/src/processing/app/Editor.java b/app/src/processing/app/Editor.java index 2f7b5bfdf79..b5f96aef2b1 100644 --- a/app/src/processing/app/Editor.java +++ b/app/src/processing/app/Editor.java @@ -31,30 +31,19 @@ import cc.arduino.view.findreplace.FindReplace; import com.jcraft.jsch.JSchException; import jssc.SerialPortException; -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 processing.app.debug.RunnerException; import processing.app.forms.PasswordAuthorizationDialog; import processing.app.helpers.Keys; import processing.app.helpers.OSUtils; import processing.app.helpers.PreferencesMapException; import processing.app.legacy.PApplet; -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; import processing.app.tools.MenuScroller; import processing.app.tools.Tool; import javax.swing.*; -import javax.swing.border.MatteBorder; import javax.swing.event.*; import javax.swing.text.BadLocationException; -import javax.swing.text.PlainDocument; import javax.swing.undo.CannotRedoException; import javax.swing.undo.CannotUndoException; import javax.swing.undo.UndoManager; @@ -88,22 +77,31 @@ public class Editor extends JFrame implements RunnerListener { public static final int MAX_TIME_AWAITING_FOR_RESUMING_SERIAL_MONITOR = 10000; - private final Platform platform; + final Platform platform; private JMenu recentSketchesMenu; private JMenu programmersMenu; + private final Box upper; + private ArrayList tabs = new ArrayList<>(); + private int currentTabIndex = -1; - private static class ShouldSaveIfModified implements Predicate { + private static class ShouldSaveIfModified + implements Predicate { @Override - public boolean test(Sketch sketch) { - return PreferencesData.getBoolean("editor.save_on_verify") && sketch.isModified() && !sketch.isReadOnly(BaseNoGui.librariesIndexer.getInstalledLibraries(), BaseNoGui.getExamplesPath()); + public boolean test(SketchController controller) { + return PreferencesData.getBoolean("editor.save_on_verify") + && controller.getSketch().isModified() + && !controller.isReadOnly( + BaseNoGui.librariesIndexer + .getInstalledLibraries(), + BaseNoGui.getExamplesPath()); } } - private static class ShouldSaveReadOnly implements Predicate { + private static class ShouldSaveReadOnly implements Predicate { @Override - public boolean test(Sketch sketch) { + public boolean test(SketchController sketch) { return sketch.isReadOnly(BaseNoGui.librariesIndexer.getInstalledLibraries(), BaseNoGui.getExamplesPath()); } } @@ -163,14 +161,15 @@ public boolean test(Sketch sketch) { private JSplitPane splitPane; // currently opened program + SketchController sketchController; Sketch sketch; - private EditorLineStatus lineStatus; + EditorLineStatus lineStatus; //JEditorPane editorPane; - private SketchTextArea textarea; - private RTextScrollPane scrollPane; + /** Contains all EditorTabs, of which only one will be visible */ + private JPanel codePanel; //Runner runtime; @@ -192,7 +191,6 @@ public boolean test(Sketch sketch) { Runnable presentHandler; private Runnable runAndSaveHandler; private Runnable presentAndSaveHandler; - private Runnable stopHandler; Runnable exportHandler; private Runnable exportAppHandler; @@ -258,7 +256,7 @@ public void windowDeactivated(WindowEvent e) { contentPain.add(pane, BorderLayout.CENTER); Box box = Box.createVerticalBox(); - Box upper = Box.createVerticalBox(); + upper = Box.createVerticalBox(); if (toolbarMenu == null) { toolbarMenu = new JMenu(); @@ -270,9 +268,6 @@ public void windowDeactivated(WindowEvent e) { header = new EditorHeader(this); upper.add(header); - textarea = createTextArea(); - textarea.setName("editor"); - // assemble console panel, consisting of status area and the console itself JPanel consolePanel = new JPanel(); consolePanel.setLayout(new BorderLayout()); @@ -289,19 +284,9 @@ public void windowDeactivated(WindowEvent e) { lineStatus = new EditorLineStatus(); consolePanel.add(lineStatus, BorderLayout.SOUTH); - // 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); + codePanel = new JPanel(new BorderLayout()); + upper.add(codePanel); - upper.add(scrollPane); splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, upper, consolePanel); splitPane.setOneTouchExpandable(true); @@ -337,9 +322,6 @@ public void windowDeactivated(WindowEvent e) { // listener = new EditorListener(this, textarea); pane.add(box); - // get shift down/up events so we can show the alt version of toolbar buttons - textarea.addKeyListener(toolbar); - pane.setTransferHandler(new FileDropHandler()); // System.out.println("t1"); @@ -365,7 +347,7 @@ public void windowDeactivated(WindowEvent e) { // Open the document that was passed in boolean loaded = handleOpenInternal(file); - if (!loaded) sketch = null; + if (!loaded) sketchController = null; // System.out.println("t5"); @@ -396,7 +378,7 @@ public boolean importData(JComponent src, Transferable transferable) { List list = (List) transferable.getTransferData(DataFlavor.javaFileListFlavor); for (File file : list) { - if (sketch.addFile(file)) { + if (sketchController.addFile(file)) { successful++; } } @@ -414,7 +396,7 @@ public boolean importData(JComponent src, Transferable transferable) { } else if (piece.startsWith("file:/")) { path = piece.substring(5); } - if (sketch.addFile(new File(path))) { + if (sketchController.addFile(new File(path))) { successful++; } } @@ -493,45 +475,11 @@ protected int[] getPlacement() { * with things in the Preferences window. */ public void applyPreferences() { - - // apply the setting for 'use external editor' boolean external = PreferencesData.getBoolean("editor.external"); - - textarea.setEditable(!external); saveMenuItem.setEnabled(!external); saveAsMenuItem.setEnabled(!external); - - textarea.setCodeFoldingEnabled(PreferencesData.getBoolean("editor.code_folding")); - scrollPane.setFoldIndicatorEnabled(PreferencesData.getBoolean("editor.code_folding")); - scrollPane.setLineNumbersEnabled(PreferencesData.getBoolean("editor.linenumbers")); - - 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); - - } else { - textarea.setBackground(Theme.getColor("editor.bgcolor")); - textarea.setHighlightCurrentLine(Theme.getBoolean("editor.linehighlight")); - textarea.setEditable(true); - } - - // apply changes to the font size for the editor - //TextAreaPainter painter = textarea.getPainter(); - textarea.setFont(PreferencesData.getFont("editor.font")); - //Font font = painter.getFont(); - //textarea.getPainter().setFont(new Font("Courier", Font.PLAIN, 36)); - - // in case tab expansion stuff has changed - // listener.applyPreferences(); - - // in case moved to a new location - // For 0125, changing to async version (to be implemented later) - //sketchbook.rebuildMenus(); - // For 0126, moved into Base, which will notify all editors. - //base.rebuildMenusAsync(); - + for (EditorTab tab: tabs) + tab.applyPreferences(); } @@ -759,7 +707,7 @@ public void actionPerformed(ActionEvent e) { item = newJMenuItemAlt(tr("Export compiled Binary"), 'S'); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { - if (new ShouldSaveReadOnly().test(sketch) && !handleSave(true)) { + if (new ShouldSaveReadOnly().test(sketchController) && !handleSave(true)) { System.out.println(tr("Export canceled, changes must first be saved.")); return; } @@ -797,7 +745,7 @@ public void actionPerformed(ActionEvent e) { item = new JMenuItem(tr("Add File...")); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { - sketch.handleAddFile(); + sketchController.handleAddFile(); } }); sketchMenu.add(item); @@ -1028,57 +976,14 @@ private String findClassInZipFile(String base, File file) { } } return null; - } - - - private SketchTextArea createTextArea() throws IOException { - final SketchTextArea textArea = new SketchTextArea(base.getPdeKeywords()); - textArea.setFocusTraversalKeysEnabled(false); - textArea.requestFocusInWindow(); - textArea.setMarkOccurrences(PreferencesData.getBoolean("editor.advanced")); - textArea.setMarginLineEnabled(false); - textArea.setCodeFoldingEnabled(PreferencesData.getBoolean("editor.code_folding")); - textArea.setAntiAliasingEnabled(PreferencesData.getBoolean("editor.antialias")); - textArea.setTabsEmulated(PreferencesData.getBoolean("editor.tabs.expand")); - textArea.setTabSize(PreferencesData.getInteger("editor.tabs.size")); - textArea.addHyperlinkListener(new HyperlinkListener() { - @Override - public void hyperlinkUpdate(HyperlinkEvent hyperlinkEvent) { - try { - platform.openURL(sketch.getFolder(), hyperlinkEvent.getURL().toExternalForm()); - } catch (Exception e) { - Base.showWarning(e.getMessage(), e.getMessage(), e); - } - } - }); - textArea.addCaretListener(new CaretListener() { - - @Override - public void caretUpdate(CaretEvent e) { - int lineStart = textArea.getDocument().getDefaultRootElement().getElementIndex(e.getMark()); - int lineEnd = textArea.getDocument().getDefaultRootElement().getElementIndex(e.getDot()); - - lineStatus.set(lineStart, lineEnd); - } - - }); - - ToolTipManager.sharedInstance().registerComponent(textArea); - - configurePopupMenu(textArea); - return textArea; - } + } 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); + for (EditorTab tab : tabs) + tab.updateKeywords(keywords); } - private JMenuItem createToolMenuItem(String className) { + JMenuItem createToolMenuItem(String className) { try { Class toolClass = Class.forName(className); final Tool tool = (Tool) toolClass.newInstance(); @@ -1410,7 +1315,7 @@ private JMenu buildEditMenu() { JMenuItem cutItem = newJMenuItem(tr("Cut"), 'X'); cutItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { - handleCut(); + getCurrentTab().handleCut(); } }); menu.add(cutItem); @@ -1418,7 +1323,7 @@ public void actionPerformed(ActionEvent e) { JMenuItem copyItem = newJMenuItem(tr("Copy"), 'C'); copyItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { - textarea.copy(); + getCurrentTab().getTextArea().copy(); } }); menu.add(copyItem); @@ -1426,11 +1331,7 @@ public void actionPerformed(ActionEvent e) { JMenuItem copyForumItem = newJMenuItemShift(tr("Copy for Forum"), 'C'); copyForumItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { -// SwingUtilities.invokeLater(new Runnable() { -// public void run() { - new DiscourseFormat(Editor.this, false).show(); -// } -// }); + getCurrentTab().handleHTMLCopy(); } }); menu.add(copyForumItem); @@ -1438,11 +1339,7 @@ public void actionPerformed(ActionEvent e) { JMenuItem copyHTMLItem = newJMenuItemAlt(tr("Copy as HTML"), 'C'); copyHTMLItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { -// SwingUtilities.invokeLater(new Runnable() { -// public void run() { - new DiscourseFormat(Editor.this, true).show(); -// } -// }); + getCurrentTab().handleDiscourseCopy(); } }); menu.add(copyHTMLItem); @@ -1450,8 +1347,7 @@ public void actionPerformed(ActionEvent e) { JMenuItem pasteItem = newJMenuItem(tr("Paste"), 'V'); pasteItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { - textarea.paste(); - sketch.setModified(true); + getCurrentTab().handlePaste(); } }); menu.add(pasteItem); @@ -1459,7 +1355,7 @@ public void actionPerformed(ActionEvent e) { JMenuItem selectAllItem = newJMenuItem(tr("Select All"), 'A'); selectAllItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { - textarea.selectAll(); + getCurrentTab().handleSelectAll(); } }); menu.add(selectAllItem); @@ -1477,7 +1373,7 @@ public void actionPerformed(ActionEvent e) { JMenuItem commentItem = newJMenuItem(tr("Comment/Uncomment"), '/'); commentItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { - handleCommentUncomment(); + getCurrentTab().handleCommentUncomment(); } }); menu.add(commentItem); @@ -1486,7 +1382,7 @@ public void actionPerformed(ActionEvent e) { increaseIndentItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0)); increaseIndentItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { - handleIndentOutdent(true); + getCurrentTab().handleIndentOutdent(true); } }); menu.add(increaseIndentItem); @@ -1496,7 +1392,7 @@ public void actionPerformed(ActionEvent e) { decreseIndentItem.setName("menuDecreaseIndent"); decreseIndentItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { - handleIndentOutdent(false); + getCurrentTab().handleIndentOutdent(false); } }); menu.add(decreseIndentItem); @@ -1510,7 +1406,7 @@ public void actionPerformed(ActionEvent e) { find = new FindReplace(Editor.this, Base.FIND_DIALOG_STATE); } if (!OSUtils.isMacOS()) { - find.setFindText(getSelectedText()); + find.setFindText(getCurrentTab().getSelectedText()); } find.setLocationRelativeTo(Editor.this); find.setVisible(true); @@ -1545,7 +1441,7 @@ public void actionPerformed(ActionEvent e) { if (find == null) { find = new FindReplace(Editor.this, Base.FIND_DIALOG_STATE); } - find.setFindText(getSelectedText()); + find.setFindText(getCurrentTab().getSelectedText()); } }); menu.add(useSelectionForFindItem); @@ -1600,8 +1496,7 @@ public UndoAction() { public void actionPerformed(ActionEvent e) { try { - textarea.undoLastAction(); - sketch.setModified(true); + getCurrentTab().handleUndo(); } catch (CannotUndoException ex) { //System.out.println("Unable to undo: " + ex); //ex.printStackTrace(); @@ -1609,8 +1504,7 @@ public void actionPerformed(ActionEvent e) { } protected void updateUndoState() { - - UndoManager undo = textarea.getUndoManager(); + UndoManager undo = getCurrentTab().getUndoManager(); if (undo.canUndo()) { this.setEnabled(true); @@ -1635,8 +1529,7 @@ public RedoAction() { public void actionPerformed(ActionEvent e) { try { - textarea.redoLastAction(); - sketch.setModified(true); + getCurrentTab().handleRedo(); } catch (CannotRedoException ex) { //System.out.println("Unable to redo: " + ex); //ex.printStackTrace(); @@ -1644,8 +1537,8 @@ public void actionPerformed(ActionEvent e) { } protected void updateRedoState() { - UndoManager undo = textarea.getUndoManager(); - + UndoManager undo = getCurrentTab().getUndoManager(); + if (undo.canRedo()) { redoItem.setEnabled(true); redoItem.setText(undo.getRedoPresentationName()); @@ -1674,7 +1567,6 @@ private void resetHandlers() { presentHandler = new BuildHandler(true); runAndSaveHandler = new BuildHandler(false, true); presentAndSaveHandler = new BuildHandler(true, true); - stopHandler = new DefaultStopHandler(); exportHandler = new DefaultExportHandler(); exportAppHandler = new DefaultExportAppHandler(); } @@ -1684,246 +1576,149 @@ private void resetHandlers() { /** - * Gets the current sketch object. + * Gets the current sketch controller. + */ + public SketchController getSketchController() { + return sketchController; + } + + /** + * Gets the current sketch. */ public Sketch getSketch() { return sketch; } - /** - * 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. + * Gets the currently displaying tab. */ - public SketchTextArea getTextArea() { - return textarea; + public EditorTab getCurrentTab() { + return tabs.get(currentTabIndex); } - /** - * Get the contents of the current buffer. Used by the Sketch class. + * Gets the index of the currently displaying tab. */ - public String getText() { - return textarea.getText(); + public int getCurrentTabIndex() { + return currentTabIndex; } - /** - * Replace the entire contents of the front-most tab. + * Returns an (unmodifiable) list of currently opened tabs. */ - public void setText(String what) { - textarea.setText(what); + public List getTabs() { + return Collections.unmodifiableList(tabs); } - - + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . /** - * Called to update the text but not switch to a different set of code - * (which would affect the undo manager). + * Change the currently displayed tab. + * Note that the GUI might not update immediately, since this needs + * to run in the Event dispatch thread. + * @param index The index of the tab to select */ -// public void setText2(String what, int start, int stop) { -// beginCompoundEdit(); -// textarea.setText(what); -// endCompoundEdit(); -// -// // make sure that a tool isn't asking for a bad location -// start = Math.max(0, Math.min(start, textarea.getDocumentLength())); -// stop = Math.max(0, Math.min(start, textarea.getDocumentLength())); -// textarea.select(start, stop); -// -// textarea.requestFocus(); // get the caret blinking -// } - - - public String getSelectedText() { - return textarea.getSelectedText(); + public void selectTab(final int index) { + currentTabIndex = index; + undoAction.updateUndoState(); + redoAction.updateRedoState(); + updateTitle(); + header.rebuild(); + getCurrentTab().activated(); + + // This must be run in the GUI thread + SwingUtilities.invokeLater(() -> { + codePanel.removeAll(); + codePanel.add(tabs.get(index), BorderLayout.CENTER); + tabs.get(index).requestFocusInWindow(); // get the caret blinking + // For some reason, these are needed. Revalidate says it should be + // automatically called when components are added or removed, but without + // it, the component switched to is not displayed. repaint() is needed to + // clear the entire text area of any previous text. + codePanel.revalidate(); + codePanel.repaint(); + }); } - - public void setSelectedText(String what) { - textarea.replaceSelection(what); + public void selectNextTab() { + selectTab((currentTabIndex + 1) % tabs.size()); } - public void setSelection(int start, int stop) { - textarea.select(start, stop); + public void selectPrevTab() { + selectTab((currentTabIndex - 1 + tabs.size()) % tabs.size()); } - - /** - * Get the beginning point of the current selection. - */ - public int getSelectionStart() { - return textarea.getSelectionStart(); + public EditorTab findTab(final SketchFile file) { + return tabs.get(findTabIndex(file)); } - /** - * Get the end point of the current selection. + * Finds the index of the tab showing the given file. Matches the file against + * EditorTab.getSketchFile() using ==. + * + * @returns The index of the tab for the given file, or -1 if no such tab was + * found. */ - public int getSelectionStop() { - return textarea.getSelectionEnd(); + public int findTabIndex(final SketchFile file) { + for (int i = 0; i < tabs.size(); ++i) { + if (tabs.get(i).getSketchFile() == file) + return i; + } + return -1; } - /** - * Get text for a specified line. + * Finds the index of the tab showing the given file. Matches the file against + * EditorTab.getSketchFile().getFile() using equals. + * + * @returns The index of the tab for the given file, or -1 if no such tab was + * found. */ - private String getLineText(int line) { - try { - return textarea.getText(textarea.getLineStartOffset(line), textarea.getLineEndOffset(line)); - } catch (BadLocationException e) { - return ""; + public int findTabIndex(final File file) { + for (int i = 0; i < tabs.size(); ++i) { + if (tabs.get(i).getSketchFile().getFile().equals(file)) + return i; } + return -1; } - - public int getScrollPosition() { - return scrollPane.getVerticalScrollBar().getValue(); - } - - - // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . - - /** - * Switch between tabs, this swaps out the Document object - * that's currently being manipulated. + * Create tabs for each of the current sketch's files, removing any existing + * tabs. */ - protected void setCode(final SketchCodeDocument codeDoc) { - RSyntaxDocument document = (RSyntaxDocument) codeDoc.getDocument(); - - if (document == null) { // this document not yet inited - document = new RSyntaxDocument(new ArduinoTokenMakerFactory(base.getPdeKeywords()), RSyntaxDocument.SYNTAX_STYLE_CPLUSPLUS); - document.putProperty(PlainDocument.tabSizeAttribute, PreferencesData.getInteger("editor.tabs.size")); - - // insert the program text into the document object + public void createTabs() { + tabs.clear(); + currentTabIndex = -1; + tabs.ensureCapacity(sketch.getCodeCount()); + for (SketchFile file : sketch.getFiles()) { try { - document.insertString(0, codeDoc.getCode().getProgram(), null); - } catch (BadLocationException bl) { - bl.printStackTrace(); + addTab(file, null); + } catch(IOException e) { + // TODO: Improve / move error handling + System.err.println(e); } - // set up this guy's own undo manager -// code.undo = new UndoManager(); - - codeDoc.setDocument(document); } - - if(codeDoc.getUndo() == null){ - codeDoc.setUndo(new LastUndoableEditAwareUndoManager(textarea, this)); - document.addUndoableEditListener(codeDoc.getUndo()); - } - - // Update the document object that's in use - textarea.switchDocument(document, codeDoc.getUndo()); - - // HACK multiple tabs: for update Listeners of Gutter, forcin call: Gutter.setTextArea(RTextArea) - // BUG: https://github.com/bobbylight/RSyntaxTextArea/issues/84 - scrollPane.setViewportView(textarea); - - textarea.select(codeDoc.getSelectionStart(), codeDoc.getSelectionStop()); - textarea.requestFocus(); // get the caret blinking - - final int position = codeDoc.getScrollPosition(); - - // invokeLater: Expect the document to be rendered correctly to set the new position - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - scrollPane.getVerticalScrollBar().setValue(position); - undoAction.updateUndoState(); - redoAction.updateRedoState(); - } - }); - - updateTitle(); + selectTab(0); } - - // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . - - /** - * Implements Edit → Cut. + * Add a new tab. + * + * @param file + * The file to show in the tab. + * @param contents + * The contents to show in the tab, or null to load the contents from + * the given file. + * @throws IOException */ - private void handleCut() { - textarea.cut(); - } - - - private void handleDiscourseCopy() { - new DiscourseFormat(Editor.this, false).show(); - } - - - private void handleHTMLCopy() { - new DiscourseFormat(Editor.this, true).show(); + protected void addTab(SketchFile file, String contents) throws IOException { + EditorTab tab = new EditorTab(this, file, contents); + tabs.add(tab); } + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . - void handleCommentUncomment() { - - Action action = textarea.getActionMap().get(RSyntaxTextAreaEditorKit.rstaToggleCommentAction); - action.actionPerformed(null); - - } - - - private 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); - } - } - - private 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; - } - - private void handleFindReference(ActionEvent e) { - String text = getCurrentKeyword(); + void handleFindReference(ActionEvent e) { + String text = getCurrentTab().getCurrentKeyword(); String referenceFile = base.getPdeKeywords().getReference(text); if (referenceFile == null) { @@ -1951,9 +1746,8 @@ public void handleRun(final boolean verbose, Runnable verboseHandler, Runnable n handleRun(verbose, new ShouldSaveIfModified(), verboseHandler, nonVerboseHandler); } - private void handleRun(final boolean verbose, Predicate shouldSavePredicate, Runnable verboseHandler, Runnable nonVerboseHandler) { - internalCloseRunner(); - if (shouldSavePredicate.test(sketch)) { + private void handleRun(final boolean verbose, Predicate shouldSavePredicate, Runnable verboseHandler, Runnable nonVerboseHandler) { + if (shouldSavePredicate.test(sketchController)) { handleSave(true); } toolbar.activateRun(); @@ -1994,8 +1788,7 @@ public BuildHandler(boolean verbose, boolean saveHex) { public void run() { try { removeAllLineHighlights(); - sketch.prepare(); - sketch.build(verbose, saveHex); + sketchController.build(verbose, saveHex); statusNotice(tr("Done compiling.")); } catch (PreferencesMapException e) { statusError(I18n.format( @@ -2012,19 +1805,13 @@ public void run() { } public void removeAllLineHighlights() { - textarea.removeAllLineHighlights(); + for (EditorTab tab : tabs) + tab.getTextArea().removeAllLineHighlights(); } public void addLineHighlight(int line) throws BadLocationException { - textarea.addLineHighlight(line, new Color(1, 0, 0, 0.2f)); - textarea.setCaretPosition(textarea.getLineStartOffset(line)); - } - - private class DefaultStopHandler implements Runnable { - public void run() { - // TODO - // DAM: we should try to kill the compilation or upload process here. - } + getCurrentTab().getTextArea().addLineHighlight(line, new Color(1, 0, 0, 0.2f)); + getCurrentTab().getTextArea().setCaretPosition(getCurrentTab().getTextArea().getLineStartOffset(line)); } @@ -2034,8 +1821,6 @@ public void run() { private void handleStop() { // called by menu or buttons // toolbar.activate(EditorToolbar.STOP); - internalCloseRunner(); - toolbar.deactivateRun(); // toolbar.deactivate(EditorToolbar.STOP); @@ -2043,32 +1828,21 @@ private void handleStop() { // called by menu or buttons toFront(); } - - /** - * Handle internal shutdown of the runner. - */ - public void internalCloseRunner() { - - if (stopHandler != null) - try { - stopHandler.run(); - } catch (Exception e) { } - } - - /** * Check if the sketch is modified and ask user to save changes. * @return false if canceling the close/quit operation */ protected boolean checkModified() { - if (!sketch.isModified()) return true; + if (!sketch.isModified()) + return true; // As of Processing 1.0.10, this always happens immediately. // http://dev.processing.org/bugs/show_bug.cgi?id=1456 toFront(); - String prompt = I18n.format(tr("Save changes to \"{0}\"? "), sketch.getName()); + String prompt = I18n.format(tr("Save changes to \"{0}\"? "), + sketch.getName()); if (!OSUtils.isMacOS()) { int result = @@ -2128,25 +1902,6 @@ protected boolean checkModified() { } } - - /** - * Open a sketch from a particular path, but don't check to save changes. - * Used by Sketch.saveAs() to re-open a sketch after the "Save As" - */ - protected void handleOpenUnchecked(File file, int codeIndex, - int selStart, int selStop, int scrollPos) { - internalCloseRunner(); - handleOpenInternal(file); - // Replacing a document that may be untitled. If this is an actual - // untitled document, then editor.untitled will be set by Base. - untitled = false; - - sketch.setCurrentCode(codeIndex); - textarea.select(selStart, selStop); - scrollPane.getVerticalScrollBar().setValue(scrollPos); - } - - /** * Second stage of open, occurs after having checked to see if the * modifications (if any) to the previous sketch need to be saved. @@ -2156,7 +1911,7 @@ protected boolean handleOpenInternal(File sketchFile) { // in a folder of the same name String fileName = sketchFile.getName(); - File file = SketchData.checkSketchFile(sketchFile); + File file = Sketch.checkSketchFile(sketchFile); if (file == null) { if (!fileName.endsWith(".ino") && !fileName.endsWith(".pde")) { @@ -2212,13 +1967,14 @@ protected boolean handleOpenInternal(File sketchFile) { } try { - sketch = new Sketch(this, file); + sketch = new Sketch(file); } catch (IOException e) { Base.showWarning(tr("Error"), tr("Could not create the sketch."), e); return false; } - header.rebuild(); - updateTitle(); + sketchController = new SketchController(this, sketch); + createTabs(); + // Disable untitled setting from previous document, if any untitled = false; @@ -2227,13 +1983,16 @@ protected boolean handleOpenInternal(File sketchFile) { } private void updateTitle() { - if (sketch == null) { + if (sketchController == null) { return; } - if (sketch.getName().equals(sketch.getCurrentCode().getPrettyName())) { - setTitle(I18n.format(tr("{0} | Arduino {1}"), sketch.getName(), BaseNoGui.VERSION_NAME_LONG)); + SketchFile current = getCurrentTab().getSketchFile(); + if (current.isPrimary()) { + setTitle(I18n.format(tr("{0} | Arduino {1}"), sketch.getName(), + BaseNoGui.VERSION_NAME_LONG)); } else { - setTitle(I18n.format(tr("{0} - {1} | Arduino {2}"), sketch.getName(), sketch.getCurrentCode().getFileName(), BaseNoGui.VERSION_NAME_LONG)); + setTitle(I18n.format(tr("{0} - {1} | Arduino {2}"), sketch.getName(), + current.getFileName(), BaseNoGui.VERSION_NAME_LONG)); } } @@ -2275,15 +2034,15 @@ private boolean handleSave2() { statusNotice(tr("Saving...")); boolean saved = false; try { - boolean wasReadOnly = sketch.isReadOnly(BaseNoGui.librariesIndexer.getInstalledLibraries(), BaseNoGui.getExamplesPath()); + boolean wasReadOnly = sketchController.isReadOnly(BaseNoGui.librariesIndexer.getInstalledLibraries(), BaseNoGui.getExamplesPath()); String previousMainFilePath = sketch.getMainFilePath(); - saved = sketch.save(); + saved = sketchController.save(); if (saved) { statusNotice(tr("Done Saving.")); if (wasReadOnly) { base.removeRecentSketchPath(previousMainFilePath); } - base.storeRecentSketches(sketch); + base.storeRecentSketches(sketchController); base.rebuildRecentSketchesMenuItems(); } else { statusEmpty(); @@ -2320,8 +2079,8 @@ public boolean handleSaveAs() { //public void run() { statusNotice(tr("Saving...")); try { - if (sketch.saveAs()) { - base.storeRecentSketches(sketch); + if (sketchController.saveAs()) { + base.storeRecentSketches(sketchController); base.rebuildRecentSketchesMenuItems(); statusNotice(tr("Done Saving.")); // Disabling this for 0125, instead rebuild the menu inside @@ -2388,7 +2147,11 @@ private boolean serialPrompt() { */ synchronized public void handleExport(final boolean usingProgrammer) { if (PreferencesData.getBoolean("editor.save_on_verify")) { - if (sketch.isModified() && !sketch.isReadOnly(BaseNoGui.librariesIndexer.getInstalledLibraries(), BaseNoGui.getExamplesPath())) { + if (sketch.isModified() + && !sketchController.isReadOnly( + BaseNoGui.librariesIndexer + .getInstalledLibraries(), + BaseNoGui.getExamplesPath())) { handleSave(true); } } @@ -2414,7 +2177,7 @@ public void run() { uploading = true; - boolean success = sketch.exportApplet(false); + boolean success = sketchController.exportApplet(false); if (success) { statusNotice(tr("Done uploading.")); } @@ -2509,7 +2272,7 @@ public void run() { uploading = true; - boolean success = sketch.exportApplet(true); + boolean success = sketchController.exportApplet(true); if (success) { statusNotice(tr("Done uploading.")); } @@ -2781,12 +2544,12 @@ private void handlePrint() { PrinterJob printerJob = PrinterJob.getPrinterJob(); if (pageFormat != null) { //System.out.println("setting page format " + pageFormat); - printerJob.setPrintable(textarea, pageFormat); + printerJob.setPrintable(getCurrentTab().getTextArea(), pageFormat); } else { - printerJob.setPrintable(textarea); + printerJob.setPrintable(getCurrentTab().getTextArea()); } // set the name of the job to the code name - printerJob.setJobName(sketch.getCurrentCode().getPrettyName()); + printerJob.setJobName(getCurrentTab().getSketchFile().getPrettyName()); if (printerJob.printDialog()) { try { @@ -2830,23 +2593,23 @@ public void statusError(Exception e) { if (e instanceof RunnerException) { RunnerException re = (RunnerException) e; - if (re.hasCodeIndex()) { - sketch.setCurrentCode(re.getCodeIndex()); + if (re.hasCodeFile()) { + selectTab(findTabIndex(re.getCodeFile())); } if (re.hasCodeLine()) { int line = re.getCodeLine(); // subtract one from the end so that the \n ain't included - if (line >= textarea.getLineCount()) { + if (line >= getCurrentTab().getTextArea().getLineCount()) { // The error is at the end of this current chunk of code, // so the last line needs to be selected. - line = textarea.getLineCount() - 1; - if (getLineText(line).length() == 0) { + line = getCurrentTab().getTextArea().getLineCount() - 1; + if (getCurrentTab().getLineText(line).length() == 0) { // The last line may be zero length, meaning nothing to select. // If so, back up one more line. line--; } } - if (line < 0 || line >= textarea.getLineCount()) { + if (line < 0 || line >= getCurrentTab().getTextArea().getLineCount()) { System.err.println(I18n.format(tr("Bad error line: {0}"), line)); } else { try { @@ -2891,7 +2654,6 @@ private void statusEmpty() { statusNotice(EMPTY); } - // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . protected void onBoardOrPortChange() { @@ -2905,110 +2667,4 @@ protected void onBoardOrPortChange() { } - private void configurePopupMenu(final SketchTextArea textarea){ - - JPopupMenu menu = textarea.getPopupMenu(); - - menu.addSeparator(); - - JMenuItem item = 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 = newJMenuItem(tr("Comment/Uncomment"), '/'); - item.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent e) { - handleCommentUncomment(); - } - }); - menu.add(item); - - item = newJMenuItem(tr("Increase Indent"), ']'); - item.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent e) { - handleIndentOutdent(true); - } - }); - menu.add(item); - - item = newJMenuItem(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(this::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 = 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 goToLine(int line) { - if (line <= 0) { - return; - } - try { - textarea.setCaretPosition(textarea.getLineStartOffset(line - 1)); - } catch (BadLocationException e) { - //ignore - } - } - } diff --git a/app/src/processing/app/EditorHeader.java b/app/src/processing/app/EditorHeader.java index 84dc49df4c1..b0e3c9c0300 100644 --- a/app/src/processing/app/EditorHeader.java +++ b/app/src/processing/app/EditorHeader.java @@ -32,7 +32,7 @@ import java.awt.*; import java.awt.event.*; import java.io.IOException; - +import java.util.List; import javax.swing.*; @@ -82,26 +82,24 @@ public class EditorHeader extends JComponent { public class Actions { public final Action newTab = new SimpleAction(tr("New Tab"), Keys.ctrlShift(KeyEvent.VK_N), - () -> editor.getSketch().handleNewCode()); + () -> editor.getSketchController().handleNewCode()); public final Action renameTab = new SimpleAction(tr("Rename"), - () -> editor.getSketch().handleRenameCode()); + () -> editor.getSketchController().handleRenameCode()); public final Action deleteTab = new SimpleAction(tr("Delete"), () -> { try { - editor.getSketch().handleDeleteCode(); + editor.getSketchController().handleDeleteCode(); } catch (IOException e) { e.printStackTrace(); } }); public final Action prevTab = new SimpleAction(tr("Previous Tab"), - Keys.ctrlAlt(KeyEvent.VK_LEFT), - () -> editor.sketch.handlePrevCode()); + Keys.ctrlAlt(KeyEvent.VK_LEFT), () -> editor.selectPrevTab()); public final Action nextTab = new SimpleAction(tr("Next Tab"), - Keys.ctrlAlt(KeyEvent.VK_RIGHT), - () -> editor.sketch.handleNextCode()); + Keys.ctrlAlt(KeyEvent.VK_RIGHT), () -> editor.selectNextTab()); Actions() { // Explicitly bind keybindings for the actions with accelerators above @@ -170,10 +168,10 @@ public void mousePressed(MouseEvent e) { popup.show(EditorHeader.this, x, y); } else { - Sketch sketch = editor.getSketch(); - for (int i = 0; i < sketch.getCodeCount(); i++) { + int numTabs = editor.getTabs().size(); + for (int i = 0; i < numTabs; i++) { if ((x > tabLeft[i]) && (x < tabRight[i])) { - sketch.setCurrentCode(i); + editor.selectTab(i); repaint(); } } @@ -186,7 +184,7 @@ public void mousePressed(MouseEvent e) { public void paintComponent(Graphics screen) { if (screen == null) return; - Sketch sketch = editor.getSketch(); + SketchController sketch = editor.getSketchController(); if (sketch == null) return; // ?? Dimension size = getSize(); @@ -229,21 +227,22 @@ public void paintComponent(Graphics screen) { g.setColor(backgroundColor); g.fillRect(0, 0, imageW, imageH); - int codeCount = sketch.getCodeCount(); + List tabs = editor.getTabs(); + + int codeCount = tabs.size(); if ((tabLeft == null) || (tabLeft.length < codeCount)) { tabLeft = new int[codeCount]; tabRight = new int[codeCount]; } int x = 6; // offset from left edge of the component - for (int i = 0; i < sketch.getCodeCount(); i++) { - SketchCode code = sketch.getCode(i); - - String codeName = code.isExtension(sketch.getHiddenExtensions()) ? - code.getPrettyName() : code.getFileName(); + int i = 0; + for (EditorTab tab : tabs) { + SketchFile file = tab.getSketchFile(); + String filename = file.getPrettyName(); // if modified, add the li'l glyph next to the name - String text = " " + codeName + (code.isModified() ? " \u00A7" : " "); + String text = " " + filename + (file.isModified() ? " \u00A7" : " "); Graphics2D g2 = (Graphics2D) g; int textWidth = (int) @@ -252,7 +251,7 @@ public void paintComponent(Graphics screen) { int pieceCount = 2 + (textWidth / PIECE_WIDTH); int pieceWidth = pieceCount * PIECE_WIDTH; - int state = (code == sketch.getCurrentCode()) ? SELECTED : UNSELECTED; + int state = (i == editor.getCurrentTabIndex()) ? SELECTED : UNSELECTED; g.drawImage(pieces[state][LEFT], x, 0, null); x += PIECE_WIDTH; @@ -272,6 +271,7 @@ public void paintComponent(Graphics screen) { g.drawImage(pieces[state][RIGHT], x, 0, null); x += PIECE_WIDTH - 1; // overlap by 1 pixel + i++; } menuLeft = sizeW - (16 + pieces[0][MENU].getWidth(this)); @@ -317,13 +317,14 @@ public void rebuildMenu() { Sketch sketch = editor.getSketch(); if (sketch != null) { menu.addSeparator(); + int i = 0; - for (SketchCode code : sketch.getCodes()) { + for (EditorTab tab : editor.getTabs()) { + SketchFile file = tab.getSketchFile(); final int index = i++; - item = new JMenuItem(code.isExtension(sketch.getDefaultExtension()) ? - code.getPrettyName() : code.getFileName()); + item = new JMenuItem(file.getPrettyName()); item.addActionListener((ActionEvent e) -> { - editor.getSketch().setCurrentCode(index); + editor.selectTab(index); }); menu.add(item); } diff --git a/app/src/processing/app/EditorStatus.java b/app/src/processing/app/EditorStatus.java index b6551c7fa00..cd56c4171c5 100644 --- a/app/src/processing/app/EditorStatus.java +++ b/app/src/processing/app/EditorStatus.java @@ -143,7 +143,7 @@ public void edit(String message, String dflt) { editField.setVisible(true); editField.setText(dflt); editField.selectAll(); - editField.requestFocus(); + editField.requestFocusInWindow(); repaint(); } @@ -242,7 +242,7 @@ private void initialize() { // answering to rename/new code question if (mode == EDIT) { // this if() isn't (shouldn't be?) necessary String answer = editField.getText(); - editor.getSketch().nameCode(answer); + editor.getSketchController().nameCode(answer); unedit(); } }); @@ -281,7 +281,7 @@ public void keyTyped(KeyEvent event) { if (c == KeyEvent.VK_ENTER) { // accept the input String answer = editField.getText(); - editor.getSketch().nameCode(answer); + editor.getSketchController().nameCode(answer); unedit(); event.consume(); diff --git a/app/src/processing/app/EditorTab.java b/app/src/processing/app/EditorTab.java new file mode 100644 index 00000000000..3ac09bc8054 --- /dev/null +++ b/app/src/processing/app/EditorTab.java @@ -0,0 +1,608 @@ +/* -*- 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 java.awt.BorderLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +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.CaretEvent; +import javax.swing.event.CaretListener; +import javax.swing.event.HyperlinkEvent; +import javax.swing.event.HyperlinkListener; +import javax.swing.event.PopupMenuEvent; +import javax.swing.event.PopupMenuListener; +import javax.swing.text.BadLocationException; +import javax.swing.text.PlainDocument; +import javax.swing.undo.UndoManager; +import javax.swing.text.DefaultCaret; + +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 org.fife.ui.rtextarea.RUndoManager; + +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 { + 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); + this.textarea = createTextArea(document); + this.scrollPane = createScrollPane(this.textarea); + file.setStorage(this); + applyPreferences(); + add(this.scrollPane, BorderLayout.CENTER); + + RUndoManager undo = new LastUndoableEditAwareUndoManager(this.textarea, + this.editor); + document.addUndoableEditListener(undo); + textarea.setUndoManager(undo); + } + + 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.setAntiAliasingEnabled(PreferencesData.getBoolean("editor.antialias")); + textArea.setTabsEmulated(PreferencesData.getBoolean("editor.tabs.expand")); + textArea.setTabSize(PreferencesData.getInteger("editor.tabs.size")); + textArea.addHyperlinkListener(new HyperlinkListener() { + @Override + public void hyperlinkUpdate(HyperlinkEvent hyperlinkEvent) { + try { + editor.platform.openURL(editor.getSketch().getFolder(), + hyperlinkEvent.getURL().toExternalForm()); + } catch (Exception e) { + Base.showWarning(e.getMessage(), e.getMessage(), e); + } + } + }); + textArea.addCaretListener(new CaretListener() { + @Override + public void caretUpdate(CaretEvent e) { + int lineStart = textArea.getDocument().getDefaultRootElement().getElementIndex(e.getMark()); + int lineEnd = textArea.getDocument().getDefaultRootElement().getElementIndex(e.getDot()); + + editor.lineStatus.set(lineStart, lineEnd); + } + + }); + + ToolTipManager.sharedInstance().registerComponent(textArea); + + configurePopupMenu(textArea); + return textArea; + } + + 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 + textarea.setFont(PreferencesData.getFont("editor.font")); + } + + 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 { + RSyntaxDocument doc = (RSyntaxDocument)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.getUndoManager().beginInternalAtomicEdit(); + try { + doc.insertString(oldLength, what, null); + doc.remove(0, oldLength); + } catch (BadLocationException e) { + System.err.println("Unexpected failure replacing text"); + } finally { + textarea.getUndoManager().endInternalAtomicEdit(); + } + } 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); + + } + + 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); + } + } + + void handleUndo() { + textarea.undoLastAction(); + } + + void handleRedo() { + textarea.redoLastAction(); + } + + public UndoManager getUndoManager() { + return textarea.getUndoManager(); + } + + 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(); + } + +} \ No newline at end of file diff --git a/app/src/processing/app/EditorToolbar.java b/app/src/processing/app/EditorToolbar.java index e433d372c49..7313baa44a8 100644 --- a/app/src/processing/app/EditorToolbar.java +++ b/app/src/processing/app/EditorToolbar.java @@ -27,7 +27,6 @@ import javax.swing.event.MouseInputListener; import java.awt.*; import java.awt.event.KeyEvent; -import java.awt.event.KeyListener; import java.awt.event.MouseEvent; import static processing.app.I18n.tr; @@ -36,7 +35,7 @@ /** * run/stop/etc buttons for the ide */ -public class EditorToolbar extends JComponent implements MouseInputListener, KeyListener { +public class EditorToolbar extends JComponent implements MouseInputListener, KeyEventDispatcher { /** * Rollover titles for each button. @@ -136,6 +135,7 @@ public EditorToolbar(Editor editor, JMenu menu) { addMouseListener(this); addMouseMotionListener(this); + KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(this); } private void loadButtons() { @@ -440,24 +440,12 @@ public Dimension getMaximumSize() { return new Dimension(3000, BUTTON_HEIGHT); } - - public void keyPressed(KeyEvent e) { - if (e.getKeyCode() == KeyEvent.VK_SHIFT) { - shiftPressed = true; - repaint(); - } - } - - - public void keyReleased(KeyEvent e) { - if (e.getKeyCode() == KeyEvent.VK_SHIFT) { - shiftPressed = false; + public boolean dispatchKeyEvent(final KeyEvent e) { + if (shiftPressed != e.isShiftDown()) { + shiftPressed = !shiftPressed; repaint(); } + // Return false to continue processing this keyEvent + return false; } - - - public void keyTyped(KeyEvent e) { - } - } diff --git a/app/src/processing/app/Sketch.java b/app/src/processing/app/Sketch.java deleted file mode 100644 index 444888c907d..00000000000 --- a/app/src/processing/app/Sketch.java +++ /dev/null @@ -1,1420 +0,0 @@ -/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ - -/* - Part of the Processing project - http://processing.org - - Copyright (c) 2004-10 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 as published by - the Free Software Foundation; either version 2 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, write to the Free Software Foundation, - Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -*/ - -package processing.app; - -import cc.arduino.Compiler; -import cc.arduino.CompilerProgressListener; -import cc.arduino.UploaderUtils; -import cc.arduino.packages.Uploader; -import org.apache.commons.codec.digest.DigestUtils; -import processing.app.debug.RunnerException; -import processing.app.forms.PasswordAuthorizationDialog; -import processing.app.helpers.FileUtils; -import processing.app.helpers.OSUtils; -import processing.app.helpers.PreferencesMapException; -import processing.app.packages.LibraryList; -import processing.app.packages.UserLibrary; - -import javax.swing.*; -import java.awt.*; -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.Arrays; -import java.util.LinkedList; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static processing.app.I18n.tr; - - -/** - * Stores information about files in the current sketch - */ -public class Sketch { - private final Editor editor; - - /** true if any of the files have been modified. */ - private boolean modified; - - private SketchCodeDocument current; - private int currentIndex; - - private final SketchData data; - - /** - * path is location of the main .pde file, because this is also - * simplest to use when opening the file from the finder/explorer. - */ - public Sketch(Editor _editor, File file) throws IOException { - editor = _editor; - data = new SketchData(file); - load(); - } - - - /** - * Build the list of files. - *

- * Generally this is only done once, rather than - * each time a change is made, because otherwise it gets to be - * a nightmare to keep track of what files went where, because - * not all the data will be saved to disk. - *

- * This also gets called when the main sketch file is renamed, - * because the sketch has to be reloaded from a different folder. - *

- * Another exception is when an external editor is in use, - * in which case the load happens each time "run" is hit. - */ - private void load() throws IOException { - load(false); - } - - protected void load(boolean forceUpdate) throws IOException { - data.load(); - - for (SketchCode code : data.getCodes()) { - if (code.getMetadata() == null) - code.setMetadata(new SketchCodeDocument(this, code)); - } - - // set the main file to be the current tab - if (editor != null) { - setCurrentCode(currentIndex, forceUpdate); - } - } - - - private boolean renamingCode; - - /** - * Handler for the New Code menu option. - */ - public void handleNewCode() { - editor.status.clearState(); - // make sure the user didn't hide the sketch folder - ensureExistence(); - - // if read-only, give an error - if (isReadOnly(BaseNoGui.librariesIndexer.getInstalledLibraries(), BaseNoGui.getExamplesPath())) { - // if the files are read-only, need to first do a "save as". - Base.showMessage(tr("Sketch is Read-Only"), - tr("Some files are marked \"read-only\", so you'll\n" + - "need to re-save the sketch in another location,\n" + - "and try again.")); - return; - } - - renamingCode = false; - editor.status.edit(tr("Name for new file:"), ""); - } - - - /** - * Handler for the Rename Code menu option. - */ - public void handleRenameCode() { - editor.status.clearState(); - // make sure the user didn't hide the sketch folder - ensureExistence(); - - if (currentIndex == 0 && editor.untitled) { - Base.showMessage(tr("Sketch is Untitled"), - tr("How about saving the sketch first \n" + - "before trying to rename it?")); - return; - } - - // if read-only, give an error - if (isReadOnly(BaseNoGui.librariesIndexer.getInstalledLibraries(), BaseNoGui.getExamplesPath())) { - // if the files are read-only, need to first do a "save as". - Base.showMessage(tr("Sketch is Read-Only"), - tr("Some files are marked \"read-only\", so you'll\n" + - "need to re-save the sketch in another location,\n" + - "and try again.")); - return; - } - - // ask for new name of file (internal to window) - // TODO maybe just popup a text area? - renamingCode = true; - String prompt = (currentIndex == 0) ? - "New name for sketch:" : "New name for file:"; - String oldName = (current.getCode().isExtension("ino")) ? - current.getCode().getPrettyName() : current.getCode().getFileName(); - editor.status.edit(prompt, oldName); - } - - - /** - * This is called upon return from entering a new file name. - * (that is, from either newCode or renameCode after the prompt) - * This code is almost identical for both the newCode and renameCode - * cases, so they're kept merged except for right in the middle - * where they diverge. - */ - protected void nameCode(String newName) { - // make sure the user didn't hide the sketch folder - ensureExistence(); - - // Add the extension here, this simplifies some of the logic below. - if (newName.indexOf('.') == -1) { - newName += "." + getDefaultExtension(); - } - - // if renaming to the same thing as before, just ignore. - // also ignoring case here, because i don't want to write - // a bunch of special stuff for each platform - // (osx is case insensitive but preserving, windows insensitive, - // *nix is sensitive and preserving.. argh) - if (renamingCode) { - if (newName.equalsIgnoreCase(current.getCode().getFileName()) && OSUtils.isWindows()) { - // exit quietly for the 'rename' case. - // if it's a 'new' then an error will occur down below - return; - } - } - - newName = newName.trim(); - if (newName.equals("")) return; - - int dot = newName.indexOf('.'); - if (dot == 0) { - Base.showWarning(tr("Problem with rename"), - tr("The name cannot start with a period."), null); - return; - } - - String newExtension = newName.substring(dot+1).toLowerCase(); - if (!validExtension(newExtension)) { - Base.showWarning(tr("Problem with rename"), - I18n.format( - tr("\".{0}\" is not a valid extension."), newExtension - ), null); - return; - } - - // Don't let the user create the main tab as a .java file instead of .pde - if (!isDefaultExtension(newExtension)) { - if (renamingCode) { // If creating a new tab, don't show this error - if (current.getCode() == data.getCode(0)) { // If this is the main tab, disallow - Base.showWarning(tr("Problem with rename"), - tr("The main file can't use an extension.\n" + - "(It may be time for your to graduate to a\n" + - "\"real\" programming environment)"), null); - return; - } - } - } - - // dots are allowed for the .pde and .java, but not in the name - // make sure the user didn't name things poo.time.pde - // or something like that (nothing against poo time) - String shortName = newName.substring(0, dot); - String sanitaryName = BaseNoGui.sanitizeName(shortName); - if (!shortName.equals(sanitaryName)) { - newName = sanitaryName + "." + newExtension; - } - - // In Arduino, we want to allow files with the same name but different - // extensions, so compare the full names (including extensions). This - // might cause problems: http://dev.processing.org/bugs/show_bug.cgi?id=543 - for (SketchCode c : data.getCodes()) { - if (newName.equalsIgnoreCase(c.getFileName()) && OSUtils.isWindows()) { - Base.showMessage(tr("Error"), - I18n.format( - tr("A file named \"{0}\" already exists in \"{1}\""), - c.getFileName(), - data.getFolder().getAbsolutePath() - )); - return; - } - } - - // In Arduino, don't allow a .cpp file with the same name as the sketch, - // because the sketch is concatenated into a file with that name as part - // of the build process. - if (newName.equals(getName() + ".cpp")) { - Base.showMessage(tr("Error"), - tr("You can't have a .cpp file with the same name as the sketch.")); - return; - } - - if (renamingCode && currentIndex == 0) { - for (SketchCode code : data.getCodes()) { - if (sanitaryName.equalsIgnoreCase(code.getPrettyName()) && - code.isExtension("cpp")) { - Base.showMessage(tr("Error"), - I18n.format(tr("You can't rename the sketch to \"{0}\"\n" - + "because the sketch already has a .cpp file with that name."), - sanitaryName)); - return; - } - } - } - - - File newFile = new File(data.getFolder(), newName); -// if (newFile.exists()) { // yay! users will try anything -// Base.showMessage("Error", -// "A file named \"" + newFile + "\" already exists\n" + -// "in \"" + folder.getAbsolutePath() + "\""); -// return; -// } - -// File newFileHidden = new File(folder, newName + ".x"); -// if (newFileHidden.exists()) { -// // don't let them get away with it if they try to create something -// // with the same name as something hidden -// Base.showMessage("No Way", -// "A hidden tab with the same name already exists.\n" + -// "Use \"Unhide\" to bring it back."); -// return; -// } - - if (renamingCode) { - if (currentIndex == 0) { - // get the new folder name/location - String folderName = newName.substring(0, newName.indexOf('.')); - File newFolder = new File(data.getFolder().getParentFile(), folderName); - if (newFolder.exists()) { - Base.showWarning(tr("Cannot Rename"), - I18n.format( - tr("Sorry, a sketch (or folder) named " + - "\"{0}\" already exists."), - newName - ), null); - return; - } - - // unfortunately this can't be a "save as" because that - // only copies the sketch files and the data folder - // however this *will* first save the sketch, then rename - - // first get the contents of the editor text area - if (current.getCode().isModified()) { - current.getCode().setProgram(editor.getText()); - try { - // save this new SketchCode - current.getCode().save(); - } catch (Exception e) { - Base.showWarning(tr("Error"), tr("Could not rename the sketch. (0)"), e); - return; - } - } - - if (!current.getCode().renameTo(newFile)) { - Base.showWarning(tr("Error"), - I18n.format( - tr("Could not rename \"{0}\" to \"{1}\""), - current.getCode().getFileName(), - newFile.getName() - ), null); - return; - } - - // save each of the other tabs because this is gonna be re-opened - try { - for (SketchCode code : data.getCodes()) { - code.save(); - } - } catch (Exception e) { - Base.showWarning(tr("Error"), tr("Could not rename the sketch. (1)"), e); - return; - } - - // now rename the sketch folder and re-open - boolean success = data.getFolder().renameTo(newFolder); - if (!success) { - Base.showWarning(tr("Error"), tr("Could not rename the sketch. (2)"), null); - return; - } - // if successful, set base properties for the sketch - - File newMainFile = new File(newFolder, newName + ".ino"); - - // having saved everything and renamed the folder and the main .pde, - // use the editor to re-open the sketch to re-init state - // (unfortunately this will kill positions for carets etc) - editor.handleOpenUnchecked(newMainFile, - currentIndex, - editor.getSelectionStart(), - editor.getSelectionStop(), - editor.getScrollPosition()); - - // get the changes into the sketchbook menu - // (re-enabled in 0115 to fix bug #332) - editor.base.rebuildSketchbookMenus(); - - } else { // else if something besides code[0] - if (!current.getCode().renameTo(newFile)) { - Base.showWarning(tr("Error"), - I18n.format( - tr("Could not rename \"{0}\" to \"{1}\""), - current.getCode().getFileName(), - newFile.getName() - ), null); - return; - } - } - - } else { // creating a new file - try { - if (!newFile.createNewFile()) { - // Already checking for IOException, so make our own. - throw new IOException(tr("createNewFile() returned false")); - } - } catch (IOException e) { - Base.showWarning(tr("Error"), - I18n.format( - "Could not create the file \"{0}\" in \"{1}\"", - newFile, - data.getFolder().getAbsolutePath() - ), e); - return; - } - ensureExistence(); - data.addCode((new SketchCodeDocument(this, newFile)).getCode()); - } - - // sort the entries - data.sortCode(); - - // set the new guy as current - setCurrentCode(newName); - - // update the tabs - editor.header.rebuild(); - } - - - /** - * Remove a piece of code from the sketch and from the disk. - */ - public void handleDeleteCode() throws IOException { - editor.status.clearState(); - // make sure the user didn't hide the sketch folder - ensureExistence(); - - // if read-only, give an error - if (isReadOnly(BaseNoGui.librariesIndexer.getInstalledLibraries(), BaseNoGui.getExamplesPath())) { - // if the files are read-only, need to first do a "save as". - Base.showMessage(tr("Sketch is Read-Only"), - tr("Some files are marked \"read-only\", so you'll\n" + - "need to re-save the sketch in another location,\n" + - "and try again.")); - return; - } - - // confirm deletion with user, yes/no - Object[] options = { tr("OK"), tr("Cancel") }; - String prompt = (currentIndex == 0) ? - tr("Are you sure you want to delete this sketch?") : - I18n.format(tr("Are you sure you want to delete \"{0}\"?"), current.getCode().getFileNameWithExtensionIfNotIno()); - int result = JOptionPane.showOptionDialog(editor, - prompt, - tr("Delete"), - JOptionPane.YES_NO_OPTION, - JOptionPane.QUESTION_MESSAGE, - null, - options, - options[0]); - if (result == JOptionPane.YES_OPTION) { - if (currentIndex == 0) { - // need to unset all the modified flags, otherwise tries - // to do a save on the handleNew() - - // delete the entire sketch - Base.removeDir(data.getFolder()); - - // get the changes into the sketchbook menu - //sketchbook.rebuildMenus(); - - // make a new sketch, and i think this will rebuild the sketch menu - //editor.handleNewUnchecked(); - //editor.handleClose2(); - editor.base.handleClose(editor); - - } else { - // delete the file - if (!current.getCode().deleteFile(BaseNoGui.getBuildFolder(data).toPath())) { - Base.showMessage(tr("Couldn't do it"), - I18n.format(tr("Could not delete \"{0}\"."), current.getCode().getFileName())); - return; - } - - // remove code from the list - data.removeCode(current.getCode()); - - // just set current tab to the main tab - setCurrentCode(0); - - // update the tabs - editor.header.repaint(); - } - } - } - - - /** - * Move to the previous tab. - */ - public void handlePrevCode() { - int prev = currentIndex - 1; - if (prev < 0) prev = data.getCodeCount()-1; - setCurrentCode(prev); - } - - - /** - * Move to the next tab. - */ - public void handleNextCode() { - setCurrentCode((currentIndex + 1) % data.getCodeCount()); - } - - - /** - * Sets the modified value for the code in the frontmost tab. - */ - public void setModified(boolean state) { - //System.out.println("setting modified to " + state); - //new Exception().printStackTrace(); - current.getCode().setModified(state); - calcModified(); - } - - - private void calcModified() { - modified = false; - for (SketchCode code : data.getCodes()) { - if (code.isModified()) { - modified = true; - break; - } - } - editor.header.repaint(); - - if (OSUtils.isMacOS()) { - // http://developer.apple.com/qa/qa2001/qa1146.html - Object modifiedParam = modified ? Boolean.TRUE : Boolean.FALSE; - editor.getRootPane().putClientProperty("windowModified", modifiedParam); - editor.getRootPane().putClientProperty("Window.documentModified", modifiedParam); - } - } - - - public boolean isModified() { - return modified; - } - - - /** - * Save all code in the current sketch. - */ - public boolean save() throws IOException { - // make sure the user didn't hide the sketch folder - ensureExistence(); - - // first get the contents of the editor text area - if (current.getCode().isModified()) { - current.getCode().setProgram(editor.getText()); - } - - // don't do anything if not actually modified - //if (!modified) return false; - - if (isReadOnly(BaseNoGui.librariesIndexer.getInstalledLibraries(), BaseNoGui.getExamplesPath())) { - Base.showMessage(tr("Sketch is read-only"), - tr("Some files are marked \"read-only\", so you'll\n" + - "need to re-save this sketch to another location.")); - return saveAs(); - } - - // rename .pde files to .ino - File mainFile = new File(getMainFilePath()); - File mainFolder = mainFile.getParentFile(); - File[] pdeFiles = mainFolder.listFiles((dir, name) -> { - return name.toLowerCase().endsWith(".pde"); - }); - - if (pdeFiles != null && pdeFiles.length > 0) { - if (PreferencesData.get("editor.update_extension") == null) { - Object[] options = {tr("OK"), tr("Cancel")}; - int result = JOptionPane.showOptionDialog(editor, - tr("In Arduino 1.0, the default file extension has changed\n" + - "from .pde to .ino. New sketches (including those created\n" + - "by \"Save-As\") will use the new extension. The extension\n" + - "of existing sketches will be updated on save, but you can\n" + - "disable this in the Preferences dialog.\n" + - "\n" + - "Save sketch and update its extension?"), - tr(".pde -> .ino"), - JOptionPane.OK_CANCEL_OPTION, - JOptionPane.QUESTION_MESSAGE, - null, - options, - options[0]); - - if (result != JOptionPane.OK_OPTION) return false; // save cancelled - - PreferencesData.setBoolean("editor.update_extension", true); - } - - if (PreferencesData.getBoolean("editor.update_extension")) { - // Do rename of all .pde files to new .ino extension - for (File pdeFile : pdeFiles) - renameCodeToInoExtension(pdeFile); - } - } - - data.save(); - calcModified(); - return true; - } - - - private boolean renameCodeToInoExtension(File pdeFile) { - for (SketchCode c : data.getCodes()) { - if (!c.getFile().equals(pdeFile)) - continue; - - String pdeName = pdeFile.getPath(); - pdeName = pdeName.substring(0, pdeName.length() - 4) + ".ino"; - return c.renameTo(new File(pdeName)); - } - return false; - } - - - /** - * Handles 'Save As' for a sketch. - *

- * This basically just duplicates the current sketch folder to - * a new location, and then calls 'Save'. (needs to take the current - * state of the open files and save them to the new folder.. - * but not save over the old versions for the old sketch..) - *

- * Also removes the previously-generated .class and .jar files, - * because they can cause trouble. - */ - protected boolean saveAs() throws IOException { - // get new name for folder - FileDialog fd = new FileDialog(editor, tr("Save sketch folder as..."), FileDialog.SAVE); - if (isReadOnly(BaseNoGui.librariesIndexer.getInstalledLibraries(), BaseNoGui.getExamplesPath()) || isUntitled()) { - // default to the sketchbook folder - fd.setDirectory(BaseNoGui.getSketchbookFolder().getAbsolutePath()); - } else { - // default to the parent folder of where this was - // on macs a .getParentFile() method is required - - fd.setDirectory(data.getFolder().getParentFile().getAbsolutePath()); - } - String oldName = data.getName(); - fd.setFile(oldName); - - fd.setVisible(true); - String newParentDir = fd.getDirectory(); - String newName = fd.getFile(); - - // user canceled selection - if (newName == null) return false; - newName = Sketch.checkName(newName); - - File newFolder = new File(newParentDir, newName); - - // make sure there doesn't exist a .cpp file with that name already - // but ignore this situation for the first tab, since it's probably being - // resaved (with the same name) to another location/folder. - for (int i = 1; i < data.getCodeCount(); i++) { - SketchCode code = data.getCode(i); - if (newName.equalsIgnoreCase(code.getPrettyName())) { - Base.showMessage(tr("Error"), - I18n.format(tr("You can't save the sketch as \"{0}\"\n" + - "because the sketch already has a file with that name."), newName - )); - return false; - } - } - - // check if the paths are identical - if (newFolder.equals(data.getFolder())) { - // just use "save" here instead, because the user will have received a - // message (from the operating system) about "do you want to replace?" - return save(); - } - - // check to see if the user is trying to save this sketch inside itself - try { - String newPath = newFolder.getCanonicalPath() + File.separator; - String oldPath = data.getFolder().getCanonicalPath() + File.separator; - - if (newPath.indexOf(oldPath) == 0) { - Base.showWarning(tr("How very Borges of you"), - tr("You cannot save the sketch into a folder\n" + - "inside itself. This would go on forever."), null); - return false; - } - } catch (IOException e) { - //ignore - } - - // if the new folder already exists, then need to remove - // its contents before copying everything over - // (user will have already been warned) - if (newFolder.exists()) { - Base.removeDir(newFolder); - } - // in fact, you can't do this on windows because the file dialog - // will instead put you inside the folder, but it happens on osx a lot. - - // now make a fresh copy of the folder - newFolder.mkdirs(); - - // grab the contents of the current tab before saving - // first get the contents of the editor text area - if (current.getCode().isModified()) { - current.getCode().setProgram(editor.getText()); - } - - // save the other tabs to their new location - for (SketchCode code : data.getCodes()) { - if (data.indexOfCode(code) == 0) continue; - File newFile = new File(newFolder, code.getFileName()); - code.saveAs(newFile); - } - - // re-copy the data folder (this may take a while.. add progress bar?) - if (data.getDataFolder().exists()) { - File newDataFolder = new File(newFolder, "data"); - Base.copyDir(data.getDataFolder(), newDataFolder); - } - - // re-copy the code folder - if (data.getCodeFolder().exists()) { - File newCodeFolder = new File(newFolder, "code"); - Base.copyDir(data.getCodeFolder(), newCodeFolder); - } - - // copy custom applet.html file if one exists - // http://dev.processing.org/bugs/show_bug.cgi?id=485 - File customHtml = new File(data.getFolder(), "applet.html"); - if (customHtml.exists()) { - File newHtml = new File(newFolder, "applet.html"); - Base.copyFile(customHtml, newHtml); - } - - // save the main tab with its new name - File newFile = new File(newFolder, newName + ".ino"); - data.getCode(0).saveAs(newFile); - - editor.handleOpenUnchecked(newFile, - currentIndex, - editor.getSelectionStart(), - editor.getSelectionStop(), - editor.getScrollPosition()); - - // Name changed, rebuild the sketch menus - //editor.sketchbook.rebuildMenusAsync(); - editor.base.rebuildSketchbookMenus(); - - // Make sure that it's not an untitled sketch - setUntitled(false); - - // let Editor know that the save was successful - return true; - } - - - /** - * Prompt the user for a new file to the sketch, then call the - * other addFile() function to actually add it. - */ - public void handleAddFile() { - // make sure the user didn't hide the sketch folder - ensureExistence(); - - // if read-only, give an error - if (isReadOnly(BaseNoGui.librariesIndexer.getInstalledLibraries(), BaseNoGui.getExamplesPath())) { - // if the files are read-only, need to first do a "save as". - Base.showMessage(tr("Sketch is Read-Only"), - tr("Some files are marked \"read-only\", so you'll\n" + - "need to re-save the sketch in another location,\n" + - "and try again.")); - return; - } - - // get a dialog, select a file to add to the sketch - FileDialog fd = new FileDialog(editor, tr("Select an image or other data file to copy to your sketch"), FileDialog.LOAD); - fd.setVisible(true); - - String directory = fd.getDirectory(); - String filename = fd.getFile(); - if (filename == null) return; - - // copy the file into the folder. if people would rather - // it move instead of copy, they can do it by hand - File sourceFile = new File(directory, filename); - - // now do the work of adding the file - boolean result = addFile(sourceFile); - - if (result) { - editor.statusNotice(tr("One file added to the sketch.")); - PreferencesData.set("last.folder", sourceFile.getAbsolutePath()); - } - } - - - /** - * Add a file to the sketch. - *

- * .pde or .java files will be added to the sketch folder.
- * .jar, .class, .dll, .jnilib, and .so files will all - * be added to the "code" folder.
- * All other files will be added to the "data" folder. - *

- * If they don't exist already, the "code" or "data" folder - * will be created. - *

- * @return true if successful. - */ - public boolean addFile(File sourceFile) { - String filename = sourceFile.getName(); - File destFile = null; - String codeExtension = null; - boolean replacement = false; - - // if the file appears to be code related, drop it - // into the code folder, instead of the data folder - if (filename.toLowerCase().endsWith(".o") || - filename.toLowerCase().endsWith(".a") || - filename.toLowerCase().endsWith(".so")) { - - //if (!codeFolder.exists()) codeFolder.mkdirs(); - prepareCodeFolder(); - destFile = new File(data.getCodeFolder(), filename); - - } else { - for (String extension : SketchData.EXTENSIONS) { - String lower = filename.toLowerCase(); - if (lower.endsWith("." + extension)) { - destFile = new File(data.getFolder(), filename); - codeExtension = extension; - } - } - if (codeExtension == null) { - prepareDataFolder(); - destFile = new File(data.getDataFolder(), filename); - } - } - - // check whether this file already exists - if (destFile.exists()) { - Object[] options = { tr("OK"), tr("Cancel") }; - String prompt = I18n.format(tr("Replace the existing version of {0}?"), filename); - int result = JOptionPane.showOptionDialog(editor, - prompt, - tr("Replace"), - JOptionPane.YES_NO_OPTION, - JOptionPane.QUESTION_MESSAGE, - null, - options, - options[0]); - if (result == JOptionPane.YES_OPTION) { - replacement = true; - } else { - return false; - } - } - - // If it's a replacement, delete the old file first, - // otherwise case changes will not be preserved. - // http://dev.processing.org/bugs/show_bug.cgi?id=969 - if (replacement) { - boolean muchSuccess = destFile.delete(); - if (!muchSuccess) { - Base.showWarning(tr("Error adding file"), - I18n.format(tr("Could not delete the existing ''{0}'' file."), filename), - null); - return false; - } - } - - // make sure they aren't the same file - if ((codeExtension == null) && sourceFile.equals(destFile)) { - Base.showWarning(tr("You can't fool me"), - tr("This file has already been copied to the\n" + - "location from which where you're trying to add it.\n" + - "I ain't not doin nuthin'."), null); - return false; - } - - // in case the user is "adding" the code in an attempt - // to update the sketch's tabs - if (!sourceFile.equals(destFile)) { - try { - Base.copyFile(sourceFile, destFile); - - } catch (IOException e) { - Base.showWarning(tr("Error adding file"), - I18n.format(tr("Could not add ''{0}'' to the sketch."), filename), - e); - return false; - } - } - - if (codeExtension != null) { - SketchCode newCode = (new SketchCodeDocument(this, destFile)).getCode(); - - if (replacement) { - data.replaceCode(newCode); - - } else { - ensureExistence(); - data.addCode(newCode); - data.sortCode(); - } - setCurrentCode(filename); - editor.header.repaint(); - if (editor.untitled) { // TODO probably not necessary? problematic? - // Mark the new code as modified so that the sketch is saved - current.getCode().setModified(true); - } - - } else { - if (editor.untitled) { // TODO probably not necessary? problematic? - // If a file has been added, mark the main code as modified so - // that the sketch is properly saved. - data.getCode(0).setModified(true); - } - } - return true; - } - - - public void importLibrary(UserLibrary lib) throws IOException { - importLibrary(lib.getSrcFolder()); - } - - /** - * Add import statements to the current tab for all of packages inside - * the specified jar file. - */ - private void importLibrary(File jarPath) throws IOException { - // make sure the user didn't hide the sketch folder - ensureExistence(); - - String list[] = Base.headerListFromIncludePath(jarPath); - if (list == null || list.length == 0) { - return; - } - - // import statements into the main sketch file (code[0]) - // if the current code is a .java file, insert into current - //if (current.flavor == PDE) { - if (hasDefaultExtension(current.getCode())) { - setCurrentCode(0); - } - // could also scan the text in the file to see if each import - // statement is already in there, but if the user has the import - // commented out, then this will be a problem. - StringBuilder buffer = new StringBuilder(); - for (String aList : list) { - buffer.append("#include <"); - buffer.append(aList); - buffer.append(">\n"); - } - buffer.append('\n'); - buffer.append(editor.getText()); - editor.setText(buffer.toString()); - editor.setSelection(0, 0); // scroll to start - setModified(true); - } - - - /** - * Change what file is currently being edited. Changes the current tab index. - *

    - *
  1. store the String for the text of the current file. - *
  2. retrieve the String for the text of the new file. - *
  3. change the text that's visible in the text area - *
- */ - public void setCurrentCode(int which) { - setCurrentCode(which, false); - } - - private void setCurrentCode(int which, boolean forceUpdate) { - // if current is null, then this is the first setCurrent(0) - if (!forceUpdate && (currentIndex == which) && (current != null)) { - return; - } - - // get the text currently being edited - if (current != null) { - current.getCode().setProgram(editor.getText()); - current.setSelectionStart(editor.getSelectionStart()); - current.setSelectionStop(editor.getSelectionStop()); - current.setScrollPosition(editor.getScrollPosition()); - } - - current = (SketchCodeDocument) data.getCode(which).getMetadata(); - currentIndex = which; - - if (SwingUtilities.isEventDispatchThread()) { - editor.setCode(current); - } else { - try { - SwingUtilities.invokeAndWait(() -> editor.setCode(current)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - editor.header.rebuild(); - } - - - /** - * Internal helper function to set the current tab based on a name. - * @param findName the file name (not pretty name) to be shown - */ - protected void setCurrentCode(String findName) { - for (SketchCode code : data.getCodes()) { - if (findName.equals(code.getFileName()) || - findName.equals(code.getPrettyName())) { - setCurrentCode(data.indexOfCode(code)); - return; - } - } - } - - - /** - * Preprocess, Compile, and Run the current code. - *

- * There are three main parts to this process: - *

-   *   (0. if not java, then use another 'engine'.. i.e. python)
-   *
-   *    1. do the p5 language preprocessing
-   *       this creates a working .java file in a specific location
-   *       better yet, just takes a chunk of java code and returns a
-   *       new/better string editor can take care of saving this to a
-   *       file location
-   *
-   *    2. compile the code from that location
-   *       catching errors along the way
-   *       placing it in a ready classpath, or .. ?
-   *
-   *    3. run the code
-   *       needs to communicate location for window
-   *       and maybe setup presentation space as well
-   *       run externally if a code folder exists,
-   *       or if more than one file is in the project
-   *
-   *    X. afterwards, some of these steps need a cleanup function
-   * 
- */ - //protected String compile() throws RunnerException { - - /** - * When running from the editor, take care of preparations before running - * the build. - */ - public void prepare() throws IOException { - // make sure the user didn't hide the sketch folder - ensureExistence(); - - current.getCode().setProgram(editor.getText()); - - // TODO record history here - //current.history.record(program, SketchHistory.RUN); - - // if an external editor is being used, need to grab the - // latest version of the code from the file. - if (PreferencesData.getBoolean("editor.external")) { - // history gets screwed by the open.. - //String historySaved = history.lastRecorded; - //handleOpen(sketch); - //history.lastRecorded = historySaved; - - // nuke previous files and settings, just get things loaded - load(true); - } - -// // handle preprocessing the main file's code -// return build(tempBuildFolder.getAbsolutePath()); - } - - /** - * Run the build inside the temporary build folder. - * @return null if compilation failed, main class name if not - * @throws RunnerException - */ - public String build(boolean verbose, boolean save) throws RunnerException, PreferencesMapException, IOException { - return build(BaseNoGui.getBuildFolder(data).getAbsolutePath(), verbose, save); - } - - /** - * Preprocess and compile all the code for this sketch. - * - * In an advanced program, the returned class name could be different, - * which is why the className is set based on the return value. - * A compilation error will burp up a RunnerException. - * - * @return null if compilation failed, main class name if not - */ - private String build(String buildPath, boolean verbose, boolean save) throws RunnerException, PreferencesMapException, IOException { - // run the preprocessor - editor.status.progressUpdate(20); - - ensureExistence(); - - CompilerProgressListener progressListener = editor.status::progressUpdate; - - boolean deleteTemp = false; - String pathToSketch = data.getMainFilePath(); - if (isModified()) { - // If any files are modified, make a copy of the sketch with the changes - // saved, so arduino-builder will see the modifications. - pathToSketch = saveSketchInTempFolder(); - deleteTemp = true; - } - - try { - return new Compiler(pathToSketch, data, buildPath).build(progressListener, - save); - } finally { - // Make sure we clean up any temporary sketch copy - if (deleteTemp) - FileUtils.recursiveDelete(new File(pathToSketch).getParentFile()); - } - } - - private String saveSketchInTempFolder() throws IOException { - File tempFolder = FileUtils.createTempFolder("arduino_modified_sketch_"); - FileUtils.copy(getFolder(), tempFolder); - - for (SketchCode sc : Stream.of(data.getCodes()).filter(SketchCode::isModified).collect(Collectors.toList())) { - Files.write(Paths.get(tempFolder.getAbsolutePath(), sc.getFileName()), sc.getProgram().getBytes()); - } - - return Paths.get(tempFolder.getAbsolutePath(), data.getPrimaryFile().getName()).toString(); - } - - protected boolean exportApplet(boolean usingProgrammer) throws Exception { - return exportApplet(BaseNoGui.getBuildFolder(data).getAbsolutePath(), usingProgrammer); - } - - - /** - * Handle export to applet. - */ - private boolean exportApplet(String appletPath, boolean usingProgrammer) - throws Exception { - - prepare(); - - // build the sketch - editor.status.progressNotice(tr("Compiling sketch...")); - String foundName = build(appletPath, false, false); - // (already reported) error during export, exit this function - if (foundName == null) return false; - -// // If name != exportSketchName, then that's weirdness -// // BUG unfortunately, that can also be a bug in the preproc :( -// if (!name.equals(foundName)) { -// Base.showWarning("Error during export", -// "Sketch name is " + name + " but the sketch\n" + -// "name in the code was " + foundName, null); -// return false; -// } - - editor.status.progressNotice(tr("Uploading...")); - boolean success = upload(appletPath, foundName, usingProgrammer); - editor.status.progressUpdate(100); - return success; - } - - private boolean upload(String buildPath, String suggestedClassName, boolean usingProgrammer) throws Exception { - - Uploader uploader = new UploaderUtils().getUploaderByPreferences(false); - - boolean success = false; - do { - if (uploader.requiresAuthorization() && !PreferencesData.has(uploader.getAuthorizationKey())) { - PasswordAuthorizationDialog dialog = new PasswordAuthorizationDialog(editor, tr("Type board password to upload a new sketch")); - dialog.setLocationRelativeTo(editor); - dialog.setVisible(true); - - if (dialog.isCancelled()) { - editor.statusNotice(tr("Upload cancelled")); - return false; - } - - PreferencesData.set(uploader.getAuthorizationKey(), dialog.getPassword()); - } - - List warningsAccumulator = new LinkedList<>(); - try { - success = new UploaderUtils().upload(data, uploader, buildPath, suggestedClassName, usingProgrammer, false, warningsAccumulator); - } finally { - if (uploader.requiresAuthorization() && !success) { - PreferencesData.remove(uploader.getAuthorizationKey()); - } - } - - for (String warning : warningsAccumulator) { - System.out.print(tr("Warning")); - System.out.print(": "); - System.out.println(warning); - } - - } while (uploader.requiresAuthorization() && !success); - - return success; - } - - /** - * Make sure the sketch hasn't been moved or deleted by some - * nefarious user. If they did, try to re-create it and save. - * Only checks to see if the main folder is still around, - * but not its contents. - */ - private void ensureExistence() { - if (data.getFolder().exists()) return; - - Base.showWarning(tr("Sketch Disappeared"), - tr("The sketch folder has disappeared.\n " + - "Will attempt to re-save in the same location,\n" + - "but anything besides the code will be lost."), null); - try { - data.getFolder().mkdirs(); - modified = true; - - for (SketchCode code : data.getCodes()) { - code.save(); // this will force a save - } - calcModified(); - - } catch (Exception e) { - Base.showWarning(tr("Could not re-save sketch"), - tr("Could not properly re-save the sketch. " + - "You may be in trouble at this point,\n" + - "and it might be time to copy and paste " + - "your code to another text editor."), e); - } - } - - - /** - * Returns true if this is a read-only sketch. Used for the - * examples directory, or when sketches are loaded from read-only - * volumes or folders without appropriate permissions. - */ - public boolean isReadOnly(LibraryList libraries, String examplesPath) { - String apath = data.getFolder().getAbsolutePath(); - - Optional libraryThatIncludesSketch = libraries.stream().filter(lib -> apath.startsWith(lib.getInstalledFolder().getAbsolutePath())).findFirst(); - if (libraryThatIncludesSketch.isPresent() && !libraryThatIncludesSketch.get().onGoingDevelopment()) { - return true; - } - - return sketchIsSystemExample(apath, examplesPath) || sketchFilesAreReadOnly(); - } - - private boolean sketchIsSystemExample(String apath, String examplesPath) { - return apath.startsWith(examplesPath); - } - - private boolean sketchFilesAreReadOnly() { - for (SketchCode code : data.getCodes()) { - if (code.isModified() && code.fileReadOnly() && code.fileExists()) { - return true; - } - } - return false; - } - - // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . - - // Breaking out extension types in order to clean up the code, and make it - // easier for other environments (like Arduino) to incorporate changes. - - /** - * True if the specified code has the default file extension. - */ - private boolean hasDefaultExtension(SketchCode code) { - return code.isExtension(getDefaultExtension()); - } - - - /** - * True if the specified extension is the default file extension. - */ - private boolean isDefaultExtension(String what) { - return what.equals(getDefaultExtension()); - } - - - /** - * Check this extension (no dots, please) against the list of valid - * extensions. - */ - private boolean validExtension(String what) { - return SketchData.EXTENSIONS.contains(what); - } - - - /** - * Returns the default extension for this editor setup. - */ - public String getDefaultExtension() { - return data.getDefaultExtension(); - } - - static private final List hiddenExtensions = Arrays.asList("ino", "pde"); - - public List getHiddenExtensions() { - return hiddenExtensions; - } - - - // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . - - // Additional accessors added in 0136 because of package work. - // These will also be helpful for tool developers. - - - /** - * Returns the name of this sketch. (The pretty name of the main tab.) - */ - public String getName() { - return data.getName(); - } - - - /** - * Returns path to the main .pde file for this sketch. - */ - public String getMainFilePath() { - return data.getMainFilePath(); - } - - - /** - * Returns the sketch folder. - */ - public File getFolder() { - return data.getFolder(); - } - - - /** - * Create the data folder if it does not exist already. As a convenience, - * it also returns the data folder, since it's likely about to be used. - */ - private File prepareDataFolder() { - if (!data.getDataFolder().exists()) { - data.getDataFolder().mkdirs(); - } - return data.getDataFolder(); - } - - - /** - * Create the code folder if it does not exist already. As a convenience, - * it also returns the code folder, since it's likely about to be used. - */ - private File prepareCodeFolder() { - if (!data.getCodeFolder().exists()) { - data.getCodeFolder().mkdirs(); - } - return data.getCodeFolder(); - } - - - public SketchCode[] getCodes() { - return data.getCodes(); - } - - - public int getCodeCount() { - return data.getCodeCount(); - } - - - public SketchCode getCode(int index) { - return data.getCode(index); - } - - - public int getCodeIndex(SketchCode who) { - return data.indexOfCode(who); - } - - - public SketchCode getCurrentCode() { - return current.getCode(); - } - - - private void setUntitled(boolean u) { - editor.untitled = u; - } - - - public boolean isUntitled() { - return editor.untitled; - } - - - // ................................................................. - - - /** - * Convert to sanitized name and alert the user - * if changes were made. - */ - private static String checkName(String origName) { - String newName = BaseNoGui.sanitizeName(origName); - - if (!newName.equals(origName)) { - String msg = - tr("The sketch name had to be modified. Sketch names can only consist\n" + - "of ASCII characters and numbers (but cannot start with a number).\n" + - "They should also be less than 64 characters long."); - System.out.println(msg); - } - return newName; - } - - -} diff --git a/app/src/processing/app/SketchCodeDocument.java b/app/src/processing/app/SketchCodeDocument.java deleted file mode 100644 index 681f0af9151..00000000000 --- a/app/src/processing/app/SketchCodeDocument.java +++ /dev/null @@ -1,103 +0,0 @@ -package processing.app; - -import java.io.File; - -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; -import javax.swing.text.Document; -import javax.swing.undo.UndoManager; - -public class SketchCodeDocument implements DocumentListener{ - - private SketchCode code; - private Sketch sketch; - private Document document; - - // Undo Manager for this tab, each tab keeps track of their own Editor.undo - // will be set to this object when this code is the tab that's currently the - // front. - private UndoManager undo; - - // saved positions from last time this tab was used - private int selectionStart; - private int selectionStop; - private int scrollPosition; - - public SketchCodeDocument(Sketch sketch, SketchCode code) { - this.code = code; - this.sketch = sketch; - this.code.setMetadata(this); - } - - public SketchCodeDocument(Sketch sketch, File file) { - this.code = new SketchCode(file, this); - this.sketch = sketch; - } - - public UndoManager getUndo() { - return undo; - } - - public void setUndo(UndoManager undo) { - this.undo = undo; - } - - public int getSelectionStart() { - return selectionStart; - } - - public void setSelectionStart(int selectionStart) { - this.selectionStart = selectionStart; - } - - public int getSelectionStop() { - return selectionStop; - } - - public void setSelectionStop(int selectionStop) { - this.selectionStop = selectionStop; - } - - public int getScrollPosition() { - return scrollPosition; - } - - public void setScrollPosition(int scrollPosition) { - this.scrollPosition = scrollPosition; - } - - public SketchCode getCode() { - return code; - } - - public void setCode(SketchCode code) { - this.code = code; - } - - public Document getDocument() { - return document; - } - - public void setDocument(Document document) { - this.document = document; - document.addDocumentListener(this); - } - - @Override - public void insertUpdate(DocumentEvent e) { - if(!code.isModified()) sketch.setModified(true); - } - - - @Override - public void removeUpdate(DocumentEvent e) { - if(!code.isModified()) sketch.setModified(true); - } - - @Override - public void changedUpdate(DocumentEvent e) { - // Callback for when styles in the current document change. - // This method is never called. - } - -} diff --git a/app/src/processing/app/SketchController.java b/app/src/processing/app/SketchController.java new file mode 100644 index 00000000000..8c6dd1c8519 --- /dev/null +++ b/app/src/processing/app/SketchController.java @@ -0,0 +1,821 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2004-10 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 as published by + the Free Software Foundation; either version 2 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, write to the Free Software Foundation, + Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +*/ + +package processing.app; + +import cc.arduino.Compiler; +import cc.arduino.CompilerProgressListener; +import cc.arduino.UploaderUtils; +import cc.arduino.packages.Uploader; +import processing.app.debug.RunnerException; +import processing.app.forms.PasswordAuthorizationDialog; +import processing.app.helpers.FileUtils; +import processing.app.helpers.OSUtils; +import processing.app.helpers.PreferencesMapException; +import processing.app.packages.LibraryList; +import processing.app.packages.UserLibrary; + +import javax.swing.*; +import java.awt.*; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static processing.app.I18n.tr; + + +/** + * Handles various tasks related to a sketch, in response to user inter-action. + */ +public class SketchController { + private final Editor editor; + private final Sketch sketch; + + public SketchController(Editor _editor, Sketch _sketch) { + editor = _editor; + sketch = _sketch; + } + + private boolean renamingCode; + + /** + * Handler for the New Code menu option. + */ + public void handleNewCode() { + editor.status.clearState(); + // make sure the user didn't hide the sketch folder + ensureExistence(); + + // if read-only, give an error + if (isReadOnly(BaseNoGui.librariesIndexer.getInstalledLibraries(), BaseNoGui.getExamplesPath())) { + // if the files are read-only, need to first do a "save as". + Base.showMessage(tr("Sketch is Read-Only"), + tr("Some files are marked \"read-only\", so you'll\n" + + "need to re-save the sketch in another location,\n" + + "and try again.")); + return; + } + + renamingCode = false; + editor.status.edit(tr("Name for new file:"), ""); + } + + + /** + * Handler for the Rename Code menu option. + */ + public void handleRenameCode() { + SketchFile current = editor.getCurrentTab().getSketchFile(); + + editor.status.clearState(); + // make sure the user didn't hide the sketch folder + ensureExistence(); + + if (current.isPrimary() && editor.untitled) { + Base.showMessage(tr("Sketch is Untitled"), + tr("How about saving the sketch first \n" + + "before trying to rename it?")); + return; + } + + // if read-only, give an error + if (isReadOnly(BaseNoGui.librariesIndexer.getInstalledLibraries(), BaseNoGui.getExamplesPath())) { + // if the files are read-only, need to first do a "save as". + Base.showMessage(tr("Sketch is Read-Only"), + tr("Some files are marked \"read-only\", so you'll\n" + + "need to re-save the sketch in another location,\n" + + "and try again.")); + return; + } + + // ask for new name of file (internal to window) + // TODO maybe just popup a text area? + renamingCode = true; + String prompt = current.isPrimary() ? + "New name for sketch:" : "New name for file:"; + String oldName = current.getPrettyName(); + editor.status.edit(prompt, oldName); + } + + + /** + * This is called upon return from entering a new file name. + * (that is, from either newCode or renameCode after the prompt) + * This code is almost identical for both the newCode and renameCode + * cases, so they're kept merged except for right in the middle + * where they diverge. + */ + protected void nameCode(String newName) { + // make sure the user didn't hide the sketch folder + ensureExistence(); + + newName = newName.trim(); + if (newName.equals("")) return; + + if (newName.charAt(0) == '.') { + Base.showWarning(tr("Problem with rename"), + tr("The name cannot start with a period."), null); + return; + } + + FileUtils.SplitFile split = FileUtils.splitFilename(newName); + if (split.extension.equals("")) + split.extension = Sketch.DEFAULT_SKETCH_EXTENSION; + + if (!Sketch.EXTENSIONS.contains(split.extension)) { + String msg = I18n.format(tr("\".{0}\" is not a valid extension."), + split.extension); + Base.showWarning(tr("Problem with rename"), msg, null); + return; + } + + // Sanitize name + split.basename = BaseNoGui.sanitizeName(split.basename); + newName = split.join(); + + if (renamingCode) { + SketchFile current = editor.getCurrentTab().getSketchFile(); + + if (current.isPrimary()) { + if (!split.extension.equals(Sketch.DEFAULT_SKETCH_EXTENSION)) { + Base.showWarning(tr("Problem with rename"), + tr("The main file cannot use an extension"), null); + return; + } + + // Primary file, rename the entire sketch + final File parent = sketch.getFolder().getParentFile(); + File newFolder = new File(parent, split.basename); + try { + sketch.renameTo(newFolder); + } catch (IOException e) { + // This does not pass on e, to prevent showing a backtrace for + // "normal" errors. + Base.showWarning(tr("Error"), e.getMessage(), null); + return; + } + + editor.base.rebuildSketchbookMenus(); + } else { + // Non-primary file, rename just that file + try { + current.renameTo(newName); + } catch (IOException e) { + // This does not pass on e, to prevent showing a backtrace for + // "normal" errors. + Base.showWarning(tr("Error"), e.getMessage(), null); + return; + } + } + + } else { // creating a new file + SketchFile file; + try { + file = sketch.addFile(newName); + editor.addTab(file, ""); + } catch (IOException e) { + // This does not pass on e, to prevent showing a backtrace for + // "normal" errors. + Base.showWarning(tr("Error"), e.getMessage(), null); + return; + } + editor.selectTab(editor.findTabIndex(file)); + } + + // update the tabs + editor.header.rebuild(); + } + + + /** + * Remove a piece of code from the sketch and from the disk. + */ + public void handleDeleteCode() throws IOException { + SketchFile current = editor.getCurrentTab().getSketchFile(); + editor.status.clearState(); + // make sure the user didn't hide the sketch folder + ensureExistence(); + + // if read-only, give an error + if (isReadOnly(BaseNoGui.librariesIndexer.getInstalledLibraries(), BaseNoGui.getExamplesPath())) { + // if the files are read-only, need to first do a "save as". + Base.showMessage(tr("Sketch is Read-Only"), + tr("Some files are marked \"read-only\", so you'll\n" + + "need to re-save the sketch in another location,\n" + + "and try again.")); + return; + } + + // confirm deletion with user, yes/no + Object[] options = { tr("OK"), tr("Cancel") }; + String prompt = current.isPrimary() ? + tr("Are you sure you want to delete this sketch?") : + I18n.format(tr("Are you sure you want to delete \"{0}\"?"), + current.getPrettyName()); + int result = JOptionPane.showOptionDialog(editor, + prompt, + tr("Delete"), + JOptionPane.YES_NO_OPTION, + JOptionPane.QUESTION_MESSAGE, + null, + options, + options[0]); + if (result == JOptionPane.YES_OPTION) { + if (current.isPrimary()) { + sketch.delete(); + editor.base.handleClose(editor); + } else { + // delete the file + if (!current.delete(sketch.getBuildPath().toPath())) { + Base.showMessage(tr("Couldn't do it"), + I18n.format(tr("Could not delete \"{0}\"."), current.getFileName())); + return; + } + + // just set current tab to the main tab + editor.selectTab(0); + + // update the tabs + editor.header.repaint(); + } + } + } + + /** + * Called whenever the modification status of one of the tabs changes. TODO: + * Move this code into Editor and improve decoupling from EditorTab + */ + public void calcModified() { + editor.header.repaint(); + + if (OSUtils.isMacOS()) { + // http://developer.apple.com/qa/qa2001/qa1146.html + Object modifiedParam = sketch.isModified() ? Boolean.TRUE : Boolean.FALSE; + editor.getRootPane().putClientProperty("windowModified", modifiedParam); + editor.getRootPane().putClientProperty("Window.documentModified", modifiedParam); + } + } + + + + /** + * Save all code in the current sketch. + */ + public boolean save() throws IOException { + // make sure the user didn't hide the sketch folder + ensureExistence(); + + if (isReadOnly(BaseNoGui.librariesIndexer.getInstalledLibraries(), BaseNoGui.getExamplesPath())) { + Base.showMessage(tr("Sketch is read-only"), + tr("Some files are marked \"read-only\", so you'll\n" + + "need to re-save this sketch to another location.")); + return saveAs(); + } + + // rename .pde files to .ino + List oldFiles = new ArrayList<>(); + for (SketchFile file : sketch.getFiles()) { + if (file.isExtension(Sketch.OLD_SKETCH_EXTENSIONS)) + oldFiles.add(file); + } + + if (oldFiles.size() > 0) { + if (PreferencesData.get("editor.update_extension") == null) { + Object[] options = {tr("OK"), tr("Cancel")}; + int result = JOptionPane.showOptionDialog(editor, + tr("In Arduino 1.0, the default file extension has changed\n" + + "from .pde to .ino. New sketches (including those created\n" + + "by \"Save-As\") will use the new extension. The extension\n" + + "of existing sketches will be updated on save, but you can\n" + + "disable this in the Preferences dialog.\n" + + "\n" + + "Save sketch and update its extension?"), + tr(".pde -> .ino"), + JOptionPane.OK_CANCEL_OPTION, + JOptionPane.QUESTION_MESSAGE, + null, + options, + options[0]); + + if (result != JOptionPane.OK_OPTION) return false; // save cancelled + + PreferencesData.setBoolean("editor.update_extension", true); + } + + if (PreferencesData.getBoolean("editor.update_extension")) { + // Do rename of all .pde files to new .ino extension + for (SketchFile file : oldFiles) { + File newName = FileUtils.replaceExtension(file.getFile(), Sketch.DEFAULT_SKETCH_EXTENSION); + file.renameTo(newName.getName()); + } + } + } + + sketch.save(); + return true; + } + + /** + * Handles 'Save As' for a sketch. + *

+ * This basically just duplicates the current sketch folder to + * a new location, and then calls 'Save'. (needs to take the current + * state of the open files and save them to the new folder.. + * but not save over the old versions for the old sketch..) + *

+ * Also removes the previously-generated .class and .jar files, + * because they can cause trouble. + */ + protected boolean saveAs() throws IOException { + // get new name for folder + FileDialog fd = new FileDialog(editor, tr("Save sketch folder as..."), FileDialog.SAVE); + if (isReadOnly(BaseNoGui.librariesIndexer.getInstalledLibraries(), BaseNoGui.getExamplesPath()) || isUntitled()) { + // default to the sketchbook folder + fd.setDirectory(BaseNoGui.getSketchbookFolder().getAbsolutePath()); + } else { + // default to the parent folder of where this was + // on macs a .getParentFile() method is required + + fd.setDirectory(sketch.getFolder().getParentFile().getAbsolutePath()); + } + String oldName = sketch.getName(); + fd.setFile(oldName); + + fd.setVisible(true); + String newParentDir = fd.getDirectory(); + String newName = fd.getFile(); + + // user canceled selection + if (newName == null) return false; + newName = SketchController.checkName(newName); + + File newFolder = new File(newParentDir, newName); + + // check if the paths are identical + if (newFolder.equals(sketch.getFolder())) { + // just use "save" here instead, because the user will have received a + // message (from the operating system) about "do you want to replace?" + return save(); + } + + // check to see if the user is trying to save this sketch inside itself + try { + String newPath = newFolder.getCanonicalPath() + File.separator; + String oldPath = sketch.getFolder().getCanonicalPath() + File.separator; + + if (newPath.indexOf(oldPath) == 0) { + Base.showWarning(tr("How very Borges of you"), + tr("You cannot save the sketch into a folder\n" + + "inside itself. This would go on forever."), null); + return false; + } + } catch (IOException e) { + //ignore + } + + // if the new folder already exists, then need to remove + // its contents before copying everything over + // (user will have already been warned) + if (newFolder.exists()) { + FileUtils.recursiveDelete(newFolder); + } + // in fact, you can't do this on windows because the file dialog + // will instead put you inside the folder, but it happens on osx a lot. + + try { + sketch.saveAs(newFolder); + } catch (IOException e) { + // This does not pass on e, to prevent showing a backtrace for "normal" + // errors. + Base.showWarning(tr("Error"), e.getMessage(), null); + } + // Name changed, rebuild the sketch menus + //editor.sketchbook.rebuildMenusAsync(); + editor.base.rebuildSketchbookMenus(); + editor.header.rebuild(); + + // Make sure that it's not an untitled sketch + setUntitled(false); + + // let Editor know that the save was successful + return true; + } + + + /** + * Prompt the user for a new file to the sketch, then call the + * other addFile() function to actually add it. + */ + public void handleAddFile() { + // make sure the user didn't hide the sketch folder + ensureExistence(); + + // if read-only, give an error + if (isReadOnly(BaseNoGui.librariesIndexer.getInstalledLibraries(), BaseNoGui.getExamplesPath())) { + // if the files are read-only, need to first do a "save as". + Base.showMessage(tr("Sketch is Read-Only"), + tr("Some files are marked \"read-only\", so you'll\n" + + "need to re-save the sketch in another location,\n" + + "and try again.")); + return; + } + + // get a dialog, select a file to add to the sketch + FileDialog fd = new FileDialog(editor, tr("Select an image or other data file to copy to your sketch"), FileDialog.LOAD); + fd.setVisible(true); + + String directory = fd.getDirectory(); + String filename = fd.getFile(); + if (filename == null) return; + + // copy the file into the folder. if people would rather + // it move instead of copy, they can do it by hand + File sourceFile = new File(directory, filename); + + // now do the work of adding the file + boolean result = addFile(sourceFile); + + if (result) { + editor.statusNotice(tr("One file added to the sketch.")); + PreferencesData.set("last.folder", sourceFile.getAbsolutePath()); + } + } + + + /** + * Add a file to the sketch. + * + * Supported code files will be copied to the sketch folder. All other files + * will be copied to the "data" folder (which is created if it does not exist + * yet). + * + * @return true if successful. + */ + public boolean addFile(File sourceFile) { + String filename = sourceFile.getName(); + File destFile = null; + boolean isData = false; + boolean replacement = false; + + if (FileUtils.hasExtension(sourceFile, Sketch.EXTENSIONS)) { + destFile = new File(sketch.getFolder(), filename); + } else { + sketch.prepareDataFolder(); + destFile = new File(sketch.getDataFolder(), filename); + isData = true; + } + + // check whether this file already exists + if (destFile.exists()) { + Object[] options = { tr("OK"), tr("Cancel") }; + String prompt = I18n.format(tr("Replace the existing version of {0}?"), filename); + int result = JOptionPane.showOptionDialog(editor, + prompt, + tr("Replace"), + JOptionPane.YES_NO_OPTION, + JOptionPane.QUESTION_MESSAGE, + null, + options, + options[0]); + if (result == JOptionPane.YES_OPTION) { + replacement = true; + } else { + return false; + } + } + + // If it's a replacement, delete the old file first, + // otherwise case changes will not be preserved. + // http://dev.processing.org/bugs/show_bug.cgi?id=969 + if (replacement) { + boolean muchSuccess = destFile.delete(); + if (!muchSuccess) { + Base.showWarning(tr("Error adding file"), + I18n.format(tr("Could not delete the existing ''{0}'' file."), filename), + null); + return false; + } + } + + // make sure they aren't the same file + if (isData && sourceFile.equals(destFile)) { + Base.showWarning(tr("You can't fool me"), + tr("This file has already been copied to the\n" + + "location from which where you're trying to add it.\n" + + "I ain't not doin nuthin'."), null); + return false; + } + + // in case the user is "adding" the code in an attempt + // to update the sketch's tabs + if (!sourceFile.equals(destFile)) { + try { + Base.copyFile(sourceFile, destFile); + + } catch (IOException e) { + Base.showWarning(tr("Error adding file"), + I18n.format(tr("Could not add ''{0}'' to the sketch."), filename), + e); + return false; + } + } + + if (!isData) { + int tabIndex; + if (replacement) { + tabIndex = editor.findTabIndex(destFile); + editor.getTabs().get(tabIndex).reload(); + } else { + SketchFile sketchFile; + try { + sketchFile = sketch.addFile(destFile.getName()); + editor.addTab(sketchFile, null); + } catch (IOException e) { + // This does not pass on e, to prevent showing a backtrace for + // "normal" errors. + Base.showWarning(tr("Error"), e.getMessage(), null); + return false; + } + tabIndex = editor.findTabIndex(sketchFile); + } + editor.selectTab(tabIndex); + } + return true; + } + + + public void importLibrary(UserLibrary lib) throws IOException { + importLibrary(lib.getSrcFolder()); + } + + /** + * Add import statements to the current tab for all of packages inside + * the specified jar file. + */ + private void importLibrary(File jarPath) throws IOException { + // make sure the user didn't hide the sketch folder + ensureExistence(); + + String list[] = Base.headerListFromIncludePath(jarPath); + if (list == null || list.length == 0) { + return; + } + + // import statements into the main sketch file (code[0]) + // if the current code is a .java file, insert into current + //if (current.flavor == PDE) { + SketchFile file = editor.getCurrentTab().getSketchFile(); + if (file.isExtension(Sketch.SKETCH_EXTENSIONS)) + editor.selectTab(0); + + // could also scan the text in the file to see if each import + // statement is already in there, but if the user has the import + // commented out, then this will be a problem. + StringBuilder buffer = new StringBuilder(); + for (String aList : list) { + buffer.append("#include <"); + buffer.append(aList); + buffer.append(">\n"); + } + buffer.append('\n'); + buffer.append(editor.getCurrentTab().getText()); + editor.getCurrentTab().setText(buffer.toString()); + editor.getCurrentTab().setSelection(0, 0); // scroll to start + } + + /** + * Preprocess and compile all the code for this sketch. + * + * In an advanced program, the returned class name could be different, + * which is why the className is set based on the return value. + * A compilation error will burp up a RunnerException. + * + * @return null if compilation failed, main class name if not + */ + public String build(boolean verbose, boolean save) throws RunnerException, PreferencesMapException, IOException { + // run the preprocessor + editor.status.progressUpdate(20); + + ensureExistence(); + + CompilerProgressListener progressListener = editor.status::progressUpdate; + + boolean deleteTemp = false; + File pathToSketch = sketch.getPrimaryFile().getFile(); + if (sketch.isModified()) { + // If any files are modified, make a copy of the sketch with the changes + // saved, so arduino-builder will see the modifications. + pathToSketch = saveSketchInTempFolder(); + deleteTemp = true; + } + + try { + return new Compiler(pathToSketch, sketch).build(progressListener, save); + } finally { + // Make sure we clean up any temporary sketch copy + if (deleteTemp) + FileUtils.recursiveDelete(pathToSketch.getParentFile()); + } + } + + private File saveSketchInTempFolder() throws IOException { + File tempFolder = FileUtils.createTempFolder("arduino_modified_sketch_"); + FileUtils.copy(sketch.getFolder(), tempFolder); + + for (SketchFile file : Stream.of(sketch.getFiles()).filter(SketchFile::isModified).collect(Collectors.toList())) { + Files.write(Paths.get(tempFolder.getAbsolutePath(), file.getFileName()), file.getProgram().getBytes()); + } + + return Paths.get(tempFolder.getAbsolutePath(), sketch.getPrimaryFile().getFileName()).toFile(); + } + + /** + * Handle export to applet. + */ + protected boolean exportApplet(boolean usingProgrammer) throws Exception { + // build the sketch + editor.status.progressNotice(tr("Compiling sketch...")); + String foundName = build(false, false); + // (already reported) error during export, exit this function + if (foundName == null) return false; + +// // If name != exportSketchName, then that's weirdness +// // BUG unfortunately, that can also be a bug in the preproc :( +// if (!name.equals(foundName)) { +// Base.showWarning("Error during export", +// "Sketch name is " + name + " but the sketch\n" + +// "name in the code was " + foundName, null); +// return false; +// } + + editor.status.progressNotice(tr("Uploading...")); + boolean success = upload(foundName, usingProgrammer); + editor.status.progressUpdate(100); + return success; + } + + private boolean upload(String suggestedClassName, boolean usingProgrammer) throws Exception { + + Uploader uploader = new UploaderUtils().getUploaderByPreferences(false); + + boolean success = false; + do { + if (uploader.requiresAuthorization() && !PreferencesData.has(uploader.getAuthorizationKey())) { + PasswordAuthorizationDialog dialog = new PasswordAuthorizationDialog(editor, tr("Type board password to upload a new sketch")); + dialog.setLocationRelativeTo(editor); + dialog.setVisible(true); + + if (dialog.isCancelled()) { + editor.statusNotice(tr("Upload cancelled")); + return false; + } + + PreferencesData.set(uploader.getAuthorizationKey(), dialog.getPassword()); + } + + List warningsAccumulator = new LinkedList<>(); + try { + success = new UploaderUtils().upload(sketch, uploader, suggestedClassName, usingProgrammer, false, warningsAccumulator); + } finally { + if (uploader.requiresAuthorization() && !success) { + PreferencesData.remove(uploader.getAuthorizationKey()); + } + } + + for (String warning : warningsAccumulator) { + System.out.print(tr("Warning")); + System.out.print(": "); + System.out.println(warning); + } + + } while (uploader.requiresAuthorization() && !success); + + return success; + } + + /** + * Make sure the sketch hasn't been moved or deleted by some + * nefarious user. If they did, try to re-create it and save. + * Only checks to see if the main folder is still around, + * but not its contents. + */ + private void ensureExistence() { + if (sketch.getFolder().exists()) return; + + Base.showWarning(tr("Sketch Disappeared"), + tr("The sketch folder has disappeared.\n " + + "Will attempt to re-save in the same location,\n" + + "but anything besides the code will be lost."), null); + try { + sketch.getFolder().mkdirs(); + + for (SketchFile file : sketch.getFiles()) { + file.save(); // this will force a save + } + calcModified(); + + } catch (Exception e) { + Base.showWarning(tr("Could not re-save sketch"), + tr("Could not properly re-save the sketch. " + + "You may be in trouble at this point,\n" + + "and it might be time to copy and paste " + + "your code to another text editor."), e); + } + } + + + /** + * Returns true if this is a read-only sketch. Used for the + * examples directory, or when sketches are loaded from read-only + * volumes or folders without appropriate permissions. + */ + public boolean isReadOnly(LibraryList libraries, String examplesPath) { + String apath = sketch.getFolder().getAbsolutePath(); + + Optional libraryThatIncludesSketch = libraries.stream().filter(lib -> apath.startsWith(lib.getInstalledFolder().getAbsolutePath())).findFirst(); + if (libraryThatIncludesSketch.isPresent() && !libraryThatIncludesSketch.get().onGoingDevelopment()) { + return true; + } + + return sketchIsSystemExample(apath, examplesPath) || sketchFilesAreReadOnly(); + } + + private boolean sketchIsSystemExample(String apath, String examplesPath) { + return apath.startsWith(examplesPath); + } + + private boolean sketchFilesAreReadOnly() { + for (SketchFile file : sketch.getFiles()) { + if (file.isModified() && file.fileReadOnly() && file.fileExists()) { + return true; + } + } + return false; + } + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + + private void setUntitled(boolean u) { + editor.untitled = u; + } + + + public boolean isUntitled() { + return editor.untitled; + } + + public Sketch getSketch() { + return sketch; + } + + // ................................................................. + + + /** + * Convert to sanitized name and alert the user + * if changes were made. + */ + private static String checkName(String origName) { + String newName = BaseNoGui.sanitizeName(origName); + + if (!newName.equals(origName)) { + String msg = + tr("The sketch name had to be modified. Sketch names can only consist\n" + + "of ASCII characters and numbers (but cannot start with a number).\n" + + "They should also be less than 64 characters long."); + System.out.println(msg); + } + return newName; + } + + +} diff --git a/app/src/processing/app/helpers/DocumentTextChangeListener.java b/app/src/processing/app/helpers/DocumentTextChangeListener.java new file mode 100644 index 00000000000..290275e3434 --- /dev/null +++ b/app/src/processing/app/helpers/DocumentTextChangeListener.java @@ -0,0 +1,39 @@ +package processing.app.helpers; + +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; + +/** + * Helper class that create a document listener that calls the given + * TextChangeListener on any change to the document text (but not changes to + * document attributes). + * + * The TextChangeListener to be passed is intended to be a lambda function, for + * easy definition of a callback. + */ +public class DocumentTextChangeListener implements DocumentListener { + public interface TextChangeListener { + public void textChanged(); + } + + private TextChangeListener onChange; + + public DocumentTextChangeListener(TextChangeListener onChange) { + this.onChange = onChange; + } + + @Override + public void changedUpdate(DocumentEvent arg0) { + /* Attributes changed, do nothing */ + } + + @Override + public void insertUpdate(DocumentEvent arg0) { + onChange.textChanged(); + } + + @Override + public void removeUpdate(DocumentEvent arg0) { + onChange.textChanged(); + } +} diff --git a/app/src/processing/app/macosx/ThinkDifferent.java b/app/src/processing/app/macosx/ThinkDifferent.java index 7436591cb2c..e946bdc0fd7 100644 --- a/app/src/processing/app/macosx/ThinkDifferent.java +++ b/app/src/processing/app/macosx/ThinkDifferent.java @@ -75,7 +75,7 @@ public void openFiles(final AppEvent.OpenFilesEvent openFilesEvent) { try { Base.INSTANCE.handleOpen(file); List editors = Base.INSTANCE.getEditors(); - if (editors.size() == 2 && editors.get(0).getSketch().isUntitled()) { + if (editors.size() == 2 && editors.get(0).getSketchController().isUntitled()) { Base.INSTANCE.handleClose(editors.get(0)); } } catch (Exception e) { diff --git a/app/src/processing/app/syntax/SketchTextArea.java b/app/src/processing/app/syntax/SketchTextArea.java index ac50a2dc2a6..bc71817fbaf 100644 --- a/app/src/processing/app/syntax/SketchTextArea.java +++ b/app/src/processing/app/syntax/SketchTextArea.java @@ -35,7 +35,6 @@ import org.fife.ui.rsyntaxtextarea.Token; import org.fife.ui.rtextarea.RTextArea; import org.fife.ui.rtextarea.RTextAreaUI; -import org.fife.ui.rtextarea.RUndoManager; import processing.app.Base; import processing.app.BaseNoGui; import processing.app.PreferencesData; @@ -44,9 +43,7 @@ import javax.swing.event.HyperlinkEvent; import javax.swing.event.HyperlinkListener; import javax.swing.text.BadLocationException; -import javax.swing.text.Document; import javax.swing.text.Segment; -import javax.swing.undo.UndoManager; import java.awt.*; import java.awt.event.MouseEvent; import java.io.File; @@ -69,7 +66,8 @@ public class SketchTextArea extends RSyntaxTextArea { private PdeKeywords pdeKeywords; - public SketchTextArea(PdeKeywords pdeKeywords) throws IOException { + public SketchTextArea(RSyntaxDocument document, PdeKeywords pdeKeywords) throws IOException { + super(document); this.pdeKeywords = pdeKeywords; installFeatures(); } @@ -148,25 +146,6 @@ public boolean isSelectionActive() { return this.getSelectedText() != null; } - public void switchDocument(Document document, UndoManager newUndo) { - - // HACK: Dont discard changes on curret UndoManager. - // BUG: https://github.com/bobbylight/RSyntaxTextArea/issues/84 - setUndoManager(null); // bypass reset current undo manager... - - super.setDocument(document); - - setUndoManager((RUndoManager) newUndo); - - // HACK: Complement previous hack (hide code folding on switch) | Drawback: Lose folding state -// if(sketch.getCodeCount() > 1 && textarea.isCodeFoldingEnabled()){ -// textarea.setCodeFoldingEnabled(false); -// textarea.setCodeFoldingEnabled(true); -// } - - - } - @Override protected RTAMouseListener createMouseListener() { return new SketchTextAreaMouseListener(this); diff --git a/app/src/processing/app/tools/Archiver.java b/app/src/processing/app/tools/Archiver.java index 34cf4efa89f..7308a4d6de7 100644 --- a/app/src/processing/app/tools/Archiver.java +++ b/app/src/processing/app/tools/Archiver.java @@ -26,7 +26,7 @@ import org.apache.commons.compress.utils.IOUtils; import processing.app.Base; import processing.app.Editor; -import processing.app.Sketch; +import processing.app.SketchController; import java.awt.*; import java.io.File; @@ -69,7 +69,7 @@ public void init(Editor editor) { public void run() { - Sketch sketch = editor.getSketch(); + SketchController sketch = editor.getSketchController(); // first save the sketch so that things don't archive strangely boolean success = false; @@ -84,7 +84,7 @@ public void run() { return; } - File location = sketch.getFolder(); + File location = sketch.getSketch().getFolder(); String name = location.getName(); File parent = new File(location.getParent()); diff --git a/app/src/processing/app/tools/DiscourseFormat.java b/app/src/processing/app/tools/DiscourseFormat.java index c631df8bd2e..c79f7d11077 100644 --- a/app/src/processing/app/tools/DiscourseFormat.java +++ b/app/src/processing/app/tools/DiscourseFormat.java @@ -25,6 +25,7 @@ import org.fife.ui.rsyntaxtextarea.Token; import processing.app.Editor; +import processing.app.EditorTab; import processing.app.syntax.SketchTextArea; import javax.swing.text.BadLocationException; @@ -65,9 +66,9 @@ public class DiscourseFormat { * from the actual Processing Tab ready to send to the processing discourse * web (copy & paste) */ - public DiscourseFormat(Editor editor, boolean html) { + public DiscourseFormat(Editor editor, EditorTab tab, boolean html) { this.editor = editor; - this.textarea = editor.getTextArea(); + this.textarea = tab.getTextArea(); this.html = html; } diff --git a/app/src/processing/app/tools/FixEncoding.java b/app/src/processing/app/tools/FixEncoding.java index d76d9b1cb2a..fa91f11c294 100644 --- a/app/src/processing/app/tools/FixEncoding.java +++ b/app/src/processing/app/tools/FixEncoding.java @@ -66,13 +66,9 @@ public void run() { } try { for (int i = 0; i < sketch.getCodeCount(); i++) { - SketchCode code = sketch.getCode(i); - code.setProgram(loadWithLocalEncoding(code.getFile())); - code.setModified(true); // yes, because we want them to save this + SketchFile file = sketch.getFile(i); + editor.findTab(file).setText(loadWithLocalEncoding(file.getFile())); } - // Update the currently visible program with its code - editor.setText(sketch.getCurrentCode().getProgram()); - } catch (IOException e) { String msg = tr("An error occurred while trying to fix the file encoding.\nDo not attempt to save this sketch as it may overwrite\nthe old version. Use Open to re-open the sketch and try again.\n") + diff --git a/app/test/processing/app/BlockCommentGeneratesOneUndoActionTest.java b/app/test/processing/app/BlockCommentGeneratesOneUndoActionTest.java index 411cb5de6ea..1a213eb1e57 100644 --- a/app/test/processing/app/BlockCommentGeneratesOneUndoActionTest.java +++ b/app/test/processing/app/BlockCommentGeneratesOneUndoActionTest.java @@ -55,7 +55,7 @@ public void shouldUndoAndRedo() throws Exception { GuiActionRunner.execute(new GuiQuery() { protected Frame executeInEDT() { - window.getEditor().handleCommentUncomment(); + window.getEditor().getCurrentTab().handleCommentUncomment(); return window.getEditor(); } diff --git a/arduino-core/src/cc/arduino/Compiler.java b/arduino-core/src/cc/arduino/Compiler.java index bb815575cfc..e0788cf7b6a 100644 --- a/arduino-core/src/cc/arduino/Compiler.java +++ b/arduino-core/src/cc/arduino/Compiler.java @@ -108,24 +108,25 @@ enum BuilderAction { private static final Pattern ERROR_FORMAT = Pattern.compile("(.+\\.\\w+):(\\d+)(:\\d+)*:\\s*error:\\s*(.*)\\s*", Pattern.MULTILINE | Pattern.DOTALL); - private final String pathToSketch; - private final SketchData sketch; - private final String buildPath; + private final File pathToSketch; + private final Sketch sketch; + private String buildPath; private final boolean verbose; private RunnerException exception; - public Compiler(SketchData data, String buildPath) { - this(data.getMainFilePath(), data, buildPath); + public Compiler(Sketch data) { + this(data.getPrimaryFile().getFile(), data); } - public Compiler(String pathToSketch, SketchData sketch, String buildPath) { + public Compiler(File pathToSketch, Sketch sketch) { this.pathToSketch = pathToSketch; this.sketch = sketch; - this.buildPath = buildPath; this.verbose = PreferencesData.getBoolean("build.verbose"); } public String build(CompilerProgressListener progListener, boolean exportHex) throws RunnerException, PreferencesMapException, IOException { + this.buildPath = sketch.getBuildPath().getAbsolutePath(); + TargetBoard board = BaseNoGui.getTargetBoard(); if (board == null) { throw new RunnerException("Board is not selected"); @@ -155,7 +156,7 @@ public String build(CompilerProgressListener progListener, boolean exportHex) th size(prefs); - return sketch.getPrimaryFile().getName(); + return sketch.getPrimaryFile().getFileName(); } private String VIDPID() { @@ -241,7 +242,7 @@ private void callArduinoBuilder(TargetBoard board, TargetPlatform platform, Targ commandLine.addArgument("-verbose", false); } - commandLine.addArgument("\"" + pathToSketch + "\"", false); + commandLine.addArgument("\"" + pathToSketch.getAbsolutePath() + "\"", false); if (verbose) { System.out.println(commandLine); @@ -565,8 +566,7 @@ public void message(String s) { RunnerException exception = placeException(error, pieces[1], PApplet.parseInt(pieces[2]) - 1); if (exception != null) { - SketchCode code = sketch.getCode(exception.getCodeIndex()); - String fileName = (code.isExtension("ino") || code.isExtension("pde")) ? code.getPrettyName() : code.getFileName(); + String fileName = exception.getCodeFile().getPrettyName(); int lineNum = exception.getCodeLine() + 1; s = fileName + ":" + lineNum + ": error: " + error + msg; } @@ -595,9 +595,9 @@ public void message(String s) { } private RunnerException placeException(String message, String fileName, int line) { - for (SketchCode code : sketch.getCodes()) { - if (new File(fileName).getName().equals(code.getFileName())) { - return new RunnerException(message, sketch.indexOfCode(code), line); + for (SketchFile file : sketch.getFiles()) { + if (new File(fileName).getName().equals(file.getFileName())) { + return new RunnerException(message, file, line); } } return null; diff --git a/arduino-core/src/cc/arduino/UploaderUtils.java b/arduino-core/src/cc/arduino/UploaderUtils.java index b243c30d216..a80eaf5065c 100644 --- a/arduino-core/src/cc/arduino/UploaderUtils.java +++ b/arduino-core/src/cc/arduino/UploaderUtils.java @@ -34,7 +34,7 @@ import cc.arduino.packages.UploaderFactory; import processing.app.BaseNoGui; import processing.app.PreferencesData; -import processing.app.SketchData; +import processing.app.Sketch; import processing.app.debug.TargetPlatform; import java.util.LinkedList; @@ -56,7 +56,7 @@ public Uploader getUploaderByPreferences(boolean noUploadPort) { return new UploaderFactory().newUploader(target.getBoards().get(board), boardPort, noUploadPort); } - public boolean upload(SketchData data, Uploader uploader, String buildPath, String suggestedClassName, boolean usingProgrammer, boolean noUploadPort, List warningsAccumulator) throws Exception { + public boolean upload(Sketch data, Uploader uploader, String suggestedClassName, boolean usingProgrammer, boolean noUploadPort, List warningsAccumulator) throws Exception { if (uploader == null) uploader = getUploaderByPreferences(noUploadPort); @@ -75,7 +75,7 @@ public boolean upload(SketchData data, Uploader uploader, String buildPath, Stri } try { - success = uploader.uploadUsingPreferences(data.getFolder(), buildPath, suggestedClassName, usingProgrammer, warningsAccumulator); + success = uploader.uploadUsingPreferences(data.getFolder(), data.getBuildPath().getAbsolutePath(), suggestedClassName, usingProgrammer, warningsAccumulator); } finally { if (uploader.requiresAuthorization() && !success) { PreferencesData.remove(uploader.getAuthorizationKey()); diff --git a/arduino-core/src/processing/app/BaseNoGui.java b/arduino-core/src/processing/app/BaseNoGui.java index 11ec4dd46a1..cb71cc6a36c 100644 --- a/arduino-core/src/processing/app/BaseNoGui.java +++ b/arduino-core/src/processing/app/BaseNoGui.java @@ -12,7 +12,6 @@ import cc.arduino.packages.DiscoveryManager; import cc.arduino.packages.Uploader; import com.fasterxml.jackson.core.JsonProcessingException; -import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.compress.utils.IOUtils; import org.apache.commons.logging.impl.LogFactoryImpl; import org.apache.commons.logging.impl.NoOpLog; @@ -28,7 +27,6 @@ import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; -import java.nio.file.Files; import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; @@ -121,18 +119,6 @@ static public String getAvrBasePath() { return path; } - static public File getBuildFolder(SketchData data) throws IOException { - File buildFolder; - if (PreferencesData.get("build.path") != null) { - buildFolder = absoluteFile(PreferencesData.get("build.path")); - Files.createDirectories(buildFolder.toPath()); - } else { - buildFolder = FileUtils.createTempFolder("build", DigestUtils.md5Hex(data.getMainFilePath()) + ".tmp"); - DeleteFilesOnShutdown.add(buildFolder); - } - return buildFolder; - } - static public PreferencesMap getBoardPreferences() { TargetBoard board = getTargetBoard(); if (board == null) @@ -466,14 +452,12 @@ static public void init(String[] args) throws Exception { boolean success = false; try { // Editor constructor loads the sketch with handleOpenInternal() that - // creates a new Sketch that, in trun, calls load() inside its constructor + // creates a new Sketch that, in turn, builds a SketchData + // inside its constructor. // This translates here as: // SketchData data = new SketchData(file); // File tempBuildFolder = getBuildFolder(); - // data.load(); - SketchData data = new SketchData(absoluteFile(parser.getFilenames().get(0))); - File tempBuildFolder = getBuildFolder(data); - data.load(); + Sketch data = new Sketch(absoluteFile(parser.getFilenames().get(0))); // Sketch.exportApplet() // - calls Sketch.prepare() that calls Sketch.ensureExistence() @@ -482,7 +466,7 @@ static public void init(String[] args) throws Exception { if (!data.getFolder().exists()) { showError(tr("No sketch"), tr("Can't find the sketch in the specified path"), null); } - String suggestedClassName = new Compiler(data, tempBuildFolder.getAbsolutePath()).build(null, false); + String suggestedClassName = new Compiler(data).build(null, false); if (suggestedClassName == null) { showError(tr("Error while verifying"), tr("An error occurred while verifying the sketch"), null); } @@ -491,7 +475,7 @@ static public void init(String[] args) throws Exception { Uploader uploader = new UploaderUtils().getUploaderByPreferences(parser.isNoUploadPort()); if (uploader.requiresAuthorization() && !PreferencesData.has(uploader.getAuthorizationKey())) showError("...", "...", null); try { - success = new UploaderUtils().upload(data, uploader, tempBuildFolder.getAbsolutePath(), suggestedClassName, parser.isDoUseProgrammer(), parser.isNoUploadPort(), warningsAccumulator); + success = new UploaderUtils().upload(data, uploader, suggestedClassName, parser.isDoUseProgrammer(), parser.isNoUploadPort(), warningsAccumulator); showMessage(tr("Done uploading"), tr("Done uploading")); } finally { if (uploader.requiresAuthorization() && !success) { @@ -518,9 +502,7 @@ static public void init(String[] args) throws Exception { // SketchData data = new SketchData(file); // File tempBuildFolder = getBuildFolder(); // data.load(); - SketchData data = new SketchData(absoluteFile(path)); - File tempBuildFolder = getBuildFolder(data); - data.load(); + Sketch data = new Sketch(absoluteFile(path)); // Sketch.prepare() calls Sketch.ensureExistence() // Sketch.build(verbose) calls Sketch.ensureExistence() and set progressListener and, finally, calls Compiler.build() @@ -528,7 +510,7 @@ static public void init(String[] args) throws Exception { // if (!data.getFolder().exists()) showError(...); // String ... = Compiler.build(data, tempBuildFolder.getAbsolutePath(), tempBuildFolder, null, verbose); if (!data.getFolder().exists()) showError(tr("No sketch"), tr("Can't find the sketch in the specified path"), null); - String suggestedClassName = new Compiler(data, tempBuildFolder.getAbsolutePath()).build(null, false); + String suggestedClassName = new Compiler(data).build(null, false); if (suggestedClassName == null) showError(tr("Error while verifying"), tr("An error occurred while verifying the sketch"), null); showMessage(tr("Done compiling"), tr("Done compiling")); } catch (Exception e) { @@ -993,49 +975,6 @@ static public void initParameters(String args[]) throws Exception { PreferencesData.init(absoluteFile(preferencesFile)); } - /** - * Recursively remove all files within a directory, - * used with removeDir(), or when the contents of a dir - * should be removed, but not the directory itself. - * (i.e. when cleaning temp files from lib/build) - */ - static public void removeDescendants(File dir) { - if (!dir.exists()) return; - - String files[] = dir.list(); - if (files == null) { - return; - } - - for (String file : files) { - if (file.equals(".") || file.equals("..")) continue; - File dead = new File(dir, file); - if (!dead.isDirectory()) { - if (!PreferencesData.getBoolean("compiler.save_build_files")) { - if (!dead.delete()) { - // temporarily disabled - System.err.println(I18n.format(tr("Could not delete {0}"), dead)); - } - } - } else { - removeDir(dead); - //dead.delete(); - } - } - } - - /** - * Remove all files in a directory and the directory itself. - */ - static public void removeDir(File dir) { - if (dir.exists()) { - removeDescendants(dir); - if (!dir.delete()) { - System.err.println(I18n.format(tr("Could not delete {0}"), dir)); - } - } - } - /** * Produce a sanitized name that fits our standards for likely to work. *

diff --git a/arduino-core/src/processing/app/Sketch.java b/arduino-core/src/processing/app/Sketch.java new file mode 100644 index 00000000000..41e6484b1b2 --- /dev/null +++ b/arduino-core/src/processing/app/Sketch.java @@ -0,0 +1,369 @@ +package processing.app; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import cc.arduino.files.DeleteFilesOnShutdown; +import processing.app.helpers.FileUtils; + +import static processing.app.I18n.tr; + +/** + * This represents a single sketch, consisting of one or more files. + */ +public class Sketch { + public static final String DEFAULT_SKETCH_EXTENSION = "ino"; + public static final List OLD_SKETCH_EXTENSIONS = Arrays.asList("pde"); + public static final List SKETCH_EXTENSIONS = Stream.concat(Stream.of(DEFAULT_SKETCH_EXTENSION), OLD_SKETCH_EXTENSIONS.stream()).collect(Collectors.toList()); + public static final List OTHER_ALLOWED_EXTENSIONS = Arrays.asList("c", "cpp", "h", "hh", "hpp", "s"); + public static final List EXTENSIONS = Stream.concat(SKETCH_EXTENSIONS.stream(), OTHER_ALLOWED_EXTENSIONS.stream()).collect(Collectors.toList()); + + /** + * folder that contains this sketch + */ + private File folder; + + private List files = new ArrayList(); + + private File buildPath; + + private static final Comparator CODE_DOCS_COMPARATOR = new Comparator() { + @Override + public int compare(SketchFile x, SketchFile y) { + if (x.isPrimary() && !y.isPrimary()) + return -1; + if (y.isPrimary() && !x.isPrimary()) + return 1; + return x.getFileName().compareTo(y.getFileName()); + } + }; + + /** + * Create a new SketchData object, and looks at the sketch directory + * on disk to get populate the list of files in this sketch. + * + * @param file + * Any file inside the sketch directory. + */ + Sketch(File file) throws IOException { + folder = file.getParentFile(); + files = listSketchFiles(true); + } + + static public File checkSketchFile(File file) { + // check to make sure that this .pde file is + // in a folder of the same name + String fileName = file.getName(); + File parent = file.getParentFile(); + String parentName = parent.getName(); + String pdeName = parentName + ".pde"; + File altPdeFile = new File(parent, pdeName); + String inoName = parentName + ".ino"; + File altInoFile = new File(parent, inoName); + + if (pdeName.equals(fileName) || inoName.equals(fileName)) + return file; + + if (altPdeFile.exists()) + return altPdeFile; + + if (altInoFile.exists()) + return altInoFile; + + return null; + } + + /** + * Reload the list of files. This checks the sketch directory on disk, + * to see if any files were added or removed. This does *not* check + * the contents of the files, just their presence. + * + * @return true when the list of files was changed, false when it was + * not. + */ + public boolean reload() throws IOException { + List reloaded = listSketchFiles(false); + if (!reloaded.equals(files)) { + files = reloaded; + return true; + } + return false; + } + + /** + * Scan this sketch's directory for files that should be loaded as + * part of this sketch. Doesn't modify this SketchData instance, just + * returns a filtered and sorted list of File objects ready to be + * passed to the SketchFile constructor. + * + * @param showWarnings + * When true, any invalid filenames will show a warning. + */ + private List listSketchFiles(boolean showWarnings) throws IOException { + Set result = new TreeSet<>(CODE_DOCS_COMPARATOR); + for (File file : FileUtils.listFiles(folder, false, EXTENSIONS)) { + if (BaseNoGui.isSanitaryName(file.getName())) { + result.add(new SketchFile(this, file)); + } else if (showWarnings) { + System.err.println(I18n.format(tr("File name {0} is invalid: ignored"), file.getName())); + } + } + + if (result.size() == 0) + throw new IOException(tr("No valid code files found")); + + return new ArrayList<>(result); + } + + /** + * Create the data folder if it does not exist already. As a + * convenience, it also returns the data folder, since it's likely + * about to be used. + */ + public File prepareDataFolder() { + File dataFolder = getDataFolder(); + if (!dataFolder.exists()) { + dataFolder.mkdirs(); + } + return dataFolder; + } + + public void save() throws IOException { + for (SketchFile file : getFiles()) { + if (file.isModified()) + file.save(); + } + } + + public int getCodeCount() { + return files.size(); + } + + public SketchFile[] getFiles() { + return files.toArray(new SketchFile[0]); + } + + /** + * Returns a file object for the primary .pde of this sketch. + */ + public SketchFile getPrimaryFile() { + return files.get(0); + } + + /** + * Returns path to the main .pde file for this sketch. + */ + public String getMainFilePath() { + return getPrimaryFile().getFile().getAbsolutePath(); + } + + public SketchFile getFile(int i) { + return files.get(i); + } + + /** + * Gets the build path for this sketch. The first time this is called, + * a build path is generated and created and the same path is returned + * on all subsequent calls. + * + * This takes into account the build.path preference. If it is set, + * that path is always returned, and the directory is *not* deleted on + * shutdown. If the preference is not set, a random pathname in a + * temporary directory is generated, which is automatically deleted on + * shutdown. + */ + public File getBuildPath() throws IOException { + if (buildPath == null) { + if (PreferencesData.get("build.path") != null) { + buildPath = BaseNoGui.absoluteFile(PreferencesData.get("build.path")); + Files.createDirectories(buildPath.toPath()); + } else { + buildPath = FileUtils.createTempFolder("arduino_build_"); + DeleteFilesOnShutdown.add(buildPath); + } + } + + return buildPath; + } + + protected void removeFile(SketchFile which) { + if (!files.remove(which)) + System.err.println("removeCode: internal error.. could not find code"); + } + + public String getName() { + return folder.getName(); + } + + public File getFolder() { + return folder; + } + + public File getDataFolder() { + return new File(folder, "data"); + } + + /** + * Is any of the files in this sketch modified? + */ + public boolean isModified() { + for (SketchFile file : files) { + if (file.isModified()) + return true; + } + return false; + } + + /** + * Finds the file with the given filename and returns its index. + * Returns -1 when the file was not found. + */ + public int findFileIndex(File filename) { + int i = 0; + for (SketchFile file : files) { + if (file.getFile().equals(filename)) + return i; + i++; + } + return -1; + } + + /** + * Check if renaming/saving this sketch to the given folder would + * cause a problem because: 1. The new folder already exists 2. + * Renaming the primary file would cause a conflict with an existing + * file. If so, an IOEXception is thrown. If not, the name of the new + * primary file is returned. + */ + protected File checkNewFoldername(File newFolder) throws IOException { + String newPrimary = FileUtils.addExtension(newFolder.getName(), DEFAULT_SKETCH_EXTENSION); + // Verify the new folder does not exist yet + if (newFolder.exists()) { + String msg = I18n.format(tr("Sorry, the folder \"{0}\" already exists."), newFolder.getAbsoluteFile()); + throw new IOException(msg); + } + + // If the folder is actually renamed (as opposed to moved somewhere + // else), check for conflicts using the new filename, but the + // existing folder name. + if(newFolder.getName() != folder.getName()) + checkNewFilename(new File(folder, newPrimary)); + + return new File(newFolder, newPrimary); + } + + /** + * Check if renaming or adding a file would cause a problem because + * the file already exists in this sketch. If so, an IOEXception is + * thrown. + * + * @param newFile + * The filename of the new file, or the new name for an + * existing file. + */ + protected void checkNewFilename(File newFile) throws IOException { + // Verify that the sketch doesn't have a filem with the new name + // already, other than the current primary (index 0) + if (findFileIndex(newFile) >= 0) { + String msg = I18n.format(tr("The sketch already contains a file named \"{0}\""), newFile.getName()); + throw new IOException(msg); + } + + } + + /** + * Rename this sketch' folder to the given name. Unlike saveAs(), this + * moves the sketch directory, not leaving anything in the old place. + * This operation does not *save* the sketch, so the files on disk are + * moved, but not modified. + * + * @param newFolder + * The new folder name for this sketch. The new primary + * file's name will be derived from this. + * + * @throws IOException + * When a problem occurs. The error message should be + * already translated. + */ + public void renameTo(File newFolder) throws IOException { + // Check intended rename (throws if there is a problem) + File newPrimary = checkNewFoldername(newFolder); + + // Rename the sketch folder + if (!getFolder().renameTo(newFolder)) + throw new IOException(tr("Failed to rename sketch folder")); + + folder = newFolder; + + // Tell each file about its new name + for (SketchFile file : files) + file.renamedTo(new File(newFolder, file.getFileName())); + + // And finally, rename the primary file + getPrimaryFile().renameTo(newPrimary.getName()); + } + + + public SketchFile addFile(String newName) throws IOException { + // Check the name will not cause any conflicts + File newFile = new File(folder, newName); + checkNewFilename(newFile); + + // Add a new sketchFile + SketchFile sketchFile = new SketchFile(this, newFile); + files.add(sketchFile); + Collections.sort(files, CODE_DOCS_COMPARATOR); + + return sketchFile; + } + + /** + * Save this sketch under the new name given. Unlike renameTo(), this + * leaves the existing sketch in place. + * + * @param newFolder + * The new folder name for this sketch. The new primary + * file's name will be derived from this. + * + * @throws IOException + * When a problem occurs. The error message should be + * already translated. + */ + public void saveAs(File newFolder) throws IOException { + // Check intented rename (throws if there is a problem) + File newPrimary = checkNewFoldername(newFolder); + + // Create the folder + if (!newFolder.mkdirs()) { + String msg = I18n.format(tr("Could not create directory \"{0}\""), newFolder.getAbsolutePath()); + throw new IOException(msg); + } + + // Save the files to their new location + for (SketchFile file : files) { + if (file.isPrimary()) + file.saveAs(newPrimary); + else + file.saveAs(new File(newFolder, file.getFileName())); + } + + folder = newFolder; + + // Copy the data folder (this may take a while.. add progress bar?) + if (getDataFolder().exists()) { + File newDataFolder = new File(newFolder, "data"); + FileUtils.copy(getDataFolder(), newDataFolder); + } + } + + /** + * Deletes this entire sketch from disk. + */ + void delete() { + FileUtils.recursiveDelete(folder); + } +} diff --git a/arduino-core/src/processing/app/SketchCode.java b/arduino-core/src/processing/app/SketchCode.java deleted file mode 100644 index 3e185867641..00000000000 --- a/arduino-core/src/processing/app/SketchCode.java +++ /dev/null @@ -1,244 +0,0 @@ -/* - SketchCode - data class for a single file inside a sketch - Part of the Processing project - http://processing.org - - Copyright (c) 2004-08 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 as published by - the Free Software Foundation; either version 2 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, write to the Free Software Foundation, - Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -*/ - -package processing.app; - -import processing.app.helpers.FileUtils; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static processing.app.I18n.tr; - -/** - * Represents a single tab of a sketch. - */ -public class SketchCode { - - /** - * File object for where this code is located - */ - private File file; - - /** - * Text of the program text for this tab - */ - private String program; - - private boolean modified; - - private Object metadata; - - public SketchCode(File file) { - init(file, null); - } - - public SketchCode(File file, Object metadata) { - init(file, metadata); - } - - private void init(File file, Object metadata) { - this.file = file; - this.metadata = metadata; - - try { - load(); - } catch (IOException e) { - System.err.println( - I18n.format(tr("Error while loading code {0}"), file.getName())); - } - } - - - public File getFile() { - return file; - } - - - protected boolean fileExists() { - return file.exists(); - } - - - protected boolean fileReadOnly() { - return !file.canWrite(); - } - - - protected boolean deleteFile(Path tempBuildFolder) throws IOException { - if (!file.delete()) { - return false; - } - - List tempBuildFolders = Stream.of(tempBuildFolder, tempBuildFolder.resolve("sketch")) - .filter(path -> Files.exists(path)).collect(Collectors.toList()); - - for (Path folder : tempBuildFolders) { - if (!deleteCompiledFilesFrom(folder)) { - return false; - } - } - - return true; - } - - private boolean deleteCompiledFilesFrom(Path tempBuildFolder) throws IOException { - List compiledFiles = Files.list(tempBuildFolder) - .filter(pathname -> pathname.getFileName().toString().startsWith(getFileName())) - .collect(Collectors.toList()); - - for (Path compiledFile : compiledFiles) { - try { - Files.delete(compiledFile); - } catch (IOException e) { - return false; - } - } - return true; - } - - protected boolean renameTo(File what) { - boolean success = file.renameTo(what); - if (success) { - file = what; - } - return success; - } - - - public String getFileName() { - return file.getName(); - } - - - public String getPrettyName() { - String prettyName = getFileName(); - int dot = prettyName.lastIndexOf('.'); - return prettyName.substring(0, dot); - } - - public String getFileNameWithExtensionIfNotIno() { - if (getFileName().endsWith(".ino")) { - return getPrettyName(); - } - return getFileName(); - } - - public boolean isExtension(String... extensions) { - return isExtension(Arrays.asList(extensions)); - } - - public boolean isExtension(List extensions) { - return FileUtils.hasExtension(file, extensions); - } - - - public String getProgram() { - return program; - } - - - public void setProgram(String replacement) { - program = replacement; - } - - - public int getLineCount() { - return BaseNoGui.countLines(program); - } - - - public void setModified(boolean modified) { - this.modified = modified; - } - - - public boolean isModified() { - return modified; - } - - - /** - * Load this piece of code from a file. - */ - private void load() throws IOException { - program = BaseNoGui.loadFile(file); - - if (program == null) { - throw new IOException(); - } - - if (program.indexOf('\uFFFD') != -1) { - System.err.println( - I18n.format( - tr("\"{0}\" contains unrecognized characters. " + - "If this code was created with an older version of Arduino, " + - "you may need to use Tools -> Fix Encoding & Reload to update " + - "the sketch to use UTF-8 encoding. If not, you may need to " + - "delete the bad characters to get rid of this warning."), - file.getName() - ) - ); - System.err.println(); - } - - setModified(false); - } - - - /** - * Save this piece of code, regardless of whether the modified - * flag is set or not. - */ - public void save() throws IOException { - // TODO re-enable history - //history.record(s, SketchHistory.SAVE); - - BaseNoGui.saveFile(program, file); - setModified(false); - } - - - /** - * Save this file to another location, used by Sketch.saveAs() - */ - public void saveAs(File newFile) throws IOException { - BaseNoGui.saveFile(program, newFile); - } - - - public Object getMetadata() { - return metadata; - } - - - public void setMetadata(Object metadata) { - this.metadata = metadata; - } -} diff --git a/arduino-core/src/processing/app/SketchData.java b/arduino-core/src/processing/app/SketchData.java deleted file mode 100644 index 21aeecb4822..00000000000 --- a/arduino-core/src/processing/app/SketchData.java +++ /dev/null @@ -1,270 +0,0 @@ -package processing.app; - -import java.io.File; -import java.io.IOException; -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static processing.app.I18n.tr; - -public class SketchData { - - public static final List SKETCH_EXTENSIONS = Arrays.asList("ino", "pde"); - public static final List OTHER_ALLOWED_EXTENSIONS = Arrays.asList("c", "cpp", "h", "hh", "hpp", "s"); - public static final List EXTENSIONS = Stream.concat(SKETCH_EXTENSIONS.stream(), OTHER_ALLOWED_EXTENSIONS.stream()).collect(Collectors.toList()); - - /** - * main pde file for this sketch. - */ - private File primaryFile; - - /** - * folder that contains this sketch - */ - private File folder; - - /** - * data folder location for this sketch (may not exist yet) - */ - private File dataFolder; - - /** - * code folder location for this sketch (may not exist yet) - */ - private File codeFolder; - - /** - * Name of sketch, which is the name of main file (without .pde or .java - * extension) - */ - private String name; - - private List codes = new ArrayList(); - - private static final Comparator CODE_DOCS_COMPARATOR = new Comparator() { - @Override - public int compare(SketchCode x, SketchCode y) { - return x.getFileName().compareTo(y.getFileName()); - } - }; - - SketchData(File file) { - primaryFile = file; - - // get the name of the sketch by chopping .pde or .java - // off of the main file name - String mainFilename = primaryFile.getName(); - int suffixLength = getDefaultExtension().length() + 1; - name = mainFilename.substring(0, mainFilename.length() - suffixLength); - - folder = new File(file.getParent()); - //System.out.println("sketch dir is " + folder); - } - - static public File checkSketchFile(File file) { - // check to make sure that this .pde file is - // in a folder of the same name - String fileName = file.getName(); - File parent = file.getParentFile(); - String parentName = parent.getName(); - String pdeName = parentName + ".pde"; - File altPdeFile = new File(parent, pdeName); - String inoName = parentName + ".ino"; - File altInoFile = new File(parent, inoName); - - if (pdeName.equals(fileName) || inoName.equals(fileName)) - return file; - - if (altPdeFile.exists()) - return altPdeFile; - - if (altInoFile.exists()) - return altInoFile; - - return null; - } - - /** - * Build the list of files. - *

- * Generally this is only done once, rather than - * each time a change is made, because otherwise it gets to be - * a nightmare to keep track of what files went where, because - * not all the data will be saved to disk. - *

- * This also gets called when the main sketch file is renamed, - * because the sketch has to be reloaded from a different folder. - *

- * Another exception is when an external editor is in use, - * in which case the load happens each time "run" is hit. - */ - protected void load() throws IOException { - codeFolder = new File(folder, "code"); - dataFolder = new File(folder, "data"); - - // get list of files in the sketch folder - String list[] = folder.list(); - if (list == null) { - throw new IOException("Unable to list files from " + folder); - } - - // reset these because load() may be called after an - // external editor event. (fix for 0099) -// codeDocs = new SketchCodeDoc[list.length]; - clearCodeDocs(); -// data.setCodeDocs(codeDocs); - - for (String filename : list) { - // Ignoring the dot prefix files is especially important to avoid files - // with the ._ prefix on Mac OS X. (You'll see this with Mac files on - // non-HFS drives, i.e. a thumb drive formatted FAT32.) - if (filename.startsWith(".")) continue; - - // Don't let some wacko name a directory blah.pde or bling.java. - if (new File(folder, filename).isDirectory()) continue; - - // figure out the name without any extension - String base = filename; - // now strip off the .pde and .java extensions - for (String extension : EXTENSIONS) { - if (base.toLowerCase().endsWith("." + extension)) { - base = base.substring(0, base.length() - (extension.length() + 1)); - - // Don't allow people to use files with invalid names, since on load, - // it would be otherwise possible to sneak in nasty filenames. [0116] - if (BaseNoGui.isSanitaryName(base)) { - addCode(new SketchCode(new File(folder, filename))); - } else { - System.err.println(I18n.format(tr("File name {0} is invalid: ignored"), filename)); - } - } - } - } - - if (getCodeCount() == 0) - throw new IOException(tr("No valid code files found")); - - // move the main class to the first tab - // start at 1, if it's at zero, don't bother - for (SketchCode code : getCodes()) { - //if (code[i].file.getName().equals(mainFilename)) { - if (code.getFile().equals(primaryFile)) { - moveCodeToFront(code); - break; - } - } - - // sort the entries at the top - sortCode(); - } - - public void save() throws IOException { - for (SketchCode code : getCodes()) { - if (code.isModified()) - code.save(); - } - } - - public int getCodeCount() { - return codes.size(); - } - - public SketchCode[] getCodes() { - return codes.toArray(new SketchCode[0]); - } - - /** - * Returns the default extension for this editor setup. - */ - public String getDefaultExtension() { - return "ino"; - } - - /** - * Returns a file object for the primary .pde of this sketch. - */ - public File getPrimaryFile() { - return primaryFile; - } - - /** - * Returns path to the main .pde file for this sketch. - */ - public String getMainFilePath() { - return primaryFile.getAbsolutePath(); - //return code[0].file.getAbsolutePath(); - } - - public void addCode(SketchCode sketchCode) { - codes.add(sketchCode); - } - - public void moveCodeToFront(SketchCode codeDoc) { - codes.remove(codeDoc); - codes.add(0, codeDoc); - } - - protected void replaceCode(SketchCode newCode) { - for (SketchCode code : codes) { - if (code.getFileName().equals(newCode.getFileName())) { - codes.set(codes.indexOf(code), newCode); - return; - } - } - } - - protected void sortCode() { - if (codes.size() < 2) - return; - SketchCode first = codes.remove(0); - Collections.sort(codes, CODE_DOCS_COMPARATOR); - codes.add(0, first); - } - - public SketchCode getCode(int i) { - return codes.get(i); - } - - protected void removeCode(SketchCode which) { - for (SketchCode code : codes) { - if (code == which) { - codes.remove(code); - return; - } - } - System.err.println("removeCode: internal error.. could not find code"); - } - - public int indexOfCode(SketchCode who) { - for (SketchCode code : codes) { - if (code == who) - return codes.indexOf(code); - } - return -1; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public void clearCodeDocs() { - codes.clear(); - } - - public File getFolder() { - return folder; - } - - public File getDataFolder() { - return dataFolder; - } - - public File getCodeFolder() { - return codeFolder; - } -} diff --git a/arduino-core/src/processing/app/SketchFile.java b/arduino-core/src/processing/app/SketchFile.java new file mode 100644 index 00000000000..19d30006533 --- /dev/null +++ b/arduino-core/src/processing/app/SketchFile.java @@ -0,0 +1,301 @@ +/* + SketchFile - data class for a single file inside a sketch + Part of the Processing project - http://processing.org + + Copyright (c) 2004-08 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 as published by + the Free Software Foundation; either version 2 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, write to the Free Software Foundation, + Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +*/ + +package processing.app; + +import processing.app.helpers.FileUtils; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static processing.app.I18n.tr; + +/** + * Represents a file within a sketch. + */ +public class SketchFile { + + /** + * File object for where this code is located + */ + private File file; + + /** + * The sketch this file belongs to. + */ + private Sketch sketch; + + /** + * Is this the primary file in the sketch? + */ + private boolean primary; + + /** + * Interface for an in-memory storage of text file contents. This is + * intended to allow a GUI to keep modified text in memory, and allow + * SketchFile to check for changes when needed. + */ + public static interface TextStorage { + /** Get the current text */ + public String getText(); + + /** + * Is the text modified externally, after the last call to + * clearModified() or setText()? + */ + public boolean isModified(); + + /** Clear the isModified() result value */ + public void clearModified(); + }; + + /** + * A storage for this file's text. This can be set by a GUI, so we can + * have access to any modified version of the file. This can be null, + * in which case the file is never modified, and saving is a no-op. + */ + private TextStorage storage; + + /** + * Create a new SketchFile + * + * @param sketch + * The sketch this file belongs to + * @param file + * The file this SketchFile represents + * @param primary + * Whether this file is the primary file of the sketch + */ + public SketchFile(Sketch sketch, File file) { + this.sketch = sketch; + this.file = file; + FileUtils.SplitFile split = FileUtils.splitFilename(file); + this.primary = split.basename.equals(sketch.getFolder().getName()) + && Sketch.SKETCH_EXTENSIONS.contains(split.extension); + } + + /** + * Set an in-memory storage for this file's text, that will be queried + * on compile, save, and whenever the text is needed. null can be + * passed to detach any attached storage. + */ + public void setStorage(TextStorage text) { + this.storage = text; + } + + + public File getFile() { + return file; + } + + /** + * Is this the primary file in the sketch? + */ + public boolean isPrimary() { + return primary; + } + + protected boolean fileExists() { + return file.exists(); + } + + + protected boolean fileReadOnly() { + return !file.canWrite(); + } + + + protected boolean delete(Path tempBuildFolder) throws IOException { + if (!file.delete()) { + return false; + } + + List tempBuildFolders = Stream.of(tempBuildFolder, tempBuildFolder.resolve("sketch")) + .filter(path -> Files.exists(path)).collect(Collectors.toList()); + + for (Path folder : tempBuildFolders) { + if (!deleteCompiledFilesFrom(folder)) { + return false; + } + } + + sketch.removeFile(this); + + return true; + } + + private boolean deleteCompiledFilesFrom(Path tempBuildFolder) throws IOException { + List compiledFiles = Files.list(tempBuildFolder) + .filter(pathname -> pathname.getFileName().toString().startsWith(getFileName())) + .collect(Collectors.toList()); + + for (Path compiledFile : compiledFiles) { + try { + Files.delete(compiledFile); + } catch (IOException e) { + return false; + } + } + return true; + } + + /** + * Rename the given file to get the given name. + * + * @param newName + * The new name, including extension, excluding directory + * name. + * @throws IOException + * When a problem occurs, or is expected to occur. The error + * message should be already translated. + */ + public void renameTo(String newName) throws IOException { + File newFile = new File(file.getParentFile(), newName); + sketch.checkNewFilename(newFile); + if (file.renameTo(newFile)) { + renamedTo(newFile); + } else { + String msg = I18n.format(tr("Failed to rename \"{0}\" to \"{1}\""), file.getName(), newName); + throw new IOException(msg); + } + } + + /** + * Should be called when this file was renamed and renameTo could not + * be used (e.g. when renaming the entire sketch directory). + */ + protected void renamedTo(File what) { + file = what; + } + + /* + * Returns the filename include extension. + */ + public String getFileName() { + return file.getName(); + } + + /** + * Returns the filename without extension for normal sketch files + * (Sketch.SKETCH_EXTENSIONS) and the filename with extension for all + * others. + */ + public String getPrettyName() { + if (isExtension(Sketch.SKETCH_EXTENSIONS)) + return getBaseName(); + else + return getFileName(); + } + + /** + * Returns the filename without extension + */ + public String getBaseName() { + return FileUtils.splitFilename(file).basename; + } + + public boolean isExtension(String... extensions) { + return isExtension(Arrays.asList(extensions)); + } + + public boolean isExtension(List extensions) { + return FileUtils.hasExtension(file, extensions); + } + + + public String getProgram() { + if (storage != null) + return storage.getText(); + + return null; + } + + + public boolean isModified() { + if (storage != null) + return storage.isModified(); + return false; + } + + public boolean equals(Object o) { + return (o instanceof SketchFile) && file.equals(((SketchFile) o).file); + } + + /** + * Load this piece of code from a file and return the contents. This + * completely ignores any changes in the linked storage, if any, and + * just directly reads the file. + */ + public String load() throws IOException { + String text = BaseNoGui.loadFile(file); + + if (text == null) { + throw new IOException(); + } + + if (text.indexOf('\uFFFD') != -1) { + System.err.println( + I18n.format( + tr("\"{0}\" contains unrecognized characters. " + + "If this code was created with an older version of Arduino, " + + "you may need to use Tools -> Fix Encoding & Reload to update " + + "the sketch to use UTF-8 encoding. If not, you may need to " + + "delete the bad characters to get rid of this warning."), + file.getName() + ) + ); + System.err.println(); + } + return text; + } + + + /** + * Save this piece of code, regardless of whether the modified + * flag is set or not. + */ + public void save() throws IOException { + if (storage == null) + return; /* Nothing to do */ + + BaseNoGui.saveFile(storage.getText(), file); + storage.clearModified(); + } + + + /** + * Save this file to another location, used by Sketch.saveAs() + */ + public void saveAs(File newFile) throws IOException { + if (storage == null) + return; /* Nothing to do */ + + BaseNoGui.saveFile(storage.getText(), newFile); + renamedTo(newFile); + } +} diff --git a/arduino-core/src/processing/app/debug/RunnerException.java b/arduino-core/src/processing/app/debug/RunnerException.java index 0a67d1e80ef..5a60ac5a48b 100644 --- a/arduino-core/src/processing/app/debug/RunnerException.java +++ b/arduino-core/src/processing/app/debug/RunnerException.java @@ -23,6 +23,7 @@ package processing.app.debug; +import processing.app.SketchFile; /** * An exception with a line number attached that occurs @@ -31,7 +32,7 @@ @SuppressWarnings("serial") public class RunnerException extends Exception { protected String message; - protected int codeIndex; + protected SketchFile codeFile; protected int codeLine; protected int codeColumn; protected boolean showStackTrace; @@ -42,23 +43,23 @@ public RunnerException(String message) { } public RunnerException(String message, boolean showStackTrace) { - this(message, -1, -1, -1, showStackTrace); + this(message, null, -1, -1, showStackTrace); } - public RunnerException(String message, int file, int line) { + public RunnerException(String message, SketchFile file, int line) { this(message, file, line, -1, true); } - public RunnerException(String message, int file, int line, int column) { + public RunnerException(String message, SketchFile file, int line, int column) { this(message, file, line, column, true); } - public RunnerException(String message, int file, int line, int column, + public RunnerException(String message, SketchFile file, int line, int column, boolean showStackTrace) { this.message = message; - this.codeIndex = file; + this.codeFile = file; this.codeLine = line; this.codeColumn = column; this.showStackTrace = showStackTrace; @@ -84,18 +85,17 @@ public void setMessage(String message) { } - public int getCodeIndex() { - return codeIndex; + public SketchFile getCodeFile() { + return codeFile; } - public void setCodeIndex(int index) { - codeIndex = index; + public void setCodeFile(SketchFile file) { + codeFile = file; } - - - public boolean hasCodeIndex() { - return codeIndex != -1; + + public boolean hasCodeFile() { + return codeFile != null; } @@ -107,8 +107,7 @@ public int getCodeLine() { public void setCodeLine(int line) { this.codeLine = line; } - - + public boolean hasCodeLine() { return codeLine != -1; } @@ -117,8 +116,7 @@ public boolean hasCodeLine() { public void setCodeColumn(int column) { this.codeColumn = column; } - - + public int getCodeColumn() { return codeColumn; } diff --git a/arduino-core/src/processing/app/helpers/FileUtils.java b/arduino-core/src/processing/app/helpers/FileUtils.java index 4083a0a69de..fdfdf313428 100644 --- a/arduino-core/src/processing/app/helpers/FileUtils.java +++ b/arduino-core/src/processing/app/helpers/FileUtils.java @@ -245,6 +245,44 @@ public static boolean hasExtension(File file, List extensions) { return extensions.contains(extension.toLowerCase()); } + /** + * Returns the given filename with the extension replaced by the one + * given. If the filename does not have an extension yet, one is + * added. + */ + public static String replaceExtension(String filename, String extension) { + SplitFile split = splitFilename(filename); + split.extension = extension; + return split.join(); + } + + /** + * Returns the given filename with the extension replaced by the one + * given. If the filename does not have an extension yet, one is + * added. + */ + public static File replaceExtension(File file, String extension) { + return new File(file.getParentFile(), replaceExtension(file.getName(), extension)); + } + + /** + * Adds an extension to the given filename. If it already contains + * one, an additional extension is added. If the extension is the + * empty string, the file is returned unmodified. + */ + public static String addExtension(String filename, String extension) { + return extension.equals("") ? filename : (filename + "." + extension); + } + + /** + * Adds an extension to the given filename. If it already contains + * one, an additional extension is added. If the extension is the + * empty string, the file is returned unmodified. + */ + public static File addExtension(File file, String extension) { + return new File(file.getParentFile(), addExtension(file.getName(), extension)); + } + /** * The result of a splitFilename call. */ @@ -256,6 +294,10 @@ public SplitFile(String basename, String extension) { public String basename; public String extension; + + public String join() { + return addExtension(basename, extension); + } } /**