/* -*- 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 java.awt.*; import java.awt.event.*; import java.io.*; import java.text.SimpleDateFormat; import java.util.*; import java.net.*; import javax.swing.*; import processing.app.debug.Compiler; import processing.app.debug.Target; import processing.core.*; /** * The base class for the main processing application. * Primary role of this class is for platform identification and * general interaction with the system (launching URLs, loading * files and images, etc) that comes from that. */ public class Base { public static final int REVISION = 101; static String VERSION_NAME = "0101"; /** Set true if this a proper release rather than a numbered revision. */ static public boolean RELEASE = false; /** True if heavy debugging error/log messages are enabled */ static public boolean DEBUG = false; static HashMap<Integer, String> platformNames = new HashMap<Integer, String>(); static { platformNames.put(PConstants.WINDOWS, "windows"); platformNames.put(PConstants.MACOSX, "macosx"); platformNames.put(PConstants.LINUX, "linux"); } static HashMap<String, Integer> platformIndices = new HashMap<String, Integer>(); static { platformIndices.put("windows", PConstants.WINDOWS); platformIndices.put("macosx", PConstants.MACOSX); platformIndices.put("linux", PConstants.LINUX); } static Platform platform; static private boolean commandLine; // A single instance of the preferences window Preferences preferencesFrame; // set to true after the first time the menu is built. // so that the errors while building don't show up again. boolean builtOnce; static File buildFolder; // these are static because they're used by Sketch static private File examplesFolder; static private File librariesFolder; static private File toolsFolder; static private File hardwareFolder; static private File coresFolder; static private File coreLibrariesFolder; static HashSet<File> libraries; // maps imported packages to their library folder static HashMap<String, File> importToLibraryTable; static public HashMap<String, Target> targetsTable; static public HashMap<String, File> coresTable; // Location for untitled items static File untitledFolder; java.util.List<Editor> editors = Collections.synchronizedList(new ArrayList<Editor>()); Editor activeEditor; // a lone file menu to be used when all sketch windows are closed static public JMenu defaultFileMenu; static public void main(final String[] args) { javax.swing.SwingUtilities.invokeLater(new Runnable() { public void run() { createAndShowGUI(args); } }); } static private void createAndShowGUI(String[] args) { try { File versionFile = getContentFile("lib/version.txt"); if (versionFile.exists()) { String version = PApplet.loadStrings(versionFile)[0]; if (!version.equals(VERSION_NAME)) { VERSION_NAME = version; RELEASE = true; } } } catch (Exception e) { e.printStackTrace(); } initPlatform(); // Use native popups so they don't look so crappy on osx JPopupMenu.setDefaultLightWeightPopupEnabled(false); // Don't put anything above this line that might make GUI, // because the platform has to be inited properly first. // run static initialization that grabs all the prefs Preferences.init(null); // setup the theme coloring fun Theme.init(); // Set the look and feel before opening the window try { platform.setLookAndFeel(); } catch (Exception e) { String mess = e.getMessage(); if (mess.indexOf("ch.randelshofer.quaqua.QuaquaLookAndFeel") == -1) { System.err.println("Non-fatal error while setting the Look & Feel."); System.err.println("The error message follows, however Wiring should run fine."); System.err.println(mess); } } // Create a location for untitled sketches untitledFolder = createTempFolder("untitled"); untitledFolder.deleteOnExit(); new Base(args); } static protected void setCommandLine() { commandLine = true; } static protected boolean isCommandLine() { return commandLine; } static public void initPlatform() { try { Class<?> platformClass = Class.forName("processing.app.Platform"); if (Base.isMacOS()) { platformClass = Class.forName("processing.app.macosx.Platform"); } else if (Base.isWindows()) { platformClass = Class.forName("processing.app.windows.Platform"); } else if (Base.isLinux()) { platformClass = Class.forName("processing.app.linux.Platform"); } platform = (Platform) platformClass.newInstance(); } catch (Exception e) { Base.showError("Problem Setting the Platform", "An unknown error occurred while trying to load\n" + "platform-specific code for your machine.", e); } } public Base(String[] args) { // Get paths for the libraries and examples in the Processing folder examplesFolder = getContentFile("examples"); librariesFolder = getContentFile("libraries"); toolsFolder = getContentFile("tools"); // Put this after loading the examples, so that building the default file // menu works on Mac OS X (since it needs examplesFolder to be set). platform.init(this); // Get the sketchbook path, and make sure it's set properly String sketchbookPath = Preferences.get("sketchbook.path"); // If a value is at least set, first check to see if the folder exists. // If it doesn't, warn the user that the sketchbook folder is being reset. if (sketchbookPath != null) { File skechbookFolder = new File(sketchbookPath); if (!skechbookFolder.exists()) { Base.showWarning("Sketchbook folder disappeared", "The sketchbook folder no longer exists.\n" + "Wiring will switch to the default sketchbook\n" + "location, and create a new sketchbook folder if\n" + "necessary. Wiring will then stop talking about\n" + "himself in the third person.", null); sketchbookPath = null; } } // If not path is set, get the default sketchbook folder for this platform if (sketchbookPath == null) { File defaultFolder = getDefaultSketchbookFolder(); Preferences.set("sketchbook.path", defaultFolder.getAbsolutePath()); if (!defaultFolder.exists()) { defaultFolder.mkdirs(); } } targetsTable = new HashMap<String, Target>(); loadHardware(getHardwareFolder()); loadHardware(getSketchbookHardwareFolder()); if (coreAvailable()) { coresTable = new HashMap<String, File>(); loadCores(getCoresFolder()); loadCores(getSketchbookCoresFolder()); coreLibrariesFolder = getCoreLibrariesFolder(); } // Check if there were previously opened sketches to be restored boolean opened = restoreSketches(); // Check if any files were passed in on the command line for (int i = 0; i < args.length; i++) { String path = args[i]; // Fix a problem with systems that use a non-ASCII languages. Paths are // being passed in with 8.3 syntax, which makes the sketch loader code // unhappy, since the sketch folder naming doesn't match up correctly. // http://dev.processing.org/bugs/show_bug.cgi?id=1089 if (isWindows()) { try { File file = new File(args[i]); path = file.getCanonicalPath(); } catch (IOException e) { e.printStackTrace(); } } if (handleOpen(path) != null) { opened = true; } } // Create a new empty window (will be replaced with any files to be opened) if (!opened) { handleNew(); } // check for updates if (Preferences.getBoolean("update.check")) { new UpdateCheck(this); } } /** * Post-constructor setup for the editor area. Loads the last * sketch that was used (if any), and restores other Editor settings. * The complement to "storePreferences", this is called when the * application is first launched. */ protected boolean restoreSketches() { if (!Preferences.getBoolean("last.sketch.restore")) { return false; } // figure out window placement Dimension screen = Toolkit.getDefaultToolkit().getScreenSize(); boolean windowPositionValid = true; if (Preferences.get("last.screen.height") != null) { // if screen size has changed, the window coordinates no longer // make sense, so don't use them unless they're identical int screenW = Preferences.getInteger("last.screen.width"); int screenH = Preferences.getInteger("last.screen.height"); if ((screen.width != screenW) || (screen.height != screenH)) { windowPositionValid = false; } } else { windowPositionValid = false; } // Save the sketch path and window placement for each open sketch int count = Preferences.getInteger("last.sketch.count"); int opened = 0; for (int i = 0; i < count; i++) { String path = Preferences.get("last.sketch" + i + ".path"); int[] location; if (windowPositionValid) { String locationStr = Preferences.get("last.sketch" + i + ".location"); location = PApplet.parseInt(PApplet.split(locationStr, ',')); } else { location = nextEditorLocation(); } // If file did not exist, null will be returned for the Editor if (handleOpen(path, location) != null) { opened++; } } return (opened > 0); } /** * Store list of sketches that are currently open. * Called when the application is quitting and documents are still open. */ protected void storeSketches() { // Save the width and height of the screen Dimension screen = Toolkit.getDefaultToolkit().getScreenSize(); Preferences.setInteger("last.screen.width", screen.width); Preferences.setInteger("last.screen.height", screen.height); String untitledPath = untitledFolder.getAbsolutePath(); // Save the sketch path and window placement for each open sketch int index = 0; for (Editor editor : editors) { String path = editor.getSketch().getMainFilePath(); // In case of a crash, save untitled sketches if they contain changes. // (Added this for release 0158, may not be a good idea.) if (path.startsWith(untitledPath) && !editor.getSketch().isModified()) { continue; } Preferences.set("last.sketch" + index + ".path", path); int[] location = editor.getPlacement(); String locationStr = PApplet.join(PApplet.str(location), ","); Preferences.set("last.sketch" + index + ".location", locationStr); index++; } Preferences.setInteger("last.sketch.count", index); } // If a sketch is untitled on quit, may need to store the new name // rather than the location from the temp folder. protected void storeSketchPath(Editor editor, int index) { String path = editor.getSketch().getMainFilePath(); String untitledPath = untitledFolder.getAbsolutePath(); if (path.startsWith(untitledPath)) { path = ""; } Preferences.set("last.sketch" + index + ".path", path); } // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . /** Command on Mac OS X, Ctrl on Windows and Linux */ static final int SHORTCUT_KEY_MASK = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(); /** Command-W on Mac OS X, Ctrl-W on Windows and Linux */ static final KeyStroke WINDOW_CLOSE_KEYSTROKE = KeyStroke.getKeyStroke('W', SHORTCUT_KEY_MASK); /** Command-Option on Mac OS X, Ctrl-Alt on Windows and Linux */ static final int SHORTCUT_ALT_KEY_MASK = ActionEvent.ALT_MASK | Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(); /** * A software engineer, somewhere, needs to have his abstraction * taken away. In some countries they jail or beat people for crafting * the sort of API that would require a five line helper function * just to set the shortcut key for a menu item. */ static public JMenuItem newJMenuItem(String title, int what) { JMenuItem menuItem = new JMenuItem(title); int modifiers = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(); menuItem.setAccelerator(KeyStroke.getKeyStroke(what, modifiers)); return menuItem; } /** * Like newJMenuItem() but adds shift as a modifier for the shortcut. */ static public JMenuItem newJMenuItemShift(String title, int what) { JMenuItem menuItem = new JMenuItem(title); int modifiers = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(); modifiers |= ActionEvent.SHIFT_MASK; menuItem.setAccelerator(KeyStroke.getKeyStroke(what, modifiers)); return menuItem; } /** * Same as newJMenuItem(), but adds the ALT (on Linux and Windows) * or OPTION (on Mac OS X) key as a modifier. */ static public JMenuItem newJMenuItemAlt(String title, int what) { JMenuItem menuItem = new JMenuItem(title); menuItem.setAccelerator(KeyStroke.getKeyStroke(what, SHORTCUT_ALT_KEY_MASK)); return menuItem; } static public JCheckBoxMenuItem newJCheckBoxMenuItem(String title, int what) { JCheckBoxMenuItem menuItem = new JCheckBoxMenuItem(title); int modifiers = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(); menuItem.setAccelerator(KeyStroke.getKeyStroke(what, modifiers)); return menuItem; } // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . static JMenu sketchbookMenu; static JMenu recentMenu; static JMenu examplesMenu; static JMenu importMenu; protected void rebuildWindowMenu(JMenu menu) { JMenuItem item; menu.removeAll(); for (Editor editor : editors) { item = new JMenuItem(editor.getSketch().getName()); item.setActionCommand(editor.getSketch().getMainFilePath()); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { handleWindow(e.getActionCommand()); } }); menu.add(item); } } public JMenu buildWindowMenu() { JMenuItem item; JMenu windowMenu = new JMenu("Window"); for (Editor editor : editors) { item = new JMenuItem(editor.getSketch().getName()); item.setActionCommand(editor.getSketch().getMainFilePath()); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { handleWindow(e.getActionCommand()); } }); windowMenu.add(item); } return windowMenu; } public JMenu buildFileMenu(final Editor editor) { JMenuItem item; JMenu fileMenu = new JMenu("File"); item = newJMenuItem("New", 'N'); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { handleNew(); } }); fileMenu.add(item); item = newJMenuItem("Open...", 'O'); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { handleOpenPrompt(); } }); fileMenu.add(item); if (recentMenu == null) { recentMenu = new JMenu("Open Recent"); rebuildRecentMenu(recentMenu); } fileMenu.add(recentMenu); if (sketchbookMenu == null) { sketchbookMenu = new JMenu("Sketchbook"); rebuildSketchbookMenu(sketchbookMenu); } fileMenu.add(sketchbookMenu); item = newJMenuItem("Close", 'W'); if (editor != null) { item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { handleClose(editor); } }); } else { item.setEnabled(false); } fileMenu.add(item); item = newJMenuItem("Save", 'S'); if (editor != null) { item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { editor.handleSave(false); } }); editor.setSaveItem(item); } else { item.setEnabled(false); } fileMenu.add(item); item = newJMenuItemShift("Save As...", 'S'); if (editor != null) { item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { editor.handleSaveAs(); } }); editor.setSaveAsItem(item); } else { item.setEnabled(false); } fileMenu.add(item); item = newJMenuItem("Upload to Wiring hardware", 'U'); if (editor != null) { item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { editor.handleExport(false); } }); } else { item.setEnabled(false); } fileMenu.add(item); fileMenu.addSeparator(); item = newJMenuItemShift("Page Setup", 'P'); if (editor != null) { item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { editor.handlePageSetup(); } }); } else { item.setEnabled(false); } fileMenu.add(item); item = newJMenuItem("Print", 'P'); if (editor != null) { item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { editor.handlePrint(); } }); } else { item.setEnabled(false); } fileMenu.add(item); // Mac OS X already has its own preferences and quit menu. // That's right! Think different, b*tches! if (!Base.isMacOS()) { fileMenu.addSeparator(); item = newJMenuItem("Preferences", ','); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { handlePrefs(); } }); fileMenu.add(item); fileMenu.addSeparator(); item = newJMenuItem("Quit", 'Q'); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { handleQuit(); } }); fileMenu.add(item); } return fileMenu; } // ................................................................. // Because of variations in native windowing systems, no guarantees about // changes to the focused and active Windows can be made. Developers must // never assume that this Window is the focused or active Window until this // Window receives a WINDOW_GAINED_FOCUS or WINDOW_ACTIVATED event. protected void handleActivated(Editor whichEditor) { activeEditor = whichEditor; // set the current window to be the console that's getting output EditorConsole.setEditor(activeEditor); } protected int[] nextEditorLocation() { Dimension screen = Toolkit.getDefaultToolkit().getScreenSize(); int defaultWidth = Preferences.getInteger("editor.window.width.default"); int defaultHeight = Preferences.getInteger("editor.window.height.default"); if (activeEditor == null) { // If no current active editor, use default placement return new int[] { (screen.width - defaultWidth) / 2, (screen.height - defaultHeight) / 2, defaultWidth, defaultHeight, 0 }; } else { // With a currently active editor, open the new window // using the same dimensions, but offset slightly. synchronized (editors) { final int OVER = 50; Editor lastOpened = editors.get(editors.size() - 1); int[] location = lastOpened.getPlacement(); // Just in case the bounds for that window are bad location[0] += OVER; location[1] += OVER; if (location[0] == OVER || location[2] == OVER || location[0] + location[2] > screen.width || location[1] + location[3] > screen.height) { // Warp the next window to a randomish location on screen. return new int[] { (int) (Math.random() * (screen.width - defaultWidth)), (int) (Math.random() * (screen.height - defaultHeight)), defaultWidth, defaultHeight, 0 }; } return location; } } } // ................................................................. boolean breakTime = false; String[] months = { "jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec" }; /** * Handle creating a sketch folder, return its base .pde file * or null if the operation was canceled. * @param shift whether shift is pressed, which will invert prompt setting * @param noPrompt disable prompt, no matter the setting */ protected String createNewUntitled() throws IOException { File newbieDir = null; String newbieName = null; // In 0126, untitled sketches will begin in the temp folder, // and then moved to a new location because Save will default to Save As. File sketchbookDir = getSketchbookFolder(); File newbieParentDir = untitledFolder; String prefix = Preferences.get("editor.untitled.prefix"); // Use a generic name like sketch_031008a, the date plus a char int index = 0; String format = Preferences.get("editor.untitled.suffix"); String suffix = null; if (format == null) { Calendar cal = Calendar.getInstance(); int day = cal.get(Calendar.DAY_OF_MONTH); // 1..31 int month = cal.get(Calendar.MONTH); // 0..11 suffix = months[month] + PApplet.nf(day, 2); } else { SimpleDateFormat formatter = new SimpleDateFormat(format); suffix = formatter.format(new Date()); } do { if (index == 26) { // In 0159, avoid running past z by sending people outdoors. if (!breakTime) { Base.showWarning("Time for a Break", "You've reached the limit for auto naming of new sketches\n" + "for the day. How about going for a walk instead?", null); breakTime = true; } else { Base.showWarning("Sunshine", "No really, time for some fresh air for you.", null); } return null; } newbieName = prefix + suffix + ((char) ('a' + index)); // Also sanitize the name since it might do strange things on // non-English systems that don't use this sort of date format. // http://code.google.com/p/processing/issues/detail?id=283 newbieName = Sketch.sanitizeName(newbieName); newbieDir = new File(newbieParentDir, newbieName); index++; // Make sure it's not in the temp folder *and* it's not in the sketchbook } while (newbieDir.exists() || new File(sketchbookDir, newbieName).exists()); // Make the directory for the new sketch newbieDir.mkdirs(); // Make an empty pde file File newbieFile = new File(newbieDir, newbieName + ".pde"); new FileOutputStream(newbieFile); // create the file return newbieFile.getAbsolutePath(); } /** * Create a new untitled document in a new sketch window. */ public void handleNew() { try { String path = createNewUntitled(); if (path != null) { Editor editor = handleOpen(path); editor.untitled = true; StringBuffer buffer = new StringBuffer(); buffer.append("void setup()\n{\n\n}\n\n"); buffer.append("void loop()\n{\n\n}"); buffer.append(editor.getText()); editor.setText(buffer.toString()); editor.setSelection(0, 0); // scroll to start editor.getSketch().setModified(false); } } catch (IOException e) { if (activeEditor != null) { activeEditor.statusError(e); } } } /** * 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 { String path = createNewUntitled(); if (path != null) { activeEditor.handleOpenInternal(path); activeEditor.untitled = true; StringBuffer buffer = new StringBuffer(); buffer.append("void setup()\n{\n\n}\n\n"); buffer.append("void loop()\n{\n\n}"); buffer.append(activeEditor.getText()); activeEditor.setText(buffer.toString()); activeEditor.setSelection(0, 0); // scroll to start activeEditor.getSketch().setModified(false); } } catch (IOException e) { activeEditor.statusError(e); } } /** * Open a sketch, replacing the sketch in the current window. * @param path Location of the primary pde file for the sketch. */ public void handleOpenReplace(String path) { 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(path); 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. */ public void handleOpenPrompt() { // get the frontmost window frame for placing file dialog FileDialog fd = new FileDialog(activeEditor, "Open a Wiring sketch...", FileDialog.LOAD); // Only show .pde files as eligible bachelors fd.setFilenameFilter(new FilenameFilter() { public boolean accept(File dir, String name) { // TODO this doesn't seem to ever be used. AWESOME. return name.toLowerCase().endsWith(".pde") || name.toLowerCase().endsWith(".ino"); } }); fd.setVisible(true); String directory = fd.getDirectory(); String filename = fd.getFile(); // User canceled selection if (filename == null) return; File inputFile = new File(directory, filename); handleOpen(inputFile.getAbsolutePath()); } protected void handleWindow(String window) { for (Editor editor : editors) { if (editor.getSketch().getMainFilePath().equals(window)) { editor.toFront(); } } } /** * Open a sketch in a new window. * @param path Path to the pde file for the sketch in question * @return the Editor object, so that properties (like 'untitled') * can be set by the caller */ public Editor handleOpen(String path) { return handleOpen(path, nextEditorLocation()); } protected Editor handleOpen(String path, int[] location) { File file = new File(path); if (!file.exists()) return null; // Cycle through open windows to make sure that it's not already open. for (Editor editor : editors) { if (editor.getSketch().getMainFilePath().equals(path)) { editor.toFront(); return editor; } } Editor editor = new Editor(this, path, location); // Make sure that the sketch actually loaded if (editor.getSketch() == null) { return null; // Just walk away quietly } editors.add(editor); // now that we're ready, show the window // (don't do earlier, cuz we might move it based on a window being closed) editor.setVisible(true); int maximum = Preferences.getInteger("recent.sketch.maximum"); if (!path.startsWith(untitledFolder.getAbsolutePath())) { boolean needShuffle = true; for (int i=0; i<maximum; i++) { String savedPath = Preferences.get("recent.sketch" + i + ".path"); if(savedPath != null) { if(savedPath.length()>0) { if (savedPath.equals(path)) { for (int j=i; j<(maximum-1); j++) { String updatePath = Preferences.get("recent.sketch"+(j+1)+ ".path"); if(updatePath != null) { if(updatePath.length() >= 0) { Preferences.set("recent.sketch"+(j)+ ".path", updatePath); } } else { Preferences.set("recent.sketch"+(j)+ ".path", ""); } } break; } } } } if (needShuffle) { //shuffle every item down the list for (int i=maximum-1; i>0; i--) { String savedPath = Preferences.get("recent.sketch"+(i-1)+ ".path"); if(savedPath != null) { if(savedPath.length()>=0) { Preferences.set("recent.sketch"+i+ ".path", savedPath); } } else { Preferences.set("recent.sketch"+i+ ".path", ""); } } } Preferences.set("recent.sketch0.path",path); Preferences.setInteger("recent.sketch.maximum", Math.min(maximum, 10)); Preferences.save(); } return editor; } /** * Close a sketch as specified by its editor window. * @param editor Editor object of the sketch to be closed. * @return true if succeeded in closing, false if canceled. */ public boolean handleClose(Editor editor) { if (!editor.checkModified()) { return false; } // Close the running window, avoid window boogers with multiple sketches editor.internalCloseRunner(); if (editors.size() == 1) { if (Base.isMacOS()) { Object[] options = { "OK", "Cancel" }; String prompt = "<html> " + "<head> <style type=\"text/css\">"+ "b { font: 13pt \"Lucida Grande\" }"+ "p { font: 11pt \"Lucida Grande\"; margin-top: 8px }"+ "</style> </head>" + "<b>Are you sure you want to Quit?</b>" + "<p>Closing the last open sketch will quit Wiring."; int result = JOptionPane.showOptionDialog(editor, prompt, "Quit", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null, options, options[0]); if (result == JOptionPane.NO_OPTION || result == JOptionPane.CLOSED_OPTION) { return false; } } // This will store the sketch count as zero editors.remove(editor); Editor.serialMonitor.closeSerialPort(); storeSketches(); // Save out the current prefs state Preferences.save(); System.exit(0); } else { // More than one editor window open, // proceed with closing the current window. editor.setVisible(false); editor.dispose(); editors.remove(editor); } return true; } /** * Handler for File → Quit. * @return false if canceled, true otherwise. */ public boolean handleQuit() { // If quit is canceled, this will be replaced anyway // by a later handleQuit() that is not canceled. storeSketches(); Editor.serialMonitor.closeSerialPort(); if (handleQuitEach()) { // make sure running sketches close before quitting for (Editor editor : editors) { editor.internalCloseRunner(); } // Save out the current prefs state Preferences.save(); if (!Base.isMacOS()) { // If this was fired from the menu or an AppleEvent (the Finder), // then Mac OS X will send the terminate signal itself. System.exit(0); } return true; } return false; } /** * Attempt to close each open sketch in preparation for quitting. * @return false if canceled along the way */ protected boolean handleQuitEach() { int index = 0; for (Editor editor : editors) { if (editor.checkModified()) { // Update to the new/final sketch path for this fella storeSketchPath(editor, index); index++; } else { return false; } } return true; } // ................................................................. /** * Asynchronous version of menu rebuild to be used on save and rename * to prevent the interface from locking up until the menus are done. */ protected void rebuildRecentMenu() { EventQueue.invokeLater(new Runnable() { public void run() { rebuildRecentMenu(recentMenu); } }); } protected void rebuildRecentMenu(JMenu menu) { JMenuItem item; menu.removeAll(); int maximum = Preferences.getInteger("recent.sketch.maximum"); ActionListener listener = new ActionListener() { public void actionPerformed(ActionEvent e) { String path = e.getActionCommand(); if (new File(path).exists()) { handleOpen(path); } else { showWarning("Sketch Does Not Exist", "The selected sketch no longer exists.\n" + "You may need to restart Wiring to update\n" + "the sketchbook menu.", null); } } }; for (int i = 0; i < maximum; i++) { String path = Preferences.get("recent.sketch" + i + ".path"); if(path != null) { if(path.length()>0) { File entry = new File(path); if(entry.exists()) { item = new JMenuItem(path); item.addActionListener(listener); item.setActionCommand(entry.getAbsolutePath()); menu.add(item); } } } } } /** * Asynchronous version of menu rebuild to be used on save and rename * to prevent the interface from locking up until the menus are done. */ protected void rebuildSketchbookMenus() { EventQueue.invokeLater(new Runnable() { public void run() { rebuildSketchbookMenu(sketchbookMenu); rebuildToolbarMenu(Editor.toolbarMenu); } }); } protected void rebuildToolbarMenu(JMenu menu) { JMenuItem item; menu.removeAll(); // Add the single "Open" item item = newJMenuItem("Open...", 'O'); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { handleOpenPrompt(); } }); menu.add(item); menu.addSeparator(); // Add a list of all sketches and subfolders try { boolean sketches = addSketches(menu, getSketchbookFolder(), true); if (sketches) menu.addSeparator(); } catch (IOException e) { e.printStackTrace(); } // Add each of the subfolders of examples directly to the menu try { boolean found = addSketches(menu, examplesFolder, true); if (found) menu.addSeparator(); found = addSketches(menu, getSketchbookLibrariesFolder(), true); if (found) menu.addSeparator(); found = addSketches(menu, librariesFolder, true); if(coreAvailable()) { if (found) menu.addSeparator(); addSketches(menu, coreLibrariesFolder, true); } } catch (IOException e) { e.printStackTrace(); } } protected void rebuildSketchbookMenu(JMenu menu) { try { menu.removeAll(); addSketches(menu, getSketchbookFolder(), false); } catch (IOException e) { e.printStackTrace(); } } public void rebuildImportMenu() { importMenu.removeAll(); // reset the set of libraries libraries = new HashSet<File>(); // reset the table mapping imports to libraries importToLibraryTable = new HashMap<String, File>(); boolean previousLibrariesFound = false; int separatorIndex = importMenu.getItemCount(); // Add libraries found in the current cores/XXX folder if (coreAvailable()) try { File coreLibraries = getCoreLibrariesFolder(); JMenu corelibs = new JMenu("Core"); if (coreLibraries != null) { boolean found = addLibraries(corelibs, coreLibraries); if (found) { corelibs.setEnabled(true); importMenu.insert(corelibs, separatorIndex); if(previousLibrariesFound) importMenu.insertSeparator(separatorIndex); previousLibrariesFound = true; } } } catch (IOException e) { e.printStackTrace(); } separatorIndex = importMenu.getItemCount(); // Add from the "libraries" subfolder in the Wiring directory try { JMenu commonLibs = new JMenu("Cross-platform"); boolean found = addLibraries(commonLibs, librariesFolder); if (found) { commonLibs.setEnabled(true); importMenu.insert(commonLibs, separatorIndex); if (previousLibrariesFound) importMenu.insertSeparator(separatorIndex); previousLibrariesFound = true; } } catch (IOException e) { e.printStackTrace(); } // Add libraries found in the sketchbook folder separatorIndex = importMenu.getItemCount(); try { File sketchbookLibraries = getSketchbookLibrariesFolder(); JMenu contrib = new JMenu("Contributed"); boolean found = addLibraries(contrib, sketchbookLibraries); if (found) { contrib.setEnabled(true); importMenu.insert(contrib, separatorIndex); if(previousLibrariesFound) importMenu.insertSeparator(separatorIndex); } } catch (IOException e) { e.printStackTrace(); } } public void rebuildExamplesMenu() { try { int separatorIndex = examplesMenu.getItemCount(); examplesMenu.removeAll(); boolean found = addSketches(examplesMenu, examplesFolder, false); separatorIndex = examplesMenu.getItemCount(); int librariesSeparatorIndex = separatorIndex; boolean librariesExamplesFound = false; if (coreAvailable()) { separatorIndex = examplesMenu.getItemCount(); JMenu coreExamples = new JMenu("Core"); found = addSketches(coreExamples, getCoreLibrariesFolder(), false); if (found) { coreExamples.setEnabled(true); examplesMenu.insert(coreExamples, separatorIndex); examplesMenu.insertSeparator(separatorIndex); librariesExamplesFound = true; } } separatorIndex = examplesMenu.getItemCount(); JMenu librariesExamples = new JMenu("Cross-platform"); found = addSketches(librariesExamples, librariesFolder, false); if (found) { librariesExamples.setEnabled(true); examplesMenu.insert(librariesExamples, separatorIndex); examplesMenu.insertSeparator(separatorIndex); librariesExamplesFound = true; } separatorIndex = examplesMenu.getItemCount(); JMenu contributedExamples = new JMenu("Contributed"); found = addSketches(contributedExamples, getSketchbookLibrariesFolder(), false); if (found) { contributedExamples.setEnabled(true); examplesMenu.insert(contributedExamples, separatorIndex); examplesMenu.insertSeparator(separatorIndex); librariesExamplesFound = true; } if (librariesExamplesFound) { JMenuItem librariesMenu = new JMenuItem("Libraries"); librariesMenu.setEnabled(false); librariesMenu.setFont(librariesMenu.getFont().deriveFont(Font.BOLD)); librariesMenu.setFont(librariesMenu.getFont().deriveFont(Font.ITALIC)); examplesMenu.insert(librariesMenu, librariesSeparatorIndex); examplesMenu.insertSeparator(librariesSeparatorIndex); } } catch (IOException e) { e.printStackTrace(); } } public void rebuildExamplesMenu(JMenu menu) { try { menu.removeAll(); boolean found = addSketches(menu, examplesFolder, false); if (found) menu.addSeparator(); found = addSketches(menu, getSketchbookLibrariesFolder(), false); if (found) menu.addSeparator(); found = addSketches(menu, librariesFolder, false); if (found) menu.addSeparator(); if (coreAvailable()) addSketches(menu, getCoreLibrariesFolder(), false); } catch (IOException e) { e.printStackTrace(); } } public void onBoardOrPortChange() { for (Editor editor : editors) { editor.onBoardOrPortChange(); } } public void rebuildBoardsMenu(JMenu menu) { targetsTable = new HashMap<String, Target>(); loadHardware(getHardwareFolder()); loadHardware(getSketchbookHardwareFolder()); if (coreAvailable()) { coresTable = new HashMap<String, File>(); loadCores(getCoresFolder()); loadCores(getSketchbookCoresFolder()); coreLibrariesFolder = getCoreLibrariesFolder(); } menu.removeAll(); ButtonGroup group = new ButtonGroup(); for (Target target : targetsTable.values()) { JMenu targetSubMenu = new JMenu(target.getName()); for (String board : target.getBoards().keySet()) { AbstractAction action = new AbstractAction(target.getBoards().get(board).get("name")) { public void actionPerformed(ActionEvent actionevent) { Preferences.set("target", (String) getValue("target")); Preferences.set("board", (String) getValue("board")); onBoardOrPortChange(); rebuildExamplesMenu(); rebuildImportMenu(); } }; action.putValue("target", target.getName()); action.putValue("board", board); JMenuItem item = new JRadioButtonMenuItem(action); if (target.getName().equals(Preferences.get("target")) && board.equals(Preferences.get("board"))) { item.setSelected(true); } group.add(item); targetSubMenu.add(item); } menu.add(targetSubMenu); } } public void rebuildBurnBootloaderMenu(JMenu menu) { menu.removeAll(); Map<String, String> boardPreferences = Base.getBoardPreferences(); for (Target target : targetsTable.values()) { if (target.getName().equals(Preferences.get("target"))) { JMenuItem targetItem = new JMenuItem(target.getName()+" : "+boardPreferences.get("name")); targetItem.setEnabled(false); targetItem.setFont(targetItem.getFont().deriveFont(Font.BOLD)); targetItem.setFont(targetItem.getFont().deriveFont(Font.ITALIC)); menu.add(targetItem); for (String programmer : target.getProgrammers().keySet()) { AbstractAction action = new AbstractAction( "w/ " + target.getProgrammers().get(programmer).get("name")) { public void actionPerformed(ActionEvent actionevent) { activeEditor.handleBurnBootloader((String) getValue("target"), (String) getValue("programmer")); } }; action.putValue("target", target.getName()); action.putValue("programmer", programmer); JMenuItem item = new JMenuItem(action); menu.add(item); } } } } /** * Scan a folder recursively, and add any sketches found to the menu * specified. Set the openReplaces parameter to true when opening the sketch * should replace the sketch in the current window, or false when the * sketch should open in a new window. */ protected boolean addSketches(JMenu menu, File folder, final boolean replaceExisting) throws IOException { // skip .DS_Store files, etc (this shouldn't actually be necessary) if (!folder.isDirectory()) return false; String[] list = folder.list(); // If a bad folder or unreadable or whatever, this will come back null if (list == null) return false; // Alphabetize list, since it's not always alpha order Arrays.sort(list, String.CASE_INSENSITIVE_ORDER); ActionListener listener = new ActionListener() { public void actionPerformed(ActionEvent e) { String path = e.getActionCommand(); if (new File(path).exists()) { boolean replace = replaceExisting; if ((e.getModifiers() & ActionEvent.SHIFT_MASK) != 0) { replace = !replace; } if (replace) { handleOpenReplace(path); } else { handleOpen(path); } } else { showWarning("Sketch Does Not Exist", "The selected sketch no longer exists.\n" + "You may need to restart Wiring to update\n" + "the sketchbook menu.", null); } } }; boolean ifound = false; for (int i = 0; i < list.length; i++) { if ((list[i].charAt(0) == '.') || list[i].equals("CVS")) continue; File subfolder = new File(folder, list[i]); if (!subfolder.isDirectory()) continue; File entry = new File(subfolder, list[i] + ".pde"); if (!entry.exists() && (new File(subfolder, list[i] + ".ino")).exists()) { entry = new File(subfolder, list[i] + ".ino"); } // if a .pde file of the same prefix as the folder exists.. if (entry.exists()) { if (!Sketch.isSanitaryName(list[i])) { if (!builtOnce) { String complaining = "The sketch \"" + list[i] + "\" cannot be used.\n" + "Sketch names must contain only basic letters and numbers\n" + "(ASCII-only with no spaces, " + "and it cannot start with a number).\n" + "To get rid of this message, remove the sketch from\n" + entry.getAbsolutePath(); Base.showMessage("Ignoring sketch with bad name", complaining); } continue; } JMenuItem item = new JMenuItem(list[i]); item.addActionListener(listener); item.setActionCommand(entry.getAbsolutePath()); menu.add(item); ifound = true; } else { // not a sketch folder, but maybe a subfolder containing sketches JMenu submenu = new JMenu(list[i]); // needs to be separate var otherwise would set ifound to false boolean found = addSketches(submenu, subfolder, replaceExisting); //, false); if (found) { menu.add(submenu); ifound = true; } } } return ifound; // actually ignored, but.. } protected boolean addLibraries(JMenu menu, File folder) throws IOException { if (!folder.isDirectory()) return false; String list[] = folder.list(new FilenameFilter() { public boolean accept(File dir, String name) { // skip .DS_Store files, .svn folders, etc if (name.charAt(0) == '.') return false; if (name.equals("CVS")) return false; return (new File(dir, name).isDirectory()); } }); // if a bad folder or something like that, this might come back null if (list == null) return false; // alphabetize list, since it's not always alpha order Arrays.sort(list, String.CASE_INSENSITIVE_ORDER); ActionListener listener = new ActionListener() { public void actionPerformed(ActionEvent e) { activeEditor.getSketch().importLibrary(e.getActionCommand()); } }; boolean ifound = false; for (String potentialName : list) { File subfolder = new File(folder, potentialName); String sanityCheck = Sketch.sanitizeName(potentialName); if (!sanityCheck.equals(potentialName)) { String mess = "The library \"" + potentialName + "\" cannot be used.\n" + "Library names must contain only basic letters and numbers.\n" + "(ASCII only and no spaces, and it cannot start with a number)"; Base.showMessage("Ignoring bad library name", mess); continue; } String libraryName = potentialName; libraries.add(subfolder); String packages[] = Compiler.headerListFromIncludePath(subfolder.getAbsolutePath()); for (String pkg : packages) { importToLibraryTable.put(pkg, subfolder); } JMenuItem item = new JMenuItem(libraryName); item.addActionListener(listener); item.setActionCommand(subfolder.getAbsolutePath()); menu.add(item); ifound = true; // XXX: DAM: should recurse here so that library folders can be nested } return ifound; } protected void loadHardware(File folder) { if (!folder.isDirectory()) return; String list[] = folder.list(new FilenameFilter() { public boolean accept(File dir, String name) { // skip .DS_Store files, .svn folders, etc if (name.charAt(0) == '.') return false; if (name.equals("CVS")) return false; return (new File(dir, name).isDirectory()); } }); // if a bad folder or something like that, this might come back null if (list == null) return; // alphabetize list, since it's not always alpha order // replaced hella slow bubble sort with this feller for 0093 Arrays.sort(list, String.CASE_INSENSITIVE_ORDER); for (String target : list) { File subfolder = new File(folder, target); targetsTable.put(target, new Target(target, subfolder)); } } protected void loadCores(File folder) { if (!folder.isDirectory()) return; String list[] = folder.list(new FilenameFilter() { public boolean accept(File dir, String name) { // skip .DS_Store files, .svn folders, etc if (name.charAt(0) == '.') return false; if (name.equals("CVS")) return false; return (new File(dir, name).isDirectory()); } }); // if a bad folder or something like that, this might come back null if (list == null) return; // alphabetize list, since it's not always alpha order // replaced hella slow bubble sort with this feller for 0093 Arrays.sort(list, String.CASE_INSENSITIVE_ORDER); for (String target : list) { File subfolder = new File(folder, target); coresTable.put(target, subfolder); } } // ................................................................. /** * Show the About box. */ public void handleAbout() { final Image image = Base.getLibImage("about.jpg", activeEditor); final Window window = new Window(activeEditor) { public void paint(Graphics g) { g.drawImage(image, 0, 0, null); Graphics2D g2 = (Graphics2D) g; g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF); g.setFont(new Font("SansSerif", Font.PLAIN, 11)); g.setColor(Color.white); g.drawString("Version "+ "1.0" + " Build " +Base.REVISION, 34, 30); } }; window.addMouseListener(new MouseAdapter() { public void mousePressed(MouseEvent e) { window.dispose(); } }); int w = image.getWidth(activeEditor); int h = image.getHeight(activeEditor); Dimension screen = Toolkit.getDefaultToolkit().getScreenSize(); window.setBounds((screen.width-w)/2, (screen.height-h)/2, w, h); window.setVisible(true); } /** * Show the Preferences window. */ public void handlePrefs() { if (preferencesFrame == null) preferencesFrame = new Preferences(); preferencesFrame.showFrame(activeEditor); } // ................................................................... static public Platform getPlatform() { return platform; } static public String getPlatformName() { String osname = System.getProperty("os.name"); if (osname.indexOf("Mac") != -1) { return "macosx"; } else if (osname.indexOf("Windows") != -1) { return "windows"; } else if (osname.equals("Linux")) { // true for the ibm vm return "linux"; } else { return "other"; } } /** * Map a platform constant to its name. * @param which PConstants.WINDOWS, PConstants.MACOSX, PConstants.LINUX * @return one of "windows", "macosx", or "linux" */ static public String getPlatformName(int which) { return platformNames.get(which); } static public int getPlatformIndex(String what) { Integer entry = platformIndices.get(what); return (entry == null) ? -1 : entry.intValue(); } /** * returns true if Processing is running on a Mac OS X machine. */ static public boolean isMacOS() { return System.getProperty("os.name").indexOf("Mac") != -1; } /** * returns true if running on windows. */ static public boolean isWindows() { return System.getProperty("os.name").indexOf("Windows") != -1; } /** * true if running on linux. */ static public boolean isLinux() { return System.getProperty("os.name").indexOf("Linux") != -1; } // ................................................................. static public File getSettingsFolder() { File settingsFolder = null; String preferencesPath = Preferences.get("settings.path"); if (preferencesPath != null) { settingsFolder = new File(preferencesPath); } else { try { settingsFolder = platform.getSettingsFolder(); } catch (Exception e) { showError("Problem getting data folder", "Error getting the Wiring data folder.", e); } } // create the folder if it doesn't exist already if (!settingsFolder.exists()) { if (!settingsFolder.mkdirs()) { showError("Settings issues", "Wiring cannot run because it could not\n" + "create a folder to store your settings.", null); } } return settingsFolder; } /** * Convenience method to get a File object for the specified filename inside * the settings folder. * For now, only used by Preferences to get the preferences.txt file. * @param filename A file inside the settings folder. * @return filename wrapped as a File object inside the settings folder */ static public File getSettingsFile(String filename) { return new File(getSettingsFolder(), filename); } static public File getBuildFolder() { if (buildFolder == null) { String buildPath = Preferences.get("build.path"); if (buildPath != null) { buildFolder = new File(buildPath); } else { buildFolder = createTempFolder("build"); buildFolder.deleteOnExit(); } } return buildFolder; } /** * Get the path to the platform's temporary folder, by creating * a temporary temporary file and getting its parent folder. * <br/> * Modified for revision 0094 to actually make the folder randomized * to avoid conflicts in multi-user environments. (Bug 177) */ static public File createTempFolder(String name) { try { File folder = File.createTempFile(name, null); folder.delete(); folder.mkdirs(); return folder; } catch (Exception e) { e.printStackTrace(); } return null; } static public Set<File> getLibraries() { return libraries; } static public String getExamplesPath() { return examplesFolder.getAbsolutePath(); } static public String getLibrariesPath() { return librariesFolder.getAbsolutePath(); } static public File getToolsFolder() { return toolsFolder; } static public String getToolsPath() { return toolsFolder.getAbsolutePath(); } static public File getHardwareFolder() { return getContentFile("hardware"); } static public String getHardwarePath() { return getHardwareFolder().getAbsolutePath(); } static public File getCoresFolder() { return getContentFile("cores"); } static public String getCoresPath() { return getCoresFolder().getAbsolutePath(); } static public boolean coreAvailable() { Map<String, String> boardPreferences = getBoardPreferences(); String hardware = boardPreferences.get("build.hardware"); if (hardware == null) return false; String core = boardPreferences.get("build.core"); if (core == null) return false; return true; } // there is only one core selected at a time, so only one "libraries" folder per core. // return the current core's selected libraries folder static public File getCoreLibrariesFolder() { Map<String, String> boardPreferences = getBoardPreferences(); String hardware = boardPreferences.get("build.hardware"); if (hardware == null) { showError("No board selected", "please choose a board from the Tools > Board menu.", null); return null; } String core = boardPreferences.get("build.core"); String coresPath; if (core == null) { showError("The board selected has no core specified", "check your board's definition of build.core.", null); return null; } File coresFolder = coresTable.get(core); if (coresFolder == null) return null; coresPath = coresFolder.getAbsolutePath(); if (coresPath == null) return null; return new File(coresFolder, "libraries"); } static public String getCoreLibrariesPath() { return getCoreLibrariesFolder().getAbsolutePath(); } static public String getAvrBasePath() { return getToolsPath() + File.separator + "avr" + File.separator + "bin" + File.separator; } static public Target getTarget() { return Base.targetsTable.get(Preferences.get("target")); } static public Map<String, String> getBoardPreferences() { Target target = getTarget(); if (target == null) return new LinkedHashMap<String, String>(); Map<String, Map<String,String>> boardsMap = target.getBoards(); if (boardsMap == null) return new LinkedHashMap<String, String>(); Map<String, String> map = boardsMap.get(Preferences.get("board")); if (map == null) return new LinkedHashMap<String, String>(); return map; } static public File getSketchbookFolder() { return new File(Preferences.get("sketchbook.path")); } static public File getSketchbookLibrariesFolder() { return new File(getSketchbookFolder(), "libraries"); } static public String getSketchbookLibrariesPath() { return getSketchbookLibrariesFolder().getAbsolutePath(); } static public File getSketchbookHardwareFolder() { return new File(getSketchbookFolder(), "hardware"); } static public File getSketchbookCoresFolder() { return new File(getSketchbookFolder(), "cores"); } protected File getDefaultSketchbookFolder() { File sketchbookFolder = null; try { sketchbookFolder = platform.getDefaultSketchbookFolder(); } catch (Exception e) { } if (sketchbookFolder == null) { sketchbookFolder = promptSketchbookLocation(); } // create the folder if it doesn't exist already boolean result = true; if (!sketchbookFolder.exists()) { result = sketchbookFolder.mkdirs(); } if (!result) { showError("You forgot your sketchbook", "Wiring cannot run because it could not\n" + "create a folder to store your sketchbook.", null); } return sketchbookFolder; } /** * Check for a new sketchbook location. */ static protected File promptSketchbookLocation() { File folder = null; folder = new File(System.getProperty("user.home"), "sketchbook"); if (!folder.exists()) { folder.mkdirs(); return folder; } String prompt = "Select (or create new) folder for sketches..."; folder = Base.selectFolder(prompt, null, null); if (folder == null) { System.exit(0); } return folder; } // ................................................................. /** * Implements the cross-platform headache of opening URLs * TODO This code should be replaced by PApplet.link(), * however that's not a static method (because it requires * an AppletContext when used as an applet), so it's mildly * trickier than just removing this method. */ static public void openURL(String url) { try { platform.openURL(url); } catch (Exception e) { showWarning("Problem Opening URL", "Could not open the URL\n" + url, e); } } /** * Used to determine whether to disable the "Show Sketch Folder" option. * @return true If a means of opening a folder is known to be available. */ static protected boolean openFolderAvailable() { return platform.openFolderAvailable(); } /** * Implements the other cross-platform headache of opening * a folder in the machine's native file browser. */ static public void openFolder(File file) { try { platform.openFolder(file); } catch (Exception e) { showWarning("Problem Opening Folder", "Could not open the folder\n" + file.getAbsolutePath(), e); } } // ................................................................. /** * Prompt for a fodler and return it as a File object (or null). * Implementation for choosing directories that handles both the * Mac OS X hack to allow the native AWT file dialog, or uses * the JFileChooser on other platforms. Mac AWT trick obtained from * <A HREF="http://lists.apple.com/archives/java-dev/2003/Jul/msg00243.html">this post</A> * on the OS X Java dev archive which explains the cryptic note in * Apple's Java 1.4 release docs about the special System property. */ static public File selectFolder(String prompt, File folder, Frame frame) { if (Base.isMacOS()) { if (frame == null) frame = new Frame(); FileDialog fd = new FileDialog(frame, prompt, FileDialog.LOAD); if (folder != null) { fd.setDirectory(folder.getParent()); } System.setProperty("apple.awt.fileDialogForDirectories", "true"); fd.setVisible(true); System.setProperty("apple.awt.fileDialogForDirectories", "false"); if (fd.getFile() == null) { return null; } return new File(fd.getDirectory(), fd.getFile()); } else { JFileChooser fc = new JFileChooser(); fc.setDialogTitle(prompt); if (folder != null) { fc.setSelectedFile(folder); } fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); int returned = fc.showOpenDialog(new JDialog()); if (returned == JFileChooser.APPROVE_OPTION) { return fc.getSelectedFile(); } } return null; } // ................................................................. /** * Give this Frame a Processing icon. */ static public void setIcon(Frame frame) { Image image = Toolkit.getDefaultToolkit().createImage(PApplet.ICON_IMAGE); frame.setIconImage(image); } /** * Registers key events for a Ctrl-W and ESC with an ActionListener * that will take care of disposing the window. */ static public void registerWindowCloseKeys(JRootPane root, ActionListener disposer) { KeyStroke stroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0); root.registerKeyboardAction(disposer, stroke, JComponent.WHEN_IN_FOCUSED_WINDOW); int modifiers = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(); stroke = KeyStroke.getKeyStroke('W', modifiers); root.registerKeyboardAction(disposer, stroke, JComponent.WHEN_IN_FOCUSED_WINDOW); } // ................................................................. static public void showReference(String filename) { String language = Preferences.get("reference.language"); File referenceFolder; if (language.equals("en")) { referenceFolder = Base.getContentFile("reference"); } else { referenceFolder = Base.getContentFile("reference" + File.separator + language); } File referenceFile = new File(referenceFolder, filename); openURL(referenceFile.getAbsolutePath()); } static public void showReference() { showReference("index.html"); } static public void showExamples() { showReference("learning" + File.separator + "index.html"); } static public void showHardware() { showReference("hardware" + File.separator + "index.html"); } static public void showEnvironment() { showReference("environment" + File.separator + "index.html"); } static public void showFAQ() { openURL("http://wiki.wiring.co/index.php?title=FAQ"); } static public void showGettingStarted() { openURL("http://wiring.org.co/learning/tutorials/"); } static public void showPlatforms() { showReference("environment" + File.separator + "platforms.html"); } static public void showTroubleshooting() { openURL("http://forum.wiring.co"); } // ................................................................. /** * "No cookie for you" type messages. Nothing fatal or all that * much of a bummer, but something to notify the user about. */ static public void showMessage(String title, String message) { if (title == null) title = "Message"; if (commandLine) { System.out.println(title + ": " + message); } else { JOptionPane.showMessageDialog(new Frame(), message, title, JOptionPane.INFORMATION_MESSAGE); } } /** * Non-fatal error message with optional stack trace side dish. */ static public void showWarning(String title, String message, Exception e) { if (title == null) title = "Warning"; if (commandLine) { System.out.println(title + ": " + message); } else { JOptionPane.showMessageDialog(new Frame(), message, title, JOptionPane.WARNING_MESSAGE); } if (e != null) e.printStackTrace(); } /** * Show an error message that's actually fatal to the program. * This is an error that can't be recovered. Use showWarning() * for errors that allow P5 to continue running. */ static public void showError(String title, String message, Throwable e) { if (title == null) title = "Error"; if (commandLine) { System.err.println(title + ": " + message); } else { JOptionPane.showMessageDialog(new Frame(), message, title, JOptionPane.ERROR_MESSAGE); } if (e != null) e.printStackTrace(); System.exit(1); } // ................................................................... // incomplete static public int showYesNoCancelQuestion(Editor editor, String title, String primary, String secondary) { if (!Base.isMacOS()) { int result = JOptionPane.showConfirmDialog(null, primary + "\n" + secondary, title, JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE); return result; } else { // 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(editor, null); dialog.setVisible(true); Object result = pane.getValue(); if (result == options[0]) { return JOptionPane.YES_OPTION; } else if (result == options[1]) { return JOptionPane.CANCEL_OPTION; } else if (result == options[2]) { return JOptionPane.NO_OPTION; } else { return JOptionPane.CLOSED_OPTION; } } } static public int showYesNoQuestion(Frame editor, String title, String primary, String secondary) { if (!Base.isMacOS()) { return JOptionPane.showConfirmDialog(editor, "<html><body>" + "<b>" + primary + "</b>" + "<br>" + secondary, title, JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE); } else { // 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>" + primary + "</b>" + "<p>" + secondary + "</p>", JOptionPane.QUESTION_MESSAGE); String[] options = new String[] { "Yes", "No" }; pane.setOptions(options); // highlight the safest option ala apple hig pane.setInitialValue(options[0]); JDialog dialog = pane.createDialog(editor, null); dialog.setVisible(true); Object result = pane.getValue(); if (result == options[0]) { return JOptionPane.YES_OPTION; } else if (result == options[1]) { return JOptionPane.NO_OPTION; } else { return JOptionPane.CLOSED_OPTION; } } } static public String urlDecode(String str) { try { return URLDecoder.decode(str, "UTF-8"); } catch (UnsupportedEncodingException e) { // safe per the JDK source return null; } } /** * Adjacent the executable on Windows and Linux, * or inside Contents/Resources/Java on Mac OS X. */ static protected File wiringRoot; static public File getContentFile(String name) { if (wiringRoot == null) { // Get the path to the .jar file that contains Base.class String path = Base.class.getProtectionDomain().getCodeSource().getLocation().getPath(); // Path may have URL encoding, so remove it String decodedPath = urlDecode(path); if (decodedPath.contains("/app/bin")) { if (Base.isMacOS()) { wiringRoot = new File(path, "../../build/macosx/work/Processing.app/Contents/Java"); } else if (Base.isWindows()) { wiringRoot = new File(path, "../../build/windows/work"); } else if (Base.isLinux()) { wiringRoot = new File(path, "../../build/linux/work"); } } else { // The .jar file will be in the lib folder File jarFolder = new File(decodedPath).getParentFile(); if (jarFolder.getName().equals("lib")) { // The main Processing installation directory. // This works for Windows, Linux, and Apple's Java 6 on OS X. wiringRoot = jarFolder.getParentFile(); } else if (Base.isMacOS()) { // This works for Java 7 on OS X. The 'lib' folder is not part of the // classpath on OS X, and adding it creates more problems than it's // worth. wiringRoot = jarFolder; } if (wiringRoot == null || !wiringRoot.exists()) { // Try working directory instead (user.dir, different from user.home) System.err.println("Could not find lib folder via " + jarFolder.getAbsolutePath() + ", switching to user.dir"); wiringRoot = new File(System.getProperty("user.dir")); } } } /* String path = System.getProperty("user.dir"); // Get a path to somewhere inside the .app folder if (Base.isMacOS()) { String javaroot = System.getProperty("javaroot"); if (javaroot != null) { path = javaroot; } } File working = new File(path); */ return new File(wiringRoot, name); } /** * Get an image associated with the current color theme. */ static public Image getThemeImage(String name, Component who) { return getLibImage("theme/" + name, who); } /** * Return an Image object from inside the Processing lib folder. */ static public Image getLibImage(String name, Component who) { Image image = null; Toolkit tk = Toolkit.getDefaultToolkit(); File imageLocation = new File(getContentFile("lib"), name); image = tk.getImage(imageLocation.getAbsolutePath()); MediaTracker tracker = new MediaTracker(who); tracker.addImage(image, 0); try { tracker.waitForAll(); } catch (InterruptedException e) { } return image; } /** * Return an InputStream for a file inside the Processing lib folder. */ static public InputStream getLibStream(String filename) throws IOException { /* Properties p = System.getProperties(); Enumeration keys = p.keys(); String res = new String(); while (keys.hasMoreElements()) { String key = (String)keys.nextElement(); String value = (String)p.get(key); res += key + ": " + value+ "\n"; } showMessage("HERE ", getContentFile("lib").getAbsolutePath() +"XXX"+ System.getProperty("os.name")+"XXX"+res); */ return new FileInputStream(new File(getContentFile("lib"), filename)); } // ................................................................... /** * Get the number of lines in a file by counting the number of newline * characters inside a String (and adding 1). */ static public int countLines(String what) { int count = 1; for (char c : what.toCharArray()) { if (c == '\n') count++; } return count; } /** * Same as PApplet.loadBytes(), however never does gzip decoding. */ static public byte[] loadBytesRaw(File file) throws IOException { int size = (int) file.length(); FileInputStream input = new FileInputStream(file); byte buffer[] = new byte[size]; int offset = 0; int bytesRead; while ((bytesRead = input.read(buffer, offset, size-offset)) != -1) { offset += bytesRead; if (bytesRead == 0) break; } input.close(); // weren't properly being closed input = null; return buffer; } /** * Read from a file with a bunch of attribute/value pairs * that are separated by = and ignore comments with #. */ static public HashMap<String,String> readSettings(File inputFile) { HashMap<String,String> outgoing = new HashMap<String,String>(); if (!inputFile.exists()) return outgoing; // return empty hash String lines[] = PApplet.loadStrings(inputFile); for (int i = 0; i < lines.length; i++) { int hash = lines[i].indexOf('#'); String line = (hash == -1) ? lines[i].trim() : lines[i].substring(0, hash).trim(); if (line.length() == 0) continue; int equals = line.indexOf('='); if (equals == -1) { System.err.println("ignoring illegal line in " + inputFile); System.err.println(" " + line); continue; } String attr = line.substring(0, equals).trim(); String valu = line.substring(equals + 1).trim(); outgoing.put(attr, valu); } return outgoing; } static public void copyFile(File sourceFile, File targetFile) throws IOException { InputStream from = new BufferedInputStream(new FileInputStream(sourceFile)); OutputStream to = new BufferedOutputStream(new FileOutputStream(targetFile)); byte[] buffer = new byte[16 * 1024]; int bytesRead; while ((bytesRead = from.read(buffer)) != -1) { to.write(buffer, 0, bytesRead); } to.flush(); from.close(); // ?? from = null; to.close(); // ?? to = null; targetFile.setLastModified(sourceFile.lastModified()); } /** * Grab the contents of a file as a string. */ static public String loadFile(File file) throws IOException { String[] contents = PApplet.loadStrings(file); if (contents == null) return null; return PApplet.join(contents, "\n"); } /** * Spew the contents of a String object out to a file. */ static public void saveFile(String str, File file) throws IOException { File temp = File.createTempFile(file.getName(), null, file.getParentFile()); PApplet.saveStrings(temp, new String[] { str }); if (file.exists()) { boolean result = file.delete(); if (!result) { throw new IOException("Could not remove old version of " + file.getAbsolutePath()); } } boolean result = temp.renameTo(file); if (!result) { throw new IOException("Could not replace " + file.getAbsolutePath()); } } /** * 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 { if (sourceDir.equals(targetDir)) { final String urDum = "source and target directories are identical"; throw new IllegalArgumentException(urDum); } targetDir.mkdirs(); String files[] = sourceDir.list(); for (int i = 0; i < files.length; i++) { // Ignore dot files (.DS_Store), dot folders (.svn) while copying if (files[i].charAt(0) == '.') continue; File source = new File(sourceDir, files[i]); File target = new File(targetDir, files[i]); if (source.isDirectory()) { 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) { if (dir.exists()) { removeDescendants(dir); if (!dir.delete()) { System.err.println("Could not delete " + 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) { if (!dir.exists()) return; String files[] = dir.list(); for (int i = 0; i < files.length; i++) { if (files[i].equals(".") || files[i].equals("..")) continue; File dead = new File(dir, files[i]); if (!dead.isDirectory()) { if (!Preferences.getBoolean("compiler.save_build_files")) { if (!dead.delete()) { // temporarily disabled System.err.println("Could not delete " + dead); } } } else { removeDir(dead); } } } /** * Calculate the size of the contents of a folder. * Used to determine whether sketches are empty or not. * Note that the function calls itself recursively. */ static public int calcFolderSize(File folder) { int size = 0; String files[] = folder.list(); // null if folder doesn't exist, happens when deleting sketch if (files == null) return -1; for (int i = 0; i < files.length; i++) { if (files[i].equals(".") || (files[i].equals("..")) || files[i].equals(".DS_Store")) continue; File fella = new File(folder, files[i]); if (fella.isDirectory()) { size += calcFolderSize(fella); } else { size += (int) fella.length(); } } 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<String> vector = new Vector<String>(); 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<String> 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); } } } }