/* -*- 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 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 processing.app.debug.*; import processing.app.syntax.*; import processing.app.tools.*; import processing.core.*; import java.awt.*; import java.awt.datatransfer.*; import java.awt.event.*; import java.awt.print.*; import java.io.*; import java.lang.reflect.Method; import java.net.*; import java.util.*; import java.util.zip.*; import javax.swing.*; import javax.swing.event.*; import javax.swing.text.*; import javax.swing.undo.*; import java.util.regex.*; import jssc.*; /** * Main editor panel for the Processing Development Environment. */ public class Editor extends JFrame implements RunnerListener { final Base base; // otherwise, if the window is resized with the message label // set to blank, it's preferredSize() will be fukered static protected final String EMPTY = " " + " " + " "; /** * true if this file has not yet been given a name by the user */ boolean untitled; private PageFormat pageFormat; private PrinterJob printerJob; // file and sketch menus for re-inserting items private JMenu fileMenu; private JMenu sketchMenu; private JMenu helpMenu; private JMenu windowMenu; JMenu toolsMenu; int numTools = 0; final EditorToolbar toolbar; // these menus are shared so that they needn't be rebuilt for all windows // each time a sketch is created, renamed, or moved. static JMenu toolbarMenu; static JMenu sketchbookMenu; static JMenu examplesMenu; static JMenu importMenu; static JMenu boardsMenu; static JMenu serialMenu; static JMenu bootloaderMenu; static SerialMenuListener serialMenuListener; static SerialMonitor serialMonitor; Schematics schematics; EditorHeader header; EditorStatus status; EditorConsole console; Serial serialPort; private final JSplitPane splitPane; private final JPanel consolePanel; // currently opened program private Sketch sketch; private EditorLineStatus lineStatus; private JEditTextArea textarea; private EditorListener listener; // runtime information and window placement private Point sketchWindowLocation; // private JMenuItem exportAppItem; private JMenuItem saveMenuItem; private JMenuItem saveAsMenuItem; boolean hasSchematics = false; boolean running; //boolean presenting; boolean uploading; // undo fellers private JMenuItem undoItem, redoItem; protected UndoAction undoAction; protected RedoAction redoAction; private UndoManager undo; // used internally, and only briefly private CompoundEdit compoundEdit; private FindReplace find; private Runnable runHandler; private Runnable presentHandler; private Runnable stopHandler; private Runnable exportHandler; private Runnable exportAppHandler; private Runnable buildHandler; private Runnable buildAppHandler; private final Stack<Integer> caretUndoStack = new Stack<Integer>(); private final Stack<Integer> caretRedoStack = new Stack<Integer>(); public Editor(Base ibase, String path, int[] location) { super("Wiring"); this.base = ibase; Base.setIcon(this); // Install default actions for Run, Present, etc. resetHandlers(); // add listener to handle window close box hit event addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e) { base.handleClose(Editor.this); base.rebuildWindowMenu(windowMenu); } }); // don't close the window when clicked, the app will take care // of that via the handleQuitInternal() methods // http://dev.processing.org/bugs/show_bug.cgi?id=440 setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); // When bringing a window to front, let the Base know addWindowListener(new WindowAdapter() { public void windowActivated(WindowEvent e) { // EditorConsole.systemOut.println("editor window activated"); base.handleActivated(Editor.this); // re-add the sub-menus that are shared by all windows fileMenu.insert(Base.recentMenu, 2); fileMenu.insert(Base.sketchbookMenu, 3); //fileMenu.insert(Base.examplesMenu, 3); sketchMenu.insert(Base.importMenu, 4); toolsMenu.insert(boardsMenu, numTools); toolsMenu.insert(serialMenu, numTools + 1); toolsMenu.insert(bootloaderMenu, numTools + 3); helpMenu.insert(Base.examplesMenu, 1); } // added for 1.0.5 // http://dev.processing.org/bugs/show_bug.cgi?id=1260 public void windowDeactivated(WindowEvent e) { // EditorConsole.systemErr.println("editor window deactivated"); fileMenu.remove(Base.recentMenu); fileMenu.remove(Base.sketchbookMenu); //fileMenu.remove(Base.examplesMenu); sketchMenu.remove(Base.importMenu); toolsMenu.remove(boardsMenu); toolsMenu.remove(serialMenu); toolsMenu.remove(bootloaderMenu); helpMenu.remove(Base.examplesMenu); } }); if (serialMonitor == null) serialMonitor = new SerialMonitor(Preferences.get("serial.port")); buildMenuBar(); // For rev 0120, placing things inside a JPanel Container contentPain = getContentPane(); contentPain.setLayout(new BorderLayout()); JPanel pain = new JPanel(); pain.setLayout(new BorderLayout()); contentPain.add(pain, BorderLayout.CENTER); Box box = Box.createVerticalBox(); Box upper = Box.createVerticalBox(); if (toolbarMenu == null) { toolbarMenu = new JMenu(); base.rebuildToolbarMenu(toolbarMenu); } toolbar = new EditorToolbar(this, toolbarMenu); upper.add(toolbar); header = new EditorHeader(this); upper.add(header); textarea = new JEditTextArea(new PdeTextAreaDefaults()); textarea.setRightClickPopup(new TextAreaPopup()); textarea.setHorizontalOffset(6); // assemble console panel, consisting of status area and the console itself consolePanel = new JPanel(); consolePanel.setLayout(new BorderLayout()); status = new EditorStatus(this); consolePanel.add(status, BorderLayout.NORTH); console = new EditorConsole(this); // windows puts an ugly border on this guy console.setBorder(null); consolePanel.add(console, BorderLayout.CENTER); lineStatus = new EditorLineStatus(textarea); consolePanel.add(lineStatus, BorderLayout.SOUTH); upper.add(textarea); splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, upper, consolePanel); splitPane.setOneTouchExpandable(true); // repaint child panes while resizing splitPane.setContinuousLayout(true); // if window increases in size, give all of increase to // the textarea in the uppper pane splitPane.setResizeWeight(1D); // to fix ugliness.. normally macosx java 1.3 puts an // ugly white border around this object, so turn it off. splitPane.setBorder(null); // the default size on windows is too small and kinda ugly int dividerSize = Preferences.getInteger("editor.divider.size"); if (dividerSize > 10) { //todo define 10 somewhere as constant? splitPane.setDividerSize(dividerSize); } else { splitPane.setDividerSize(10); } splitPane.setMinimumSize(new Dimension(600, 400)); box.add(splitPane); // hopefully these are no longer needed w/ swing // (har har har.. that was wishful thinking) listener = new EditorListener(this, textarea); pain.add(box); // get shift down/up events so we can show the alt version of toolbar buttons textarea.addKeyListener(toolbar); pain.setTransferHandler(new FileDropHandler()); // Finish preparing Editor (formerly found in Base) pack(); // Set the window bounds and the divider location before setting it visible setPlacement(location); // If the window is resized too small this will resize it again to the // minimums. Adapted by Chris Lonnen from comments here: // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4320050 // as a fix for http://dev.processing.org/bugs/show_bug.cgi?id=25 final int minW = Preferences.getInteger("editor.window.width.min"); final int minH = Preferences.getInteger("editor.window.height.min"); addComponentListener(new java.awt.event.ComponentAdapter() { public void componentResized(ComponentEvent event) { setSize((getWidth() < minW) ? minW : getWidth(), (getHeight() < minH) ? minH : getHeight()); } }); // Bring back the general options for the editor applyPreferences(); // Open the document that was passed in boolean loaded = handleOpenInternal(path); if (!loaded) { sketch = null; } else { //the sketch is now loaded, let's determine if we can show schematics String s = sketch.getFolder().getAbsolutePath() + File.separator + sketch.getName() + ".png"; File file = new File(s); if(!file.exists()) { sketchMenu.getItem(2/*index of Schematic*/).setEnabled(false); toolbar.disable(EditorToolbar.SCHEMATICS); } } base.rebuildRecentMenu(); } /** * Handles files dragged & dropped from the desktop and into the editor * window. Dragging files into the editor window is the same as using * "Sketch → Add File" for each file. */ class FileDropHandler extends TransferHandler { public boolean canImport(JComponent dest, DataFlavor[] flavors) { return true; } @SuppressWarnings("unchecked") public boolean importData(JComponent src, Transferable transferable) { int successful = 0; try { DataFlavor uriListFlavor = new DataFlavor("text/uri-list;class=java.lang.String"); if (transferable.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) { java.util.List list = (java.util.List) transferable.getTransferData(DataFlavor.javaFileListFlavor); for (int i = 0; i < list.size(); i++) { File file = (File) list.get(i); if (sketch.addFile(file)) { successful++; } } } else if (transferable.isDataFlavorSupported(uriListFlavor)) { // Some platforms (Mac OS X and Linux, when this began) preferred // this method of moving files. String data = (String)transferable.getTransferData(uriListFlavor); String[] pieces = PApplet.splitTokens(data, "\r\n"); for (int i = 0; i < pieces.length; i++) { if (pieces[i].startsWith("#")) continue; String path = null; if (pieces[i].startsWith("file:///")) { path = pieces[i].substring(7); } else if (pieces[i].startsWith("file:/")) { path = pieces[i].substring(5); } if (sketch.addFile(new File(path))) { successful++; } } } } catch (Exception e) { e.printStackTrace(); return false; } if (successful == 0) { statusError("No files were added to the sketch."); } else if (successful == 1) { statusNotice("One file added to the sketch."); } else { statusNotice(successful + " files added to the sketch."); } return true; } } protected void setPlacement(int[] location) { setBounds(location[0], location[1], location[2], location[3]); if (location[4] != 0) { splitPane.setDividerLocation(location[4]); } } protected int[] getPlacement() { int[] location = new int[5]; // Get the dimensions of the Frame Rectangle bounds = getBounds(); location[0] = bounds.x; location[1] = bounds.y; location[2] = bounds.width; location[3] = bounds.height; // Get the current placement of the divider location[4] = splitPane.getDividerLocation(); return location; } /** * Hack for #@#)$(* Mac OS X 10.2. * <p/> * This appears to only be required on OS X 10.2, and is not * even being called on later versions of OS X or Windows. */ // public Dimension getMinimumSize() { // //System.out.println("getting minimum size"); // return new Dimension(500, 550); // } // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . /** * Read and apply new values from the preferences, either because * the app is just starting up, or the user just finished messing * with things in the Preferences window. */ protected void applyPreferences() { // apply the setting for 'use external editor' boolean external = Preferences.getBoolean("editor.external"); textarea.setEditable(!external); saveMenuItem.setEnabled(!external); saveAsMenuItem.setEnabled(!external); TextAreaPainter painter = textarea.getPainter(); if (external) { // disable line highlight and turn off the caret when disabling Color color = Theme.getColor("editor.external.bgcolor"); painter.setBackground(color); painter.setLineHighlightEnabled(false); textarea.setCaretVisible(false); // new stuff splitPane.setDividerLocation(toolbar.getHeight() + header.getHeight()); splitPane.setResizeWeight(0D); textarea.setMinimumSize(new Dimension(textarea.getWidth(), 0)); } else { Color color = Theme.getColor("editor.bgcolor"); painter.setBackground(color); boolean highlight = Preferences.getBoolean("editor.linehighlight"); painter.setLineHighlightEnabled(highlight); textarea.setCaretVisible(true); // new stuff splitPane.setDividerLocation(-1); // any negative value resets to preferred size splitPane.setResizeWeight(1D); textarea.setMinimumSize(null); } // apply changes to the font size for the editor //TextAreaPainter painter = textarea.getPainter(); painter.setFont(Preferences.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(); } // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . protected void buildMenuBar() { JMenuBar menubar = new JMenuBar(); menubar = new JMenuBar(); menubar.add(fileMenu = base.buildFileMenu(this)); menubar.add(buildEditMenu()); menubar.add(buildSketchMenu()); menubar.add(buildToolsMenu()); menubar.add(windowMenu = base.buildWindowMenu()); windowMenu.addMenuListener(new MenuListener() { public void menuCanceled(MenuEvent e) {} public void menuDeselected(MenuEvent e) {} public void menuSelected(MenuEvent e) { base.rebuildWindowMenu(windowMenu); } }); menubar.add(buildHelpMenu()); setJMenuBar(menubar); } public void setSaveItem(JMenuItem item) { saveMenuItem = item; } public void setSaveAsItem(JMenuItem item) { saveAsMenuItem = item; } protected JMenu buildSketchMenu() { JMenuItem item; sketchMenu = new JMenu("Sketch"); item = Base.newJMenuItem("Verify / Compile", 'R'); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { handleRun(false); } }); sketchMenu.add(item); item = Base.newJMenuItem("Build", 'B'); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { handleBuild(false); } }); sketchMenu.add(item); /* item = newJMenuItemShift("Present", 'R'); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { handleRun(true); } }); sketchMenu.add(item); */ item = new JMenuItem("Schematics"); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { handleSchematics(); } }); sketchMenu.add(item); sketchMenu.addSeparator(); if (Base.importMenu == null) { Base.importMenu = new JMenu("Import Library..."); base.rebuildImportMenu(); } sketchMenu.add(Base.importMenu); item = Base.newJMenuItem("Show Sketch Folder", 'K'); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { Base.openFolder(sketch.getFolder()); } }); sketchMenu.add(item); item.setEnabled(Base.openFolderAvailable()); item = new JMenuItem("Add File..."); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { sketch.handleAddFile(); } }); sketchMenu.add(item); return sketchMenu; } protected JMenu buildToolsMenu() { toolsMenu = new JMenu("Tools"); JMenu menu = toolsMenu; JMenuItem item; addInternalTools(menu); item = Base.newJMenuItemShift("Serial Monitor", 'M'); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { handleSerial(); } }); menu.add(item); addTools(menu, Base.getToolsFolder()); File sketchbookTools = new File(Base.getSketchbookFolder(), "tools"); addTools(menu, sketchbookTools); menu.addSeparator(); numTools = menu.getItemCount(); if (boardsMenu == null) { boardsMenu = new JMenu("Board"); } base.rebuildBoardsMenu(boardsMenu); //} menu.add(boardsMenu); if (serialMenuListener == null) serialMenuListener = new SerialMenuListener(); if (serialMenu == null) serialMenu = new JMenu("Serial Port"); populateSerialMenu(); menu.add(serialMenu); menu.addSeparator(); if (bootloaderMenu == null) { bootloaderMenu = new JMenu("Burn Bootloader"); } base.rebuildBurnBootloaderMenu(bootloaderMenu); menu.add(bootloaderMenu); menu.addMenuListener(new MenuListener() { public void menuCanceled(MenuEvent e) {} public void menuDeselected(MenuEvent e) {} public void menuSelected(MenuEvent e) { //System.out.println("Tools menu selected."); populateSerialMenu(); base.rebuildBoardsMenu(boardsMenu); base.rebuildBurnBootloaderMenu(bootloaderMenu); } }); return menu; } protected void addTools(JMenu menu, File sourceFolder) { HashMap<String, JMenuItem> toolItems = new HashMap<String, JMenuItem>(); File[] folders = sourceFolder.listFiles(new FileFilter() { public boolean accept(File folder) { if (folder.isDirectory()) { //System.out.println("checking " + folder); File subfolder = new File(folder, "tool"); return subfolder.exists(); } return false; } }); if (folders == null || folders.length == 0) { return; } for (int i = 0; i < folders.length; i++) { File toolDirectory = new File(folders[i], "tool"); try { // add dir to classpath for .classes //urlList.add(toolDirectory.toURL()); // add .jar files to classpath File[] archives = toolDirectory.listFiles(new FilenameFilter() { public boolean accept(File dir, String name) { return (name.toLowerCase().endsWith(".jar") || name.toLowerCase().endsWith(".zip")); } }); URL[] urlList = new URL[archives.length]; for (int j = 0; j < urlList.length; j++) { urlList[j] = archives[j].toURI().toURL(); } URLClassLoader loader = new URLClassLoader(urlList); String className = null; for (int j = 0; j < archives.length; j++) { className = findClassInZipFile(folders[i].getName(), archives[j]); if (className != null) break; } /* // Alternatively, could use manifest files with special attributes: // http://java.sun.com/j2se/1.3/docs/guide/jar/jar.html // Example code for loading from a manifest file: // http://forums.sun.com/thread.jspa?messageID=3791501 File infoFile = new File(toolDirectory, "tool.txt"); if (!infoFile.exists()) continue; String[] info = PApplet.loadStrings(infoFile); //Main-Class: org.poo.shoe.AwesomerTool //String className = folders[i].getName(); String className = null; for (int k = 0; k < info.length; k++) { if (info[k].startsWith(";")) continue; String[] pieces = PApplet.splitTokens(info[k], ": "); if (pieces.length == 2) { if (pieces[0].equals("Main-Class")) { className = pieces[1]; } } } */ // If no class name found, just move on. if (className == null) continue; Class<?> toolClass = Class.forName(className, true, loader); final Tool tool = (Tool) toolClass.newInstance(); tool.init(Editor.this); String title = tool.getMenuTitle(); JMenuItem item = new JMenuItem(title); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { SwingUtilities.invokeLater(tool); } }); //menu.add(item); toolItems.put(title, item); } catch (Exception e) { e.printStackTrace(); } } ArrayList<String> toolList = new ArrayList<String>(toolItems.keySet()); if (toolList.size() == 0) return; menu.addSeparator(); Collections.sort(toolList); for (String title : toolList) { menu.add((JMenuItem) toolItems.get(title)); } } protected String findClassInZipFile(String base, File file) { // Class file to search for String classFileName = "/" + base + ".class"; try { ZipFile zipFile = new ZipFile(file); Enumeration<?> entries = zipFile.entries(); while (entries.hasMoreElements()) { ZipEntry entry = (ZipEntry) entries.nextElement(); if (!entry.isDirectory()) { String name = entry.getName(); //System.out.println("entry: " + name); if (name.endsWith(classFileName)) { //int slash = name.lastIndexOf('/'); //String packageName = (slash == -1) ? "" : name.substring(0, slash); // Remove .class and convert slashes to periods. return name.substring(0, name.length() - 6).replace('/', '.'); } } } } catch (IOException e) { //System.err.println("Ignoring " + filename + " (" + e.getMessage() + ")"); e.printStackTrace(); } return null; } protected JMenuItem createToolMenuItem(String className) { try { Class<?> toolClass = Class.forName(className); final Tool tool = (Tool) toolClass.newInstance(); JMenuItem item = new JMenuItem(tool.getMenuTitle()); tool.init(Editor.this); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { SwingUtilities.invokeLater(tool); } }); return item; } catch (Exception e) { e.printStackTrace(); return null; } } protected JMenu addInternalTools(JMenu menu) { JMenuItem item; item = createToolMenuItem("processing.app.tools.AutoFormatTool"); int modifiers = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(); item.setAccelerator(KeyStroke.getKeyStroke('T', modifiers)); menu.add(item); // menu.add(createToolMenuItem("processing.app.tools.CreateFont")); // menu.add(createToolMenuItem("processing.app.tools.ColorSelector")); menu.add(createToolMenuItem("processing.app.tools.Archiver")); menu.add(createToolMenuItem("processing.app.tools.FixEncoding")); //menu.add(buildHardwareMenu()); //menu.add(buildFirmwareMenu()); //menu.add(buildSerialMenu()); /* menu.addMenuListener( new MenuListener() { public void menuCanceled(MenuEvent e) {} public void menuDeselected(MenuEvent e) {} public void menuSelected(MenuEvent e) { // build an updated serial menu buildSerialMenu(); } } ); */ return menu; } class SerialMenuListener implements ActionListener { //public SerialMenuListener() { } public void actionPerformed(ActionEvent e) { if(serialMenu == null) { System.out.println("serialMenu is null"); return; } int count = serialMenu.getItemCount(); for (int i = 0; i < count; i++) { ((JCheckBoxMenuItem)serialMenu.getItem(i)).setState(false); } JCheckBoxMenuItem item = (JCheckBoxMenuItem)e.getSource(); item.setState(true); String name = item.getText(); //System.out.println(item.getLabel()); Preferences.set("serial.port", name); serialMonitor.closeSerialPort(); serialMonitor.setVisible(false); toolbar.deactivate(EditorToolbar.SERIAL); serialMonitor = new SerialMonitor(Preferences.get("serial.port")); base.onBoardOrPortChange(); //System.out.println("set to " + get("serial.port")); } /* public void actionPerformed(ActionEvent e) { System.out.println(e.getSource()); String name = e.getActionCommand(); PdeBase.properties.put("serial.port", name); System.out.println("set to " + get("serial.port")); //editor.skOpen(path + File.separator + name, name); // need to push "serial.port" into PdeBase.properties } */ } protected void populateSerialMenu() { // getting list of ports JMenuItem rbMenuItem; //System.out.println("Clearing serial port menu."); serialMenu.removeAll(); boolean empty = true; try { Vector<String> ports = new Vector<String>(Arrays.asList(SerialPortList.getPortNames())); Enumeration<?> portList = ports.elements(); while (portList.hasMoreElements()) { String portId = (String) portList.nextElement(); rbMenuItem = new JCheckBoxMenuItem(portId, portId.equals(Preferences.get("serial.port"))); rbMenuItem.addActionListener(serialMenuListener); //serialGroup.add(rbMenuItem); serialMenu.add(rbMenuItem); empty = false; } /*for (Enumeration enumeration = CommPortIdentifier.getPortIdentifiers(); enumeration.hasMoreElements();) { CommPortIdentifier commportidentifier = (CommPortIdentifier)enumeration.nextElement(); //System.out.println("Found communication port: " + commportidentifier); if (commportidentifier.getPortType() == CommPortIdentifier.PORT_SERIAL) { //System.out.println("Adding port to serial port menu: " + commportidentifier); String curr_port = commportidentifier.getName(); rbMenuItem = new JCheckBoxMenuItem(curr_port, curr_port.equals(Preferences.get("serial.port"))); rbMenuItem.addActionListener(serialMenuListener); //serialGroup.add(rbMenuItem); serialMenu.add(rbMenuItem); empty = false; } }*/ if (!empty) { //System.out.println("enabling the serialMenu"); serialMenu.setEnabled(true); } } catch (Exception exception) { System.out.println("error retrieving port list"); exception.printStackTrace(); } if (serialMenu.getItemCount() == 0) { serialMenu.setEnabled(false); } //serialMenu.addSeparator(); //serialMenu.add(item); } protected JMenu buildSerialMenu() { // get list of names for serial ports // have the default port checked (if present) SerialMenuListener listener = new SerialMenuListener(); String defaultName = Preferences.get("serial.port"); boolean problem = false; if(null == serialMenu){ serialMenu = new JMenu("Serial Port"); }else{ serialMenu.removeAll(); } // if this is failing, it may be because // lib/javax.comm.properties is missing. // java is weird about how it searches for java.comm.properties // so it tends to be very fragile. i.e. quotes in the CLASSPATH // environment variable will hose things. try { //System.out.println("building port list"); Vector<String> ports = new Vector<String>(Arrays.asList(SerialPortList.getPortNames())); Enumeration<?> portList = ports.elements(); while( portList.hasMoreElements()) { String portId = (String) portList.nextElement(); //System.out.println(portId); //if (portId.getName().equals(port)) { String name = portId; JCheckBoxMenuItem mi = new JCheckBoxMenuItem(name, name.equals(defaultName)); mi.addActionListener(listener); //mi.addItemListener(listener); serialMenu.add(mi); } } catch (UnsatisfiedLinkError e) { e.printStackTrace(); problem = true; } catch (Exception e) { System.out.println("Exception building serial menu"); e.printStackTrace(); } if (serialMenu.getItemCount() == 0) { //System.out.println("dimming serial menu"); serialMenu.setEnabled(false); } return serialMenu; } protected JMenu buildHelpMenu() { // To deal with a Mac OS X 10.5 bug, add an extra space after the name // so that the OS doesn't try to insert its slow help menu. // helpMenu = new JMenu("Help "); JMenuItem item; item = new JMenuItem("Getting started"); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { Base.showGettingStarted(); } }); helpMenu.add(item); if (Base.examplesMenu == null) { Base.examplesMenu = new JMenu("Examples"); base.rebuildExamplesMenu(); } helpMenu.add(Base.examplesMenu); item = new JMenuItem("Environment"); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { Base.showEnvironment(); } }); helpMenu.add(item); item = new JMenuItem("Reference"); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { Base.showReference(); } }); helpMenu.add(item); item = Base.newJMenuItemShift("Find in Reference", 'F'); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { if (textarea.isSelectionActive()) { handleFindReference(); } } }); helpMenu.add(item); item = new JMenuItem("Wiring Hardware"); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { Base.showHardware(); } }); helpMenu.add(item); item = new JMenuItem("Frequently Asked Questions"); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { Base.showFAQ(); } }); helpMenu.add(item); item = new JMenuItem("Troubleshooting"); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { Base.showTroubleshooting(); } }); helpMenu.add(item); item = Base.newJMenuItem("Visit wiring.org.co", '5'); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { Base.openURL("http://wiring.org.co/"); } }); helpMenu.add(item); // macosx already has its own about menu if (!Base.isMacOS()) { helpMenu.addSeparator(); item = new JMenuItem("About Wiring"); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { base.handleAbout(); } }); helpMenu.add(item); } return helpMenu; } protected JMenu buildEditMenu() { JMenu menu = new JMenu("Edit"); JMenuItem item; undoItem = Base.newJMenuItem("Undo", 'Z'); undoItem.addActionListener(undoAction = new UndoAction()); menu.add(undoItem); // Gotta follow them interface guidelines // http://code.google.com/p/processing/issues/detail?id=363 if (Base.isWindows()) { redoItem = Base.newJMenuItem("Redo", 'Y'); } else { // Linux and OS X redoItem = Base.newJMenuItemShift("Redo", 'Z'); } redoItem.addActionListener(redoAction = new RedoAction()); menu.add(redoItem); menu.addSeparator(); // TODO "cut" and "copy" should really only be enabled // if some text is currently selected item = Base.newJMenuItem("Cut", 'X'); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { handleCut(); } }); menu.add(item); item = Base.newJMenuItem("Copy", 'C'); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { textarea.copy(); } }); menu.add(item); item = Base.newJMenuItemShift("Copy as HTML", 'C'); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { handleCopyAsHTML(); } }); menu.add(item); item = Base.newJMenuItem("Paste", 'V'); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { textarea.paste(); sketch.setModified(true); } }); menu.add(item); item = Base.newJMenuItem("Select All", 'A'); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { textarea.selectAll(); } }); menu.add(item); menu.addSeparator(); item = Base.newJMenuItem("Comment/Uncomment", '/'); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { handleCommentUncomment(); } }); menu.add(item); item = Base.newJMenuItem("Increase Indent", ']'); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { handleIndentOutdent(true); } }); menu.add(item); item = Base.newJMenuItem("Decrease Indent", '['); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { handleIndentOutdent(false); } }); menu.add(item); menu.addSeparator(); item = Base.newJMenuItem("Find...", 'F'); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { if (find == null) { find = new FindReplace(Editor.this); } //new FindReplace(Editor.this).show(); find.setVisible(true); //find.setVisible(true); } }); menu.add(item); // TODO find next should only be enabled after a // search has actually taken place item = Base.newJMenuItem("Find Next", 'G'); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { if (find != null) { find.find(true); } } }); menu.add(item); return menu; } // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . class UndoAction extends AbstractAction { public UndoAction() { super("Undo"); this.setEnabled(false); } public void actionPerformed(ActionEvent e) { try { final Integer caret = caretUndoStack.pop(); caretRedoStack.push(caret); textarea.setCaretPosition(caret); textarea.scrollToCaret(); } catch (Exception ignore) { } try { undo.undo(); } catch (CannotUndoException ex) { } updateUndoState(); redoAction.updateRedoState(); if (sketch != null) { sketch.setModified(!getText().equals(sketch.getCurrentCode().getSavedProgram())); } } protected void updateUndoState() { if (undo.canUndo()) { this.setEnabled(true); undoItem.setEnabled(true); undoItem.setText(undo.getUndoPresentationName()); putValue(Action.NAME, undo.getUndoPresentationName()); } else { this.setEnabled(false); undoItem.setEnabled(false); undoItem.setText("Undo"); putValue(Action.NAME, "Undo"); } } } class RedoAction extends AbstractAction { public RedoAction() { super("Redo"); this.setEnabled(false); } public void actionPerformed(ActionEvent e) { try { undo.redo(); } catch (CannotRedoException ex) { } try { final Integer caret = caretRedoStack.pop(); caretUndoStack.push(caret); textarea.setCaretPosition(caret); } catch (Exception ignore) { } updateRedoState(); undoAction.updateUndoState(); if (sketch != null) { sketch.setModified(!getText().equals(sketch.getCurrentCode().getSavedProgram())); } } protected void updateRedoState() { if (undo.canRedo()) { redoItem.setEnabled(true); redoItem.setText(undo.getRedoPresentationName()); putValue(Action.NAME, undo.getRedoPresentationName()); } else { this.setEnabled(false); redoItem.setEnabled(false); redoItem.setText("Redo"); putValue(Action.NAME, "Redo"); } } } // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . // these will be done in a more generic way soon, more like: // setHandler("action name", Runnable); // but for the time being, working out the kinks of how many things to // abstract from the editor in this fashion. public void setHandlers(Runnable runHandler, Runnable presentHandler, Runnable stopHandler, Runnable exportHandler, Runnable exportAppHandler, Runnable buildHandler, Runnable buildAppHandler) { this.runHandler = runHandler; this.presentHandler = presentHandler; this.stopHandler = stopHandler; this.exportHandler = exportHandler; this.exportAppHandler = exportAppHandler; this.buildHandler = buildHandler; this.buildAppHandler = buildAppHandler; } public void resetHandlers() { runHandler = new DefaultRunHandler(); presentHandler = new DefaultPresentHandler(); stopHandler = new DefaultStopHandler(); exportHandler = new DefaultExportHandler(); exportAppHandler = new DefaultExportAppHandler(); buildHandler = new DefaultBuildHandler(); buildAppHandler = new DefaultBuildAppHandler(); } // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . /** * Gets the current sketch object. */ public Sketch getSketch() { return sketch; } /** * Get the JEditTextArea object for use (not recommended). This should only * be used in obscure cases that really need to hack the internals of the * JEditTextArea. Most tools should only interface via the get/set functions * found in this class. This will maintain compatibility with future releases, * which will not use JEditTextArea. */ public JEditTextArea getTextArea() { return textarea; } /** * Get the contents of the current buffer. Used by the Sketch class. */ public String getText() { return textarea.getText(); } /** * Get a range of text from the current buffer. */ public String getText(int start, int stop) { return textarea.getText(start, stop - start); } /** * Replace the entire contents of the front-most tab. */ public void setText(String what) { startCompoundEdit(); textarea.setText(what); stopCompoundEdit(); } public void insertText(String what) { startCompoundEdit(); int caret = getCaretOffset(); setSelection(caret, caret); textarea.setSelectedText(what); stopCompoundEdit(); } public String getSelectedText() { return textarea.getSelectedText(); } public void setSelectedText(String what) { textarea.setSelectedText(what); } public void setSelection(int start, int stop) { // make sure that a tool isn't asking for a bad location start = PApplet.constrain(start, 0, textarea.getDocumentLength()); stop = PApplet.constrain(stop, 0, textarea.getDocumentLength()); textarea.select(start, stop); } /** * Get the position (character offset) of the caret. With text selected, * this will be the last character actually selected, no matter the direction * of the selection. That is, if the user clicks and drags to select lines * 7 up to 4, then the caret position will be somewhere on line four. */ public int getCaretOffset() { return textarea.getCaretPosition(); } /** * True if some text is currently selected. */ public boolean isSelectionActive() { return textarea.isSelectionActive(); } /** * Get the beginning point of the current selection. */ public int getSelectionStart() { return textarea.getSelectionStart(); } /** * Get the end point of the current selection. */ public int getSelectionStop() { return textarea.getSelectionStop(); } /** * Get text for a specified line. */ public String getLineText(int line) { return textarea.getLineText(line); } /** * Replace the text on a specified line. */ public void setLineText(int line, String what) { startCompoundEdit(); textarea.select(getLineStartOffset(line), getLineStopOffset(line)); textarea.setSelectedText(what); stopCompoundEdit(); } /** * Get character offset for the start of a given line of text. */ public int getLineStartOffset(int line) { return textarea.getLineStartOffset(line); } /** * Get character offset for end of a given line of text. */ public int getLineStopOffset(int line) { return textarea.getLineStopOffset(line); } /** * Get the number of lines in the currently displayed buffer. */ public int getLineCount() { return textarea.getLineCount(); } /** * Use before a manipulating text to group editing operations together as a * single undo. Use stopCompoundEdit() once finished. */ public void startCompoundEdit() { compoundEdit = new CompoundEdit(); } /** * Use with startCompoundEdit() to group edit operations in a single undo. */ public void stopCompoundEdit() { compoundEdit.end(); undo.addEdit(compoundEdit); undoAction.updateUndoState(); redoAction.updateRedoState(); compoundEdit = null; } public int getScrollPosition() { return textarea.getScrollPosition(); } // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . /** * Switch between tabs, this swaps out the Document object * that's currently being manipulated. */ protected void setCode(SketchCode code) { SyntaxDocument document = (SyntaxDocument) code.getDocument(); if (document == null) { // this document not yet inited document = new SyntaxDocument(); code.setDocument(document); // turn on syntax highlighting document.setTokenMarker(new PdeKeywords()); // insert the program text into the document object try { document.insertString(0, code.getProgram(), null); } catch (BadLocationException bl) { bl.printStackTrace(); } // connect the undo listener to the editor document.addUndoableEditListener(new UndoableEditListener() { public void undoableEditHappened(UndoableEditEvent e) { if (compoundEdit != null) { compoundEdit.addEdit(e.getEdit()); } else if (undo != null) { caretUndoStack.push(textarea.getCaretPosition()); caretRedoStack.clear(); undo.addEdit(e.getEdit()); undoAction.updateUndoState(); redoAction.updateRedoState(); } } }); } // update the document object that's in use textarea.setDocument(document, code.getSelectionStart(), code.getSelectionStop(), code.getScrollPosition()); textarea.requestFocusInWindow(); // required for caret blinking this.undo = code.getUndo(); undoAction.updateUndoState(); redoAction.updateRedoState(); } // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . /** * Implements Edit → Cut. */ public void handleCut() { textarea.cut(); sketch.setModified(true); } /** * Implements Edit → Copy. */ public void handleCopy() { textarea.copy(); } public void handleCopyAsHTML() { textarea.copyAsHTML(); statusNotice("Code formatted as HTML has been copied to the clipboard."); } /** * Implements Edit → Paste. */ public void handlePaste() { textarea.paste(); sketch.setModified(true); } /** * Implements Edit → Select All. */ public void handleSelectAll() { textarea.selectAll(); } protected void handleCommentUncomment() { startCompoundEdit(); int startLine = textarea.getSelectionStartLine(); int stopLine = textarea.getSelectionStopLine(); int lastLineStart = textarea.getLineStartOffset(stopLine); int selectionStop = textarea.getSelectionStop(); // If the selection ends at the beginning of the last line, // then don't (un)comment that line. if (selectionStop == lastLineStart) { // Though if there's no selection, don't do that if (textarea.isSelectionActive()) { stopLine--; } } // If the text is empty, ignore the user. // Also ensure that all lines are commented (not just the first) // when determining whether to comment or uncomment. int length = textarea.getDocumentLength(); boolean commented = true; for (int i = startLine; commented && (i <= stopLine); i++) { int pos = textarea.getLineStartOffset(i); if (pos + 2 > length) { commented = false; } else { // Check the first two characters to see if it's already a comment. String begin = textarea.getText(pos, 2); commented = begin.equals("//"); } } for (int line = startLine; line <= stopLine; line++) { int location = textarea.getLineStartOffset(line); if (commented) { // remove a comment textarea.select(location, location+2); if (textarea.getSelectedText().equals("//")) { textarea.setSelectedText(""); } } else { // add a comment textarea.select(location, location); textarea.setSelectedText("//"); } } // Subtract one from the end, otherwise selects past the current line. // (Which causes subsequent calls to keep expanding the selection) textarea.select(textarea.getLineStartOffset(startLine), textarea.getLineStopOffset(stopLine) - 1); stopCompoundEdit(); } protected void handleIndentOutdent(boolean indent) { int tabSize = Preferences.getInteger("editor.tabs.size"); String tabString = Editor.EMPTY.substring(0, tabSize); startCompoundEdit(); int startLine = textarea.getSelectionStartLine(); int stopLine = textarea.getSelectionStopLine(); // If the selection ends at the beginning of the last line, // then don't (un)comment that line. int lastLineStart = textarea.getLineStartOffset(stopLine); int selectionStop = textarea.getSelectionStop(); if (selectionStop == lastLineStart) { // Though if there's no selection, don't do that if (textarea.isSelectionActive()) { stopLine--; } } for (int line = startLine; line <= stopLine; line++) { int location = textarea.getLineStartOffset(line); if (indent) { textarea.select(location, location); textarea.setSelectedText(tabString); } else { // outdent int length = textarea.getDocumentLength(); if (location+tabSize <= length) textarea.select(location, location + tabSize); else textarea.select(location, length); // Don't eat code if it's not indented if (textarea.getSelectedText().equals(tabString)) { textarea.setSelectedText(""); } } } // Subtract one from the end, otherwise selects past the current line. // (Which causes subsequent calls to keep expanding the selection) textarea.select(textarea.getLineStartOffset(startLine), textarea.getLineStopOffset(stopLine) - 1); stopCompoundEdit(); } protected void handleFindReference() { String text = textarea.getSelectedText().trim(); if (text.length() == 0) { statusNotice("First select a word to find in the reference."); } else { String referenceFile = PdeKeywords.getReference(text); if (referenceFile == null) { statusNotice("No reference available for \"" + text + "\""); } else { Base.showReference(referenceFile + ".html"); } } } // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . /** * Implements Sketch → Run. * @param present Set true to run in full screen (present mode). */ public void handleRun(final boolean verbose) { splitPane.setDividerLocation(splitPane.getLastDividerLocation()); internalCloseRunner(); running = true; toolbar.activate(EditorToolbar.RUN); statusEmpty(); status.progress("Compiling..."); // do this to advance/clear the terminal window / dos prompt / etc for (int i = 0; i < 10; i++) System.out.println(); // clear the console on each run, unless the user doesn't want to if (Preferences.getBoolean("console.auto_clear")) { console.clear(); } new Thread(verbose ? presentHandler : runHandler).start(); } class DefaultRunHandler implements Runnable { public void run() { try { String appletClassName = sketch.compile(false); statusNotice("Done compiling."); if (appletClassName != null) { statusNotice("Done compiling. No syntax errors found"); handleStop(); } } catch (Exception e) { status.unprogress(); statusNotice("Error compiling..."); statusError(e); handleStop(); } status.unprogress(); toolbar.deactivate(EditorToolbar.RUN); } } class DefaultPresentHandler implements Runnable { public void run() { try { //sketch.compile(true); String appletClassName = sketch.compile(true); statusNotice("Done compiling."); if (appletClassName != null) { statusNotice("Done compiling. No syntax errors found"); handleStop(); } } catch (Exception e) { status.unprogress(); statusNotice("Error compiling..."); statusError(e); handleStop(); } status.unprogress(); toolbar.deactivate(EditorToolbar.RUN); } } /** * Called by Sketch → Build. * Handles calling the build() function on sketch, and * queues all the gui status stuff that comes along with it. * <p/> * Made synchronized to (hopefully) avoid problems of people * hitting export twice, quickly, and horking things up. */ synchronized public void handleBuild(final boolean verbose) { toolbar.activate(EditorToolbar.RUN); console.clear(); status.progress("Building sketch..."); new Thread(verbose ? buildAppHandler : buildHandler).start(); } class DefaultBuildHandler implements Runnable { public void run() { try { serialMonitor.closeSerialPort(); serialMonitor.setVisible(false); toolbar.deactivate(EditorToolbar.SERIAL); uploading = true; boolean success = sketch.buildApplet(false); if (success) { statusNotice("Done Building."); uploading = false; } else { // error message will already be visible } } catch (RunnerException e) { status.unprogress(); statusError(e); } catch (Exception e) { e.printStackTrace(); } status.unprogress(); uploading = false; toolbar.deactivate(EditorToolbar.RUN); } } class DefaultBuildAppHandler implements Runnable { public void run() { try { serialMonitor.closeSerialPort(); serialMonitor.setVisible(false); toolbar.deactivate(EditorToolbar.SERIAL); uploading = true; boolean success = sketch.buildApplet(true); if (success) { statusNotice("Done Building."); uploading = false; } else { // error message will already be visible } } catch (RunnerException e) { status.unprogress(); statusError(e); } catch (Exception e) { e.printStackTrace(); } status.unprogress(); uploading = false; toolbar.deactivate(EditorToolbar.EXPORT); } } class DefaultStopHandler implements Runnable { public void run() { try { } catch (Exception e) { statusError(e); } } } /** * Set the location of the sketch run window. Used by Runner to update the * Editor about window drag events while the sketch is running. */ public void setSketchLocation(Point p) { sketchWindowLocation = p; } /** * Get the last location of the sketch's run window. Used by Runner to make * the window show up in the same location as when it was last closed. */ public Point getSketchLocation() { return sketchWindowLocation; } /** * Implements Sketch → Stop, or pressing Stop on the toolbar. */ public void handleStop() { // called by menu or buttons internalCloseRunner(); toolbar.deactivate(EditorToolbar.RUN); toolbar.deactivate(EditorToolbar.SERIAL); // focus the PDE again after quitting presentation mode [toxi 030903] toFront(); } /** * Deactivate the Run button. This is called by Runner to notify that the * sketch has stopped running, usually in response to an error (or maybe * the sketch completing and exiting?) Tools should not call this function. * To initiate a "stop" action, call handleStop() instead. */ public void deactivateRun() { toolbar.deactivate(EditorToolbar.RUN); } public void handleSchematics() { String s = sketch.getFolder().getAbsolutePath() + File.separator + sketch.getName() + ".png"; File file = new File(s); if(file.exists()) { hasSchematics = true; } if(hasSchematics) { toolbar.activate(EditorToolbar.SCHEMATICS); if (schematics == null) schematics = new Schematics(s); schematics.showFrame(this); } else { sketchMenu.getItem(2/*index of Schematic*/).setEnabled(false); statusNotice("This sketch doesn't include schematics"); toolbar.disable(EditorToolbar.SCHEMATICS); } } /** * Called by Runner to notify that the sketch has stopped running. * Tools should not call this function, use handleStop() instead. */ public void internalRunnerClosed() { running = false; toolbar.deactivate(EditorToolbar.RUN); } /** * Handle internal shutdown of the runner. */ public void internalCloseRunner() { running = false; if (stopHandler != null) try { stopHandler.run(); } catch (Exception e) { } sketch.cleanup(); } /** * Check if the sketch is modified and ask user to save changes. * Immediately should be set true when quitting, or when the save should * not happen asynchronously. Come to think of it, that's always now? * @return false if canceling the close/quit operation */ protected boolean checkModified() { 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 String prompt = "Save changes to " + sketch.getName() + "? "; if (!Base.isMacOS()) { int result = JOptionPane.showConfirmDialog(this, prompt, "Close", JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE); if (result == JOptionPane.YES_OPTION) { return handleSave(true); } else if (result == JOptionPane.NO_OPTION) { return true; // ok to continue } else if (result == JOptionPane.CANCEL_OPTION) { return false; } else { throw new IllegalStateException(); } } else { // This code is disabled unless Java 1.5 is being used on Mac OS X // because of a Java bug that prevents the initial value of the // dialog from being set properly (at least on my MacBook Pro). // The bug causes the "Don't Save" option to be the highlighted, // blinking, default. This sucks. But I'll tell you what doesn't // suck--workarounds for the Mac and Apple's snobby attitude about it! // I think it's nifty that they treat their developers like dirt. // Pane formatting adapted from the quaqua guide // http://www.randelshofer.ch/quaqua/guide/joptionpane.html JOptionPane pane = new JOptionPane("<html> " + "<head> <style type=\"text/css\">"+ "b { font: 13pt \"Lucida Grande\" }"+ "p { font: 11pt \"Lucida Grande\"; margin-top: 8px }"+ "</style> </head>" + "<b>Do you want to save changes to this sketch<BR>" + " before closing?</b>" + "<p>If you don't save, your changes will be lost.", JOptionPane.QUESTION_MESSAGE); String[] options = new String[] { "Save", "Cancel", "Don't Save" }; pane.setOptions(options); // highlight the safest option ala apple hig pane.setInitialValue(options[0]); // on macosx, setting the destructive property places this option // away from the others at the lefthand side pane.putClientProperty("Quaqua.OptionPane.destructiveOption", new Integer(2)); JDialog dialog = pane.createDialog(this, null); dialog.setVisible(true); Object result = pane.getValue(); if (result == options[0]) { // save (and close/quit) return handleSave(true); } else if (result == options[2]) { // don't save (still close/quit) return true; } else { // cancel? return false; } } } /** * 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(String path, int codeIndex, int selStart, int selStop, int scrollPos) { internalCloseRunner(); handleOpenInternal(path); // 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); textarea.setScrollPosition(scrollPos); } /** * Second stage of open, occurs after having checked to see if the * modifications (if any) to the previous sketch need to be saved. */ protected boolean handleOpenInternal(String path) { // check to make sure that this .pde file is // in a folder of the same name File file = new File(path); File parentFile = new File(file.getParent()); String parentName = parentFile.getName(); String pdeName = parentName + ".pde"; File altFile = new File(file.getParent(), pdeName); String inoName = parentName + ".ino"; File altInoFile = new File(file.getParent(), pdeName); if (pdeName.equals(file.getName()) || inoName.equals(file.getName())) { // no beef with this guy } else if (altFile.exists()) { // user selected a .java from the same sketch, // but open the .pde instead path = altFile.getAbsolutePath(); } else if (altInoFile.exists()) { path = altInoFile.getAbsolutePath(); } else if (!path.endsWith(".pde") && !path.endsWith(".ino")) { Base.showWarning("Bad file selected", "Wiring can only open its own sketches\n" + "and other files ending in .pde", null); return false; } else { String properParent = file.getName().substring(0, file.getName().length() - 4); Object[] options = { "OK", "Cancel" }; String prompt = "The file \"" + file.getName() + "\" needs to be inside\n" + "a sketch folder named \"" + properParent + "\".\n" + "Create this folder, move the file, and continue?"; int result = JOptionPane.showOptionDialog(this, prompt, "Moving", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null, options, options[0]); if (result == JOptionPane.YES_OPTION) { // create properly named folder File properFolder = new File(file.getParent(), properParent); if (properFolder.exists()) { Base.showWarning("Error", "A folder named \"" + properParent + "\" " + "already exists. Can't open sketch.", null); return false; } if (!properFolder.mkdirs()) { Base.showWarning("Error", "Could not create the sketch folder.", null); return false; } // copy the sketch inside File properPdeFile = new File(properFolder, file.getName()); File origPdeFile = new File(path); try { Base.copyFile(origPdeFile, properPdeFile); } catch (IOException e) { Base.showWarning("Error", "Could not copy to a proper location.", e); return false; } // remove the original file, so user doesn't get confused origPdeFile.delete(); // update with the new path path = properPdeFile.getAbsolutePath(); } else if (result == JOptionPane.NO_OPTION) { return false; } } try { sketch = new Sketch(this, path); } catch (IOException e) { Base.showWarning("Error", "Could not create the sketch.", e); return false; } header.rebuild(); // Set the title of the window to "sketch_070752a - Processing 0126" setTitle(sketch.getName() + " | Wiring " + Base.VERSION_NAME); // Disable untitled setting from previous document, if any untitled = false; // Store information on who's open and running // (in case there's a crash or something that can't be recovered) base.storeSketches(); Preferences.save(); // opening was successful return true; } /** * Actually handle the save command. If 'immediately' is set to false, * this will happen in another thread so that the message area * will update and the save button will stay highlighted while the * save is happening. If 'immediately' is true, then it will happen * immediately. This is used during a quit, because invokeLater() * won't run properly while a quit is happening. This fixes * <A HREF="http://dev.processing.org/bugs/show_bug.cgi?id=276">Bug 276</A>. */ public boolean handleSave(boolean immediately) { handleStop(); // 0136 if (untitled) { return handleSaveAs(); // need to get the name, user might also cancel here } else if (immediately) { handleSave2(); } else { SwingUtilities.invokeLater(new Runnable() { public void run() { handleSave2(); } }); } return true; } protected void handleSave2() { toolbar.activate(EditorToolbar.SAVE); statusNotice("Saving..."); try { if (sketch.save()) { statusNotice("Done Saving."); } else { statusEmpty(); } } catch (Exception e) { // show the error as a message in the window statusError(e); } toolbar.deactivate(EditorToolbar.SAVE); } public boolean handleSaveAs() { handleStop(); toolbar.activate(EditorToolbar.SAVE); statusNotice("Saving..."); try { if (sketch.saveAs()) { statusNotice("Done Saving."); } else { statusNotice("Save Canceled."); return false; } } catch (Exception e) { // show the error as a message in the window statusError(e); } finally { // make sure the toolbar button deactivates toolbar.deactivate(EditorToolbar.SAVE); } return true; } /** * Called by Sketch → Export. * Handles calling the export() function on sketch, and * queues all the gui status stuff that comes along with it. * <p/> * Made synchronized to (hopefully) avoid problems of people * hitting export twice, quickly, and horking things up. */ synchronized public void handleExport(final boolean verbose) { splitPane.setDividerLocation(splitPane.getLastDividerLocation()); toolbar.activate(EditorToolbar.EXPORT); console.clear(); status.progress("Uploading to Wiring Hardware..."); new Thread(verbose ? exportAppHandler : exportHandler).start(); } class DefaultExportHandler implements Runnable { public void run() { try { serialMonitor.closeSerialPort(); serialMonitor.setVisible(false); toolbar.deactivate(EditorToolbar.SERIAL); uploading = true; boolean success = sketch.exportApplet(false); if (success) { statusNotice("Done Uploading."); uploading = false; if (Preferences.getBoolean("monitor.start")) { handleSerial(); System.out.println("Serial monitor started after upload (Wiring -> Preferences)"); } } else { // error message will already be visible } } catch (RunnerException e) { status.unprogress(); statusError(e); } catch (Exception e) { e.printStackTrace(); } status.unprogress(); uploading = false; toolbar.deactivate(EditorToolbar.EXPORT); } } class DefaultExportAppHandler implements Runnable { public void run() { try { serialMonitor.closeSerialPort(); serialMonitor.setVisible(false); toolbar.deactivate(EditorToolbar.SERIAL); uploading = true; boolean success = sketch.exportApplet(true); if (success) { statusNotice("Done Uploading."); uploading = false; if (Preferences.getBoolean("monitor.start")) { handleSerial(); System.out.println("Serial monitor started after upload (Wiring -> Preferences)"); } } else { // error message will already be visible } } catch (RunnerException e) { status.unprogress(); statusError(e); } catch (Exception e) { e.printStackTrace(); } status.unprogress(); uploading = false; toolbar.deactivate(EditorToolbar.EXPORT); } } /** * Handler for Sketch → Export Application */ synchronized public void handleExportApplication() { if (!handleExportCheckModified()) return; toolbar.activate(EditorToolbar.EXPORT); SwingUtilities.invokeLater(new Runnable() { public void run() { statusNotice("Exporting application..."); try { if (sketch.exportApplicationPrompt()) { Base.openFolder(sketch.getFolder()); statusNotice("Done exporting."); } else { // error message will already be visible // or there was no error, in which case it was canceled. } } catch (Exception e) { statusNotice("Error during export."); e.printStackTrace(); } toolbar.deactivate(EditorToolbar.EXPORT); }}); } /** * Checks to see if the sketch has been modified, and if so, * asks the user to save the sketch or cancel the export. * This prevents issues where an incomplete version of the sketch * would be exported, and is a fix for * <A HREF="http://dev.processing.org/bugs/show_bug.cgi?id=157">Bug 157</A> */ protected boolean handleExportCheckModified() { if (!sketch.isModified()) return true; Object[] options = { "OK", "Cancel" }; int result = JOptionPane.showOptionDialog(this, "Save changes before export?", "Save", JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null, options, options[0]); if (result == JOptionPane.OK_OPTION) { handleSave(true); } else { statusNotice("Export canceled, changes must first be saved."); return false; } return true; } protected void handleBurnBootloader(final String target, final String programmer) { console.clear(); statusNotice("Burning bootloader to I/O Board (this may take a minute)..."); SwingUtilities.invokeLater(new Runnable() { public void run() { try { Uploader uploader = new AvrdudeUploader(); if (uploader.burnBootloader(target, programmer)) { statusNotice("Done burning bootloader."); } else { statusError("Error while burning bootloader."); // error message will already be visible } } catch (RunnerException e) { statusError("Error while burning bootloader."); e.printStackTrace(); } catch (Exception e) { statusError("Error while burning bootloader."); e.printStackTrace(); } }}); } /** * Handler for File → Page Setup. */ public void handlePageSetup() { if (printerJob == null) { printerJob = PrinterJob.getPrinterJob(); } if (pageFormat == null) { pageFormat = printerJob.defaultPage(); } pageFormat = printerJob.pageDialog(pageFormat); } /** * Handler for File → Print. */ public void handlePrint() { statusNotice("Printing..."); if (printerJob == null) { printerJob = PrinterJob.getPrinterJob(); } if (pageFormat != null) { printerJob.setPrintable(textarea.getPainter(), pageFormat); } else { printerJob.setPrintable(textarea.getPainter()); } // set the name of the job to the code name printerJob.setJobName(sketch.getCurrentCode().getPrettyName()); if (printerJob.printDialog()) { try { printerJob.print(); statusNotice("Done printing."); } catch (PrinterException pe) { statusError("Error while printing."); pe.printStackTrace(); } } else { statusNotice("Printing canceled."); } } private int getFirmataSpeed() { String mess; String speedString = null; Integer speedInteger = new Integer(0); // create pattern mess = "Firmata\\.begin\\(\\s*(\\d+)\\s*\\)"; Pattern pattern = Pattern.compile(mess); // scan for matches in current tab ArrayList<String> matches = new ArrayList<String>(); Matcher matcher = pattern.matcher(sketch.current.program); while (matcher.find()) { speedString = matcher.group(1).toString(); } // return integer of speed if(null != speedString){ try{ return speedInteger.parseInt(speedString); }catch(Exception e){ // ignore exceptions } } // or fail return 0; } private int getSerialSpeed() { String mess; String speedString = null; Integer speedInteger = new Integer(0); // create pattern mess = "Serial\\.begin\\(\\s*(\\d+)\\s*\\)"; Pattern pattern = Pattern.compile(mess); // scan for matches in current tab ArrayList<String> matches = new ArrayList<String>(); Matcher matcher = pattern.matcher(sketch.current.program); while (matcher.find()) { //result = matcher.getMatch(); speedString = matcher.group(1).toString(); } // return integer of speed if(null != speedString){ try{ return speedInteger.parseInt(speedString); }catch(Exception e){ // ignore exceptions } } // or fail return 0; } public void handleSerial() { if (uploading) return; try { toolbar.activate(EditorToolbar.SERIAL); boolean detectSpeed = Preferences.getBoolean("monitor.detect_speed"); int portSpeed; if (detectSpeed == true) { portSpeed = Math.max(getSerialSpeed(), getFirmataSpeed()); } else { portSpeed = 0; } if(portSpeed == 0) // no Serial.begin or Firmata.begin guess serialMonitor.openSerialPort(); else // Serial or Firmata speed detected, assume that speed serialMonitor.openSerialPort(portSpeed); serialMonitor.setVisible(true); toolbar.deactivate(EditorToolbar.SERIAL); } catch (SerialException e) { status.error("Error opening serial port, see possible causes below..."); System.err.println(e.getMessage()); handleStop(); } } // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . /** * Show an error int the status bar. */ public void statusError(String what) { status.error(what); toolbar.deactivate(EditorToolbar.RUN); } /** * Show an exception in the editor status bar. */ public void statusError(Exception e) { e.printStackTrace(); if (e instanceof RunnerException) { RunnerException re = (RunnerException) e; if (re.hasCodeIndex()) { sketch.setCurrentCode(re.getCodeIndex()); } if (re.hasCodeLine()) { int line = re.getCodeLine(); // subtract one from the end so that the \n ain't included if (line >= textarea.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 (textarea.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()) { System.err.println("Bad error line: " + line); } else { textarea.select(textarea.getLineStartOffset(line), textarea.getLineStopOffset(line) - 1); } } } // Since this will catch all Exception types, spend some time figuring // out which kind and try to give a better error message to the user. String mess = e.getMessage(); if (mess != null) { String javaLang = "java.lang."; if (mess.indexOf(javaLang) == 0) { mess = mess.substring(javaLang.length()); } String rxString = "RuntimeException: "; if (mess.indexOf(rxString) == 0) { mess = mess.substring(rxString.length()); } statusError(mess); } } /** * Show a notice message in the editor status bar. */ public void statusNotice(String msg) { status.notice(msg); } /** * Clear the status area. */ public void statusEmpty() { statusNotice(EMPTY); } // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . protected void onBoardOrPortChange() { Map<String, String> boardPreferences = Base.getBoardPreferences(); lineStatus.setBoardName(boardPreferences.get("name")); lineStatus.setSerialPort(Preferences.get("serial.port")); lineStatus.repaint(); } /** * Returns the edit popup menu. */ class TextAreaPopup extends JPopupMenu { String referenceFile = null; String clickedURL; JMenuItem cutItem; JMenuItem copyItem; JMenuItem discourseItem; JMenuItem referenceItem; JMenuItem openURLItem; JSeparator openURLItemSeparator; public TextAreaPopup() { JMenuItem item; openURLItem = new JMenuItem("Open URL"); openURLItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { Base.openURL(clickedURL); } }); add(openURLItem); openURLItemSeparator = new JSeparator(); add(openURLItemSeparator); cutItem = new JMenuItem("Cut"); cutItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { handleCut(); } }); this.add(cutItem); copyItem = new JMenuItem("Copy"); copyItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { handleCopy(); } }); this.add(copyItem); discourseItem = new JMenuItem("Copy as HTML"); discourseItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { handleCopyAsHTML(); } }); this.add(discourseItem); item = new JMenuItem("Paste"); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { handlePaste(); } }); this.add(item); item = new JMenuItem("Select All"); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { handleSelectAll(); } }); this.add(item); this.addSeparator(); item = new JMenuItem("Comment/Uncomment"); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { handleCommentUncomment(); } }); this.add(item); item = new JMenuItem("Increase Indent"); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { handleIndentOutdent(true); } }); this.add(item); item = new JMenuItem("Decrease Indent"); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { handleIndentOutdent(false); } }); this.add(item); this.addSeparator(); referenceItem = new JMenuItem("Find in Reference"); referenceItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { handleFindReference(); } }); this.add(referenceItem); } // if no text is selected, disable copy and cut menu items public void show(Component component, int x, int y) { int lineNo = textarea.getLineOfOffset(textarea.xyToOffset(x, y)); int offset = textarea.xToOffset(lineNo, x); String line = textarea.getLineText(lineNo); clickedURL = textarea.checkClickedURL(line, offset); if (clickedURL != null) { openURLItem.setVisible(true); openURLItemSeparator.setVisible(true); } else { openURLItem.setVisible(false); openURLItemSeparator.setVisible(false); } if (textarea.isSelectionActive()) { cutItem.setEnabled(true); copyItem.setEnabled(true); discourseItem.setEnabled(true); String sel = textarea.getSelectedText().trim(); String referenceFile = PdeKeywords.getReference(sel); referenceItem.setEnabled(referenceFile != null); } else { cutItem.setEnabled(false); copyItem.setEnabled(false); discourseItem.setEnabled(false); referenceItem.setEnabled(false); } super.show(component, x, y); } } }