1 /* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */
2 
3 /*
4   Part of the Processing project - http://processing.org
5 
6   Copyright (c) 2004-10 Ben Fry and Casey Reas
7   Copyright (c) 2001-04 Massachusetts Institute of Technology
8 
9   This program is free software; you can redistribute it and/or modify
10   it under the terms of the GNU General Public License version 2
11   as published by the Free Software Foundation.
12 
13   This program is distributed in the hope that it will be useful,
14   but WITHOUT ANY WARRANTY; without even the implied warranty of
15   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16   GNU General Public License for more details.
17 
18   You should have received a copy of the GNU General Public License
19   along with this program; if not, write to the Free Software Foundation,
20   Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
21 */
22 
23 package processing.app;
24 
25 import cc.arduino.Compiler;
26 import cc.arduino.Constants;
27 import cc.arduino.UpdatableBoardsLibsFakeURLsHandler;
28 import cc.arduino.UploaderUtils;
29 import cc.arduino.contributions.*;
30 import cc.arduino.contributions.libraries.*;
31 import cc.arduino.contributions.libraries.ui.LibraryManagerUI;
32 import cc.arduino.contributions.packages.ContributedPlatform;
33 import cc.arduino.contributions.packages.ContributionInstaller;
34 import cc.arduino.contributions.packages.ContributionsIndexer;
35 import cc.arduino.contributions.packages.ui.ContributionManagerUI;
36 import cc.arduino.files.DeleteFilesOnShutdown;
37 import cc.arduino.packages.DiscoveryManager;
38 import cc.arduino.view.Event;
39 import cc.arduino.view.JMenuUtils;
40 import cc.arduino.view.SplashScreenHelper;
41 
42 import org.apache.commons.compress.utils.IOUtils;
43 import org.apache.commons.lang3.StringUtils;
44 import processing.app.debug.TargetBoard;
45 import processing.app.debug.TargetPackage;
46 import processing.app.debug.TargetPlatform;
47 import processing.app.helpers.*;
48 import processing.app.helpers.filefilters.OnlyDirs;
49 import processing.app.helpers.filefilters.OnlyFilesWithExtension;
50 import processing.app.javax.swing.filechooser.FileNameExtensionFilter;
51 import processing.app.legacy.PApplet;
52 import processing.app.macosx.ThinkDifferent;
53 import processing.app.packages.LibraryList;
54 import processing.app.packages.UserLibrary;
55 import processing.app.syntax.PdeKeywords;
56 import processing.app.syntax.SketchTextAreaDefaultInputMap;
57 import processing.app.tools.MenuScroller;
58 import processing.app.tools.ZipDeflater;
59 
60 import javax.swing.*;
61 import java.awt.*;
62 import java.awt.event.*;
63 import java.io.*;
64 import java.util.*;
65 import java.util.List;
66 import java.util.Timer;
67 import java.util.logging.Handler;
68 import java.util.logging.Level;
69 import java.util.logging.Logger;
70 import java.util.stream.Collectors;
71 import java.util.stream.Stream;
72 
73 import static processing.app.I18n.tr;
74 
75 
76 /**
77  * The base class for the main processing application.
78  * Primary role of this class is for platform identification and
79  * general interaction with the system (launching URLs, loading
80  * files and images, etc) that comes from that.
81  */
82 public class Base {
83 
84   private static final int RECENT_SKETCHES_MAX_SIZE = 10;
85 
86   private static boolean commandLine;
87   public static volatile Base INSTANCE;
88 
89   public static Map<String, Object> FIND_DIALOG_STATE = new HashMap<>();
90   private final ContributionInstaller contributionInstaller;
91   private final LibraryInstaller libraryInstaller;
92   private ContributionsSelfCheck contributionsSelfCheck;
93 
94   // set to true after the first time the menu is built.
95   // so that the errors while building don't show up again.
96   boolean builtOnce;
97 
98   // classpath for all known libraries for p5
99   // (both those in the p5/libs folder and those with lib subfolders
100   // found in the sketchbook)
101   static public String librariesClassPath;
102 
103   // Location for untitled items
104   static File untitledFolder;
105 
106   // p5 icon for the window
107 //  static Image icon;
108 
109   //  int editorCount;
110   List<Editor> editors = Collections.synchronizedList(new ArrayList<Editor>());
111   Editor activeEditor;
112 
113   // these menus are shared so that the board and serial port selections
114   // are the same for all windows (since the board and serial port that are
115   // actually used are determined by the preferences, which are shared)
116   private List<JMenu> boardsCustomMenus;
117   private List<JMenuItem> programmerMenus;
118 
119   private PdeKeywords pdeKeywords;
120   private final List<JMenuItem> recentSketchesMenuItems = new LinkedList<>();
121 
main(String args[])122   static public void main(String args[]) throws Exception {
123     if (!OSUtils.isWindows()) {
124       // Those properties helps enabling anti-aliasing on Linux
125       // (but not on Windows where they made things worse actually
126       // and the font rendering becomes ugly).
127 
128       // Those properties must be set before initializing any
129       // graphic object, otherwise they don't have any effect.
130       System.setProperty("awt.useSystemAAFontSettings", "on");
131       System.setProperty("swing.aatext", "true");
132     }
133     System.setProperty("java.net.useSystemProxies", "true");
134 
135     if (OSUtils.isMacOS()) {
136       ThinkDifferent.init();
137     }
138 
139     try {
140       INSTANCE = new Base(args);
141     } catch (Throwable e) {
142       e.printStackTrace(System.err);
143       System.exit(255);
144     }
145   }
146 
initLogger()147   static public void initLogger() {
148     Handler consoleHandler = new ConsoleLogger();
149     consoleHandler.setLevel(Level.ALL);
150     consoleHandler.setFormatter(new LogFormatter("%1$tl:%1$tM:%1$tS [%4$7s] %2$s: %5$s%n"));
151 
152     Logger globalLogger = Logger.getLogger(Logger.GLOBAL_LOGGER_NAME);
153     globalLogger.setLevel(consoleHandler.getLevel());
154 
155     // Remove default
156     Handler[] handlers = globalLogger.getHandlers();
157     for(Handler handler : handlers) {
158         globalLogger.removeHandler(handler);
159     }
160     Logger root = Logger.getLogger("");
161     handlers = root.getHandlers();
162     for(Handler handler : handlers) {
163       root.removeHandler(handler);
164     }
165 
166     globalLogger.addHandler(consoleHandler);
167 
168     Logger.getLogger("cc.arduino.packages.autocomplete").setParent(globalLogger);
169     Logger.getLogger("br.com.criativasoft.cpluslibparser").setParent(globalLogger);
170     Logger.getLogger(Base.class.getPackage().getName()).setParent(globalLogger);
171 
172   }
173 
isCommandLine()174   static protected boolean isCommandLine() {
175     return commandLine;
176   }
177 
178   // Returns a File object for the given pathname. If the pathname
179   // is not absolute, it is interpreted relative to the current
180   // directory when starting the IDE (which is not the same as the
181   // current working directory!).
absoluteFile(String path)182   static public File absoluteFile(String path) {
183     return BaseNoGui.absoluteFile(path);
184   }
185 
Base(String[] args)186   public Base(String[] args) throws Exception {
187     Thread deleteFilesOnShutdownThread = new Thread(DeleteFilesOnShutdown.INSTANCE);
188     deleteFilesOnShutdownThread.setName("DeleteFilesOnShutdown");
189     Runtime.getRuntime().addShutdownHook(deleteFilesOnShutdownThread);
190 
191     BaseNoGui.initLogger();
192 
193     initLogger();
194 
195     BaseNoGui.initPlatform();
196 
197     BaseNoGui.getPlatform().init();
198 
199     BaseNoGui.initPortableFolder();
200 
201     // Look for a possible "--preferences-file" parameter and load preferences
202     BaseNoGui.initParameters(args);
203 
204     CommandlineParser parser = new CommandlineParser(args);
205     parser.parseArgumentsPhase1();
206     commandLine = !parser.isGuiMode();
207 
208     SplashScreenHelper splash;
209     if (parser.isGuiMode()) {
210       // Setup all notification widgets
211       splash = new SplashScreenHelper(SplashScreen.getSplashScreen());
212       BaseNoGui.notifier = new GUIUserNotifier(this);
213 
214       // Setup the theme coloring fun
215       Theme.init();
216       System.setProperty("swing.aatext", PreferencesData.get("editor.antialias", "true"));
217 
218       // Set the look and feel before opening the window
219       try {
220         BaseNoGui.getPlatform().setLookAndFeel();
221       } catch (Exception e) {
222         // ignore
223       }
224 
225       // Use native popups so they don't look so crappy on osx
226       JPopupMenu.setDefaultLightWeightPopupEnabled(false);
227     } else {
228       splash = new SplashScreenHelper(null);
229     }
230 
231     splash.splashText(tr("Loading configuration..."));
232 
233     BaseNoGui.initVersion();
234 
235     // Don't put anything above this line that might make GUI,
236     // because the platform has to be inited properly first.
237 
238     // Create a location for untitled sketches
239     untitledFolder = FileUtils.createTempFolder("untitled" + new Random().nextInt(Integer.MAX_VALUE), ".tmp");
240     DeleteFilesOnShutdown.add(untitledFolder);
241 
242     BaseNoGui.checkInstallationFolder();
243 
244     // If no path is set, get the default sketchbook folder for this platform
245     if (BaseNoGui.getSketchbookPath() == null) {
246       File defaultFolder = getDefaultSketchbookFolderOrPromptForIt();
247       if (BaseNoGui.getPortableFolder() != null)
248         PreferencesData.set("sketchbook.path", BaseNoGui.getPortableSketchbookFolder());
249       else
250         PreferencesData.set("sketchbook.path", defaultFolder.getAbsolutePath());
251       if (!defaultFolder.exists()) {
252         defaultFolder.mkdirs();
253       }
254     }
255 
256     splash.splashText(tr("Initializing packages..."));
257     BaseNoGui.initPackages();
258 
259     splash.splashText(tr("Preparing boards..."));
260 
261     if (!isCommandLine()) {
262       rebuildBoardsMenu();
263       rebuildProgrammerMenu();
264     }
265 
266     // Setup board-dependent variables.
267     onBoardOrPortChange();
268 
269     pdeKeywords = new PdeKeywords();
270     pdeKeywords.reload();
271 
272     contributionInstaller = new ContributionInstaller(BaseNoGui.getPlatform(), new GPGDetachedSignatureVerifier());
273     libraryInstaller = new LibraryInstaller(BaseNoGui.getPlatform());
274 
275     parser.parseArgumentsPhase2();
276 
277     // Save the preferences. For GUI mode, this happens in the quit
278     // handler, but for other modes we should also make sure to save
279     // them.
280     PreferencesData.save();
281 
282     if (parser.isInstallBoard()) {
283       ContributionsIndexer indexer = new ContributionsIndexer(
284           BaseNoGui.getSettingsFolder(), BaseNoGui.getHardwareFolder(),
285           BaseNoGui.getPlatform(), new GPGDetachedSignatureVerifier());
286       ProgressListener progressListener = new ConsoleProgressListener();
287 
288       List<String> downloadedPackageIndexFiles = contributionInstaller.updateIndex(progressListener);
289       contributionInstaller.deleteUnknownFiles(downloadedPackageIndexFiles);
290       indexer.parseIndex();
291       indexer.syncWithFilesystem();
292 
293       String[] boardToInstallParts = parser.getBoardToInstall().split(":");
294 
295       ContributedPlatform selected = null;
296       if (boardToInstallParts.length == 3) {
297         selected = indexer.getIndex().findPlatform(boardToInstallParts[0], boardToInstallParts[1], VersionHelper.valueOf(boardToInstallParts[2]).toString());
298       } else if (boardToInstallParts.length == 2) {
299         List<ContributedPlatform> platformsByName = indexer.getIndex().findPlatforms(boardToInstallParts[0], boardToInstallParts[1]);
300         Collections.sort(platformsByName, new DownloadableContributionVersionComparator());
301         if (!platformsByName.isEmpty()) {
302           selected = platformsByName.get(platformsByName.size() - 1);
303         }
304       }
305       if (selected == null) {
306         System.out.println(tr("Selected board is not available"));
307         System.exit(1);
308       }
309 
310       ContributedPlatform installed = indexer.getInstalled(boardToInstallParts[0], boardToInstallParts[1]);
311 
312       if (!selected.isReadOnly()) {
313         contributionInstaller.install(selected, progressListener);
314       }
315 
316       if (installed != null && !installed.isReadOnly()) {
317         contributionInstaller.remove(installed);
318       }
319 
320       System.exit(0);
321 
322     } else if (parser.isInstallLibrary()) {
323       BaseNoGui.onBoardOrPortChange();
324 
325       ProgressListener progressListener = new ConsoleProgressListener();
326       libraryInstaller.updateIndex(progressListener);
327 
328       LibrariesIndexer indexer = new LibrariesIndexer(BaseNoGui.getSettingsFolder());
329       indexer.parseIndex();
330       indexer.setSketchbookLibrariesFolder(BaseNoGui.getSketchbookLibrariesFolder());
331       indexer.setLibrariesFolders(BaseNoGui.getLibrariesPath());
332 
333       for (String library : parser.getLibraryToInstall().split(",")) {
334         String[] libraryToInstallParts = library.split(":");
335 
336         ContributedLibrary selected = null;
337         if (libraryToInstallParts.length == 2) {
338           selected = indexer.getIndex().find(libraryToInstallParts[0], VersionHelper.valueOf(libraryToInstallParts[1]).toString());
339         } else if (libraryToInstallParts.length == 1) {
340           List<ContributedLibrary> librariesByName = indexer.getIndex().find(libraryToInstallParts[0]);
341           Collections.sort(librariesByName, new DownloadableContributionVersionComparator());
342           if (!librariesByName.isEmpty()) {
343             selected = librariesByName.get(librariesByName.size() - 1);
344           }
345         }
346         if (selected == null) {
347           System.out.println(tr("Selected library is not available"));
348           System.exit(1);
349         }
350 
351         ContributedLibrary installed = indexer.getIndex().getInstalled(libraryToInstallParts[0]);
352         if (selected.isReadOnly()) {
353           libraryInstaller.remove(installed, progressListener);
354         } else {
355           libraryInstaller.install(selected, installed, progressListener);
356         }
357       }
358 
359       System.exit(0);
360 
361     } else if (parser.isVerifyOrUploadMode()) {
362       // Set verbosity for command line build
363       PreferencesData.setBoolean("build.verbose", parser.isDoVerboseBuild());
364       PreferencesData.setBoolean("upload.verbose", parser.isDoVerboseUpload());
365 
366       // Set preserve-temp flag
367       PreferencesData.setBoolean("runtime.preserve.temp.files", parser.isPreserveTempFiles());
368 
369       // Make sure these verbosity preferences are only for the current session
370       PreferencesData.setDoSave(false);
371 
372       Sketch sketch = null;
373       String outputFile = null;
374 
375       try {
376         // Build
377         splash.splashText(tr("Verifying..."));
378 
379         File sketchFile = BaseNoGui.absoluteFile(parser.getFilenames().get(0));
380         sketch = new Sketch(sketchFile);
381 
382         outputFile = new Compiler(sketch).build(progress -> {}, false);
383       } catch (Exception e) {
384         // Error during build
385         System.exit(1);
386       }
387 
388       if (parser.isUploadMode()) {
389         // Upload
390         splash.splashText(tr("Uploading..."));
391 
392         try {
393           List<String> warnings = new ArrayList<>();
394           UploaderUtils uploader = new UploaderUtils();
395           boolean res = uploader.upload(sketch, null, outputFile,
396                                         parser.isDoUseProgrammer(),
397                                         parser.isNoUploadPort(), warnings);
398           for (String warning : warnings) {
399             System.out.println(tr("Warning") + ": " + warning);
400           }
401           if (!res) {
402             throw new Exception();
403           }
404         } catch (Exception e) {
405           // Error during upload
406           System.out.flush();
407           System.err.flush();
408           System.err
409               .println(tr("An error occurred while uploading the sketch"));
410           System.exit(1);
411         }
412       }
413 
414       // No errors exit gracefully
415       System.exit(0);
416     } else if (parser.isGuiMode()) {
417       splash.splashText(tr("Starting..."));
418 
419       for (String path : parser.getFilenames()) {
420         // Correctly resolve relative paths
421         File file = absoluteFile(path);
422 
423         // Fix a problem with systems that use a non-ASCII languages. Paths are
424         // being passed in with 8.3 syntax, which makes the sketch loader code
425         // unhappy, since the sketch folder naming doesn't match up correctly.
426         // http://dev.processing.org/bugs/show_bug.cgi?id=1089
427         if (OSUtils.isWindows()) {
428           try {
429             file = file.getCanonicalFile();
430           } catch (IOException e) {
431             e.printStackTrace();
432           }
433         }
434 
435         if (!parser.isForceSavePrefs())
436           PreferencesData.setDoSave(true);
437         if (handleOpen(file, retrieveSketchLocation(".default"), false) == null) {
438           String mess = I18n.format(tr("Failed to open sketch: \"{0}\""), path);
439           // Open failure is fatal in upload/verify mode
440           if (parser.isVerifyOrUploadMode())
441             showError(null, mess, 2);
442           else
443             showWarning(null, mess, null);
444         }
445       }
446 
447       installKeyboardInputMap();
448 
449       // Check if there were previously opened sketches to be restored
450       restoreSketches();
451 
452       // Create a new empty window (will be replaced with any files to be opened)
453       if (editors.isEmpty()) {
454         handleNew();
455       }
456 
457       new Thread(new BuiltInCoreIsNewerCheck(this)).start();
458 
459       // Check for boards which need an additional core
460       new Thread(new NewBoardListener(this)).start();
461 
462       // Check for updates
463       if (PreferencesData.getBoolean("update.check")) {
464         new UpdateCheck(this);
465 
466         contributionsSelfCheck = new ContributionsSelfCheck(this, new UpdatableBoardsLibsFakeURLsHandler(this), contributionInstaller, libraryInstaller);
467         new Timer(false).schedule(contributionsSelfCheck, Constants.BOARDS_LIBS_UPDATABLE_CHECK_START_PERIOD);
468       }
469 
470     } else if (parser.isNoOpMode()) {
471       // Do nothing (intended for only changing preferences)
472       System.exit(0);
473     } else if (parser.isGetPrefMode()) {
474       BaseNoGui.dumpPrefs(parser);
475     }
476   }
477 
installKeyboardInputMap()478   private void installKeyboardInputMap() {
479     UIManager.put("RSyntaxTextAreaUI.inputMap", new SketchTextAreaDefaultInputMap());
480   }
481 
482   /**
483    * Post-constructor setup for the editor area. Loads the last
484    * sketch that was used (if any), and restores other Editor settings.
485    * The complement to "storePreferences", this is called when the
486    * application is first launched.
487    *
488    * @throws Exception
489    */
restoreSketches()490   protected boolean restoreSketches() throws Exception {
491     // Iterate through all sketches that were open last time p5 was running.
492     // If !windowPositionValid, then ignore the coordinates found for each.
493 
494     // Save the sketch path and window placement for each open sketch
495     int count = PreferencesData.getInteger("last.sketch.count");
496     int opened = 0;
497     for (int i = count - 1; i >= 0; i--) {
498       String path = PreferencesData.get("last.sketch" + i + ".path");
499       if (path == null) {
500         continue;
501       }
502       if (BaseNoGui.getPortableFolder() != null && !new File(path).isAbsolute()) {
503         File absolute = new File(BaseNoGui.getPortableFolder(), path);
504         try {
505           path = absolute.getCanonicalPath();
506         } catch (IOException e) {
507           // path unchanged.
508         }
509       }
510       int[] location = retrieveSketchLocation("" + i);
511       // If file did not exist, null will be returned for the Editor
512       if (handleOpen(new File(path), location, nextEditorLocation(), false, false) != null) {
513         opened++;
514       }
515     }
516     return (opened > 0);
517   }
518 
519 
520   /**
521    * Store list of sketches that are currently open.
522    * Called when the application is quitting and documents are still open.
523    */
storeSketches()524   protected void storeSketches() {
525     // Save the width and height of the screen
526     Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
527     PreferencesData.setInteger("last.screen.width", screen.width);
528     PreferencesData.setInteger("last.screen.height", screen.height);
529 
530     // If there is only one sketch opened save his position as default
531     if (editors.size() == 1) {
532       storeSketchLocation(editors.get(0), ".default");
533     }
534 
535     // Save the sketch path and window placement for each open sketch
536     String untitledPath = untitledFolder.getAbsolutePath();
537     List<Editor> reversedEditors = new LinkedList<>(editors);
538     Collections.reverse(reversedEditors);
539     int index = 0;
540     for (Editor editor : reversedEditors) {
541       Sketch sketch = editor.getSketch();
542       String path = sketch.getMainFilePath();
543       // Skip untitled sketches if they do not contains changes.
544       if (path.startsWith(untitledPath) && !sketch.isModified()) {
545         continue;
546       }
547       storeSketchLocation(editor, "" + index);
548       index++;
549     }
550     PreferencesData.setInteger("last.sketch.count", index);
551   }
552 
storeSketchLocation(Editor editor, String index)553   private void storeSketchLocation(Editor editor, String index) {
554     String path = editor.getSketch().getMainFilePath();
555     String loc = StringUtils.join(editor.getPlacement(), ',');
556     PreferencesData.set("last.sketch" + index + ".path", path);
557     PreferencesData.set("last.sketch" + index + ".location", loc);
558   }
559 
retrieveSketchLocation(String index)560   private int[] retrieveSketchLocation(String index) {
561     if (PreferencesData.get("last.screen.height") == null)
562       return defaultEditorLocation();
563 
564     // if screen size has changed, the window coordinates no longer
565     // make sense, so don't use them unless they're identical
566     Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
567     int screenW = PreferencesData.getInteger("last.screen.width");
568     int screenH = PreferencesData.getInteger("last.screen.height");
569 
570     if ((screen.width != screenW) || (screen.height != screenH))
571       return defaultEditorLocation();
572 
573     String locationStr = PreferencesData
574         .get("last.sketch" + index + ".location");
575     if (locationStr == null)
576       return defaultEditorLocation();
577     return PApplet.parseInt(PApplet.split(locationStr, ','));
578   }
579 
storeRecentSketches(SketchController sketch)580   protected void storeRecentSketches(SketchController sketch) {
581     if (sketch.isUntitled()) {
582       return;
583     }
584 
585     Set<String> sketches = new LinkedHashSet<>();
586     sketches.add(sketch.getSketch().getMainFilePath());
587     sketches.addAll(PreferencesData.getCollection("recent.sketches"));
588 
589     PreferencesData.setCollection("recent.sketches", sketches);
590   }
591 
removeRecentSketchPath(String path)592   protected void removeRecentSketchPath(String path) {
593     Collection<String> sketches = new LinkedList<>(PreferencesData.getCollection("recent.sketches"));
594     sketches.remove(path);
595     PreferencesData.setCollection("recent.sketches", sketches);
596   }
597 
598   // Because of variations in native windowing systems, no guarantees about
599   // changes to the focused and active Windows can be made. Developers must
600   // never assume that this Window is the focused or active Window until this
601   // Window receives a WINDOW_GAINED_FOCUS or WINDOW_ACTIVATED event.
handleActivated(Editor whichEditor)602   protected void handleActivated(Editor whichEditor) {
603     activeEditor = whichEditor;
604     activeEditor.rebuildRecentSketchesMenu();
605     if (PreferencesData.getBoolean("editor.external")) {
606       try {
607         // If the list of files on disk changed, recreate the tabs for them
608         if (activeEditor.getSketch().reload())
609           activeEditor.createTabs();
610         else // Let the current tab know it was activated, so it can reload
611           activeEditor.getCurrentTab().activated();
612       } catch (IOException e) {
613         System.err.println(e);
614       }
615     }
616 
617     // set the current window to be the console that's getting output
618     EditorConsole.setCurrentEditorConsole(activeEditor.console);
619   }
620 
defaultEditorLocation()621   protected int[] defaultEditorLocation() {
622     int defaultWidth = PreferencesData.getInteger("editor.window.width.default");
623     int defaultHeight = PreferencesData.getInteger("editor.window.height.default");
624     Rectangle screen = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration().getBounds();
625     return new int[]{
626             (screen.width - defaultWidth) / 2,
627             (screen.height - defaultHeight) / 2,
628             defaultWidth, defaultHeight, 0
629     };
630   }
631 
nextEditorLocation()632   protected int[] nextEditorLocation() {
633     if (activeEditor == null) {
634       // If no current active editor, use default placement
635       return defaultEditorLocation();
636     }
637 
638     Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
639 
640     // With a currently active editor, open the new window
641     // using the same dimensions, but offset slightly.
642     synchronized (editors) {
643       int[] location = activeEditor.getPlacement();
644 
645       // Just in case the bounds for that window are bad
646       final int OVER = 50;
647       location[0] += OVER;
648       location[1] += OVER;
649 
650       if (location[0] == OVER || location[2] == OVER
651           || location[0] + location[2] > screen.width
652           || location[1] + location[3] > screen.height) {
653         // Warp the next window to a randomish location on screen.
654         int[] l = defaultEditorLocation();
655         l[0] *= Math.random() * 2;
656         l[1] *= Math.random() * 2;
657         return l;
658       }
659 
660       return location;
661     }
662   }
663 
664 
665   // .................................................................
666 
667 
668   boolean breakTime = false;
669   String[] months = {
670           "jan", "feb", "mar", "apr", "may", "jun",
671           "jul", "aug", "sep", "oct", "nov", "dec"
672   };
673 
createNewUntitled()674   protected File createNewUntitled() throws IOException {
675     File newbieDir = null;
676     String newbieName = null;
677 
678     // In 0126, untitled sketches will begin in the temp folder,
679     // and then moved to a new location because Save will default to Save As.
680     File sketchbookDir = BaseNoGui.getSketchbookFolder();
681     File newbieParentDir = untitledFolder;
682 
683     // Use a generic name like sketch_031008a, the date plus a char
684     int index = 0;
685     //SimpleDateFormat formatter = new SimpleDateFormat("yyMMdd");
686     //SimpleDateFormat formatter = new SimpleDateFormat("MMMdd");
687     //String purty = formatter.format(new Date()).toLowerCase();
688     Calendar cal = Calendar.getInstance();
689     int day = cal.get(Calendar.DAY_OF_MONTH);  // 1..31
690     int month = cal.get(Calendar.MONTH);  // 0..11
691     String purty = months[month] + PApplet.nf(day, 2);
692 
693     do {
694       if (index == 26*26) {
695         // In 0166, avoid running past zz by sending people outdoors.
696         if (!breakTime) {
697           showWarning(tr("Time for a Break"),
698                   tr("You've reached the limit for auto naming of new sketches\n" +
699                           "for the day. How about going for a walk instead?"), null);
700           breakTime = true;
701         } else {
702           showWarning(tr("Sunshine"),
703                   tr("No really, time for some fresh air for you."), null);
704         }
705         return null;
706       }
707 
708       int multiples = index / 26;
709 
710       if(multiples > 0){
711         newbieName = ((char) ('a' + (multiples-1))) + "" + ((char) ('a' + (index % 26))) + "";
712       }else{
713         newbieName = ((char) ('a' + index)) + "";
714       }
715       newbieName = "sketch_" + purty + newbieName;
716       newbieDir = new File(newbieParentDir, newbieName);
717       index++;
718       // Make sure it's not in the temp folder *and* it's not in the sketchbook
719     } while (newbieDir.exists() || new File(sketchbookDir, newbieName).exists());
720 
721     // Make the directory for the new sketch
722     newbieDir.mkdirs();
723 
724     // Make an empty pde file
725     File newbieFile = new File(newbieDir, newbieName + ".ino");
726     if (!newbieFile.createNewFile()) {
727       throw new IOException();
728     }
729     FileUtils.copyFile(new File(getContentFile("examples"), "01.Basics" + File.separator + "BareMinimum" + File.separator + "BareMinimum.ino"), newbieFile);
730     return newbieFile;
731   }
732 
733 
734   /**
735    * Create a new untitled document in a new sketch window.
736    *
737    * @throws Exception
738    */
handleNew()739   public void handleNew() throws Exception {
740     try {
741       File file = createNewUntitled();
742       if (file != null) {
743         handleOpen(file, true);
744       }
745 
746     } catch (IOException e) {
747       if (activeEditor != null) {
748         activeEditor.statusError(e);
749       }
750     }
751   }
752 
753 
754   /**
755    * Prompt for a sketch to open, and open it in a new window.
756    *
757    * @throws Exception
758    */
handleOpenPrompt()759   public void handleOpenPrompt() throws Exception {
760     // get the frontmost window frame for placing file dialog
761     FileDialog fd = new FileDialog(activeEditor, tr("Open an Arduino sketch..."), FileDialog.LOAD);
762     File lastFolder = new File(PreferencesData.get("last.folder", BaseNoGui.getSketchbookFolder().getAbsolutePath()));
763     if (lastFolder.exists() && lastFolder.isFile()) {
764       lastFolder = lastFolder.getParentFile();
765     }
766     fd.setDirectory(lastFolder.getAbsolutePath());
767 
768     // Only show .pde files as eligible bachelors
769     fd.setFilenameFilter(new FilenameFilter() {
770       public boolean accept(File dir, String name) {
771         return name.toLowerCase().endsWith(".ino")
772                 || name.toLowerCase().endsWith(".pde");
773       }
774     });
775 
776     fd.setVisible(true);
777 
778     String directory = fd.getDirectory();
779     String filename = fd.getFile();
780 
781     // User canceled selection
782     if (filename == null) return;
783 
784     File inputFile = new File(directory, filename);
785 
786     PreferencesData.set("last.folder", inputFile.getAbsolutePath());
787     handleOpen(inputFile);
788   }
789 
790 
791   /**
792    * Open a sketch in a new window.
793    *
794    * @param file File to open
795    * @return the Editor object, so that properties (like 'untitled')
796    * can be set by the caller
797    * @throws Exception
798    */
handleOpen(File file)799   public Editor handleOpen(File file) throws Exception {
800     return handleOpen(file, false);
801   }
802 
handleOpen(File file, boolean untitled)803   public Editor handleOpen(File file, boolean untitled) throws Exception {
804     return handleOpen(file, nextEditorLocation(), untitled);
805   }
806 
handleOpen(File file, int[] location, boolean untitled)807   protected Editor handleOpen(File file, int[] location, boolean untitled) throws Exception {
808     return handleOpen(file, location, location, true, untitled);
809   }
810 
handleOpen(File file, int[] storedLocation, int[] defaultLocation, boolean storeOpenedSketches, boolean untitled)811   protected Editor handleOpen(File file, int[] storedLocation, int[] defaultLocation, boolean storeOpenedSketches, boolean untitled) throws Exception {
812     if (!file.exists()) return null;
813 
814     // Cycle through open windows to make sure that it's not already open.
815     for (Editor editor : editors) {
816       if (editor.getSketch().getPrimaryFile().getFile().equals(file)) {
817         editor.toFront();
818         return editor;
819       }
820     }
821 
822     Editor editor = new Editor(this, file, storedLocation, defaultLocation, BaseNoGui.getPlatform());
823 
824     // Make sure that the sketch actually loaded
825     if (editor.getSketchController() == null) {
826       return null;  // Just walk away quietly
827     }
828 
829     editor.untitled = untitled;
830 
831     editors.add(editor);
832 
833     if (storeOpenedSketches) {
834       // Store information on who's open and running
835       // (in case there's a crash or something that can't be recovered)
836       storeSketches();
837       storeRecentSketches(editor.getSketchController());
838       rebuildRecentSketchesMenuItems();
839       PreferencesData.save();
840     }
841 
842     // now that we're ready, show the window
843     // (don't do earlier, cuz we might move it based on a window being closed)
844     SwingUtilities.invokeLater(() -> editor.setVisible(true));
845 
846     return editor;
847   }
848 
rebuildRecentSketchesMenuItems()849   protected void rebuildRecentSketchesMenuItems() {
850     Set<File> recentSketches = new LinkedHashSet<File>() {
851 
852       @Override
853       public boolean add(File file) {
854         if (size() >= RECENT_SKETCHES_MAX_SIZE) {
855           return false;
856         }
857         return super.add(file);
858       }
859     };
860 
861     for (String path : PreferencesData.getCollection("recent.sketches")) {
862       File file = new File(path);
863       if (file.exists()) {
864         recentSketches.add(file);
865       }
866     }
867 
868     recentSketchesMenuItems.clear();
869     for (final File recentSketch : recentSketches) {
870       JMenuItem recentSketchMenuItem = new JMenuItem(recentSketch.getParentFile().getName());
871       recentSketchMenuItem.addActionListener(new ActionListener() {
872         @Override
873         public void actionPerformed(ActionEvent actionEvent) {
874           try {
875             handleOpen(recentSketch);
876           } catch (Exception e) {
877             e.printStackTrace();
878           }
879         }
880       });
881       recentSketchesMenuItems.add(recentSketchMenuItem);
882     }
883   }
884 
885 
886   /**
887    * Close a sketch as specified by its editor window.
888    *
889    * @param editor Editor object of the sketch to be closed.
890    * @return true if succeeded in closing, false if canceled.
891    */
handleClose(Editor editor)892   public boolean handleClose(Editor editor) {
893     // Check if modified
894 //    boolean immediate = editors.size() == 1;
895     if (!editor.checkModified()) {
896       return false;
897     }
898 
899     if (editors.size() == 1) {
900       storeSketches();
901 
902       // This will store the sketch count as zero
903       editors.remove(editor);
904       try {
905         Editor.serialMonitor.close();
906       } catch (Exception e) {
907         //ignore
908       }
909       rebuildRecentSketchesMenuItems();
910 
911       // Save out the current prefs state
912       PreferencesData.save();
913 
914       // Since this wasn't an actual Quit event, call System.exit()
915       System.exit(0);
916 
917     } else {
918       // More than one editor window open,
919       // proceed with closing the current window.
920       editor.setVisible(false);
921       editor.dispose();
922 //      for (int i = 0; i < editorCount; i++) {
923 //        if (editor == editors[i]) {
924 //          for (int j = i; j < editorCount-1; j++) {
925 //            editors[j] = editors[j+1];
926 //          }
927 //          editorCount--;
928 //          // Set to null so that garbage collection occurs
929 //          editors[editorCount] = null;
930 //        }
931 //      }
932       editors.remove(editor);
933     }
934     return true;
935   }
936 
937 
938   /**
939    * Handler for File &rarr; Quit.
940    *
941    * @return false if canceled, true otherwise.
942    */
handleQuit()943   public boolean handleQuit() {
944     // If quit is canceled, this will be replaced anyway
945     // by a later handleQuit() that is not canceled.
946     storeSketches();
947     try {
948       Editor.serialMonitor.close();
949     } catch (Exception e) {
950       // ignore
951     }
952 
953     if (handleQuitEach()) {
954       // Save out the current prefs state
955       PreferencesData.save();
956 
957       if (!OSUtils.isMacOS()) {
958         // If this was fired from the menu or an AppleEvent (the Finder),
959         // then Mac OS X will send the terminate signal itself.
960         System.exit(0);
961       }
962       return true;
963     }
964     return false;
965   }
966 
967 
968   /**
969    * Attempt to close each open sketch in preparation for quitting.
970    *
971    * @return false if canceled along the way
972    */
handleQuitEach()973   protected boolean handleQuitEach() {
974     for (Editor editor : editors) {
975       if (!editor.checkModified()) {
976         return false;
977       }
978     }
979     return true;
980   }
981 
982 
983   // .................................................................
984 
985 
986   /**
987    * Asynchronous version of menu rebuild to be used on save and rename
988    * to prevent the interface from locking up until the menus are done.
989    */
rebuildSketchbookMenus()990   public void rebuildSketchbookMenus() {
991     //System.out.println("async enter");
992     //new Exception().printStackTrace();
993     SwingUtilities.invokeLater(new Runnable() {
994       public void run() {
995         //System.out.println("starting rebuild");
996         rebuildSketchbookMenu(Editor.sketchbookMenu);
997         rebuildToolbarMenu(Editor.toolbarMenu);
998         //System.out.println("done with rebuild");
999       }
1000     });
1001     //System.out.println("async exit");
1002   }
1003 
1004 
rebuildToolbarMenu(JMenu menu)1005   protected void rebuildToolbarMenu(JMenu menu) {
1006     JMenuItem item;
1007     menu.removeAll();
1008 
1009     // Add the single "Open" item
1010     item = Editor.newJMenuItem(tr("Open..."), 'O');
1011     item.addActionListener(new ActionListener() {
1012       public void actionPerformed(ActionEvent e) {
1013         try {
1014           handleOpenPrompt();
1015         } catch (Exception e1) {
1016           e1.printStackTrace();
1017         }
1018       }
1019     });
1020     menu.add(item);
1021     menu.addSeparator();
1022 
1023     // Add a list of all sketches and subfolders
1024     boolean sketches = addSketches(menu, BaseNoGui.getSketchbookFolder());
1025     if (sketches) menu.addSeparator();
1026 
1027     // Add each of the subfolders of examples directly to the menu
1028     boolean found = addSketches(menu, BaseNoGui.getExamplesFolder());
1029     if (found) menu.addSeparator();
1030   }
1031 
1032 
rebuildSketchbookMenu(JMenu menu)1033   protected void rebuildSketchbookMenu(JMenu menu) {
1034     menu.removeAll();
1035     addSketches(menu, BaseNoGui.getSketchbookFolder());
1036 
1037     JMenu librariesMenu = JMenuUtils.findSubMenuWithLabel(menu, "libraries");
1038     if (librariesMenu != null) {
1039       menu.remove(librariesMenu);
1040     }
1041     JMenu hardwareMenu = JMenuUtils.findSubMenuWithLabel(menu, "hardware");
1042     if (hardwareMenu != null) {
1043       menu.remove(hardwareMenu);
1044     }
1045   }
1046 
getSortedLibraries()1047   private List<ContributedLibrary> getSortedLibraries() {
1048     List<ContributedLibrary> installedLibraries = new LinkedList<>(BaseNoGui.librariesIndexer.getInstalledLibraries());
1049     Collections.sort(installedLibraries, new LibraryByTypeComparator());
1050     Collections.sort(installedLibraries, new LibraryOfSameTypeComparator());
1051     return installedLibraries;
1052   }
1053 
rebuildImportMenu(JMenu importMenu)1054   public void rebuildImportMenu(JMenu importMenu) {
1055     if (importMenu == null)
1056       return;
1057     importMenu.removeAll();
1058 
1059     JMenuItem menu = new JMenuItem(tr("Manage Libraries..."));
1060     menu.addActionListener(e -> openLibraryManager("", ""));
1061     importMenu.add(menu);
1062     importMenu.addSeparator();
1063 
1064     JMenuItem addLibraryMenuItem = new JMenuItem(tr("Add .ZIP Library..."));
1065     addLibraryMenuItem.addActionListener(new ActionListener() {
1066       public void actionPerformed(ActionEvent e) {
1067         Base.this.handleAddLibrary();
1068         Base.this.onBoardOrPortChange();
1069         Base.this.rebuildImportMenu(Editor.importMenu);
1070         Base.this.rebuildExamplesMenu(Editor.examplesMenu);
1071       }
1072     });
1073     importMenu.add(addLibraryMenuItem);
1074     importMenu.addSeparator();
1075 
1076     // Split between user supplied libraries and IDE libraries
1077     TargetPlatform targetPlatform = BaseNoGui.getTargetPlatform();
1078 
1079     if (targetPlatform != null) {
1080       List<ContributedLibrary> libs = getSortedLibraries();
1081       String lastLibType = null;
1082       for (ContributedLibrary lib : libs) {
1083         if (lastLibType == null || !lastLibType.equals(lib.getTypes().get(0))) {
1084           if (lastLibType != null) {
1085             importMenu.addSeparator();
1086           }
1087           lastLibType = lib.getTypes().get(0);
1088           JMenuItem platformItem = new JMenuItem(I18n.format(tr("{0} libraries"), lastLibType));
1089           platformItem.setEnabled(false);
1090           importMenu.add(platformItem);
1091         }
1092 
1093         AbstractAction action = new AbstractAction(lib.getName()) {
1094           public void actionPerformed(ActionEvent event) {
1095             UserLibrary l = (UserLibrary) getValue("library");
1096             try {
1097               activeEditor.getSketchController().importLibrary(l);
1098             } catch (IOException e) {
1099               showWarning(tr("Error"), I18n.format("Unable to list header files in {0}", l.getSrcFolder()), e);
1100             }
1101           }
1102         };
1103         action.putValue("library", lib);
1104 
1105         // Add new element at the bottom
1106         JMenuItem item = new JMenuItem(action);
1107         item.putClientProperty("library", lib);
1108         importMenu.add(item);
1109       }
1110     }
1111   }
1112 
rebuildExamplesMenu(JMenu menu)1113   public void rebuildExamplesMenu(JMenu menu) {
1114     if (menu == null) {
1115       return;
1116     }
1117 
1118     menu.removeAll();
1119 
1120     // Add examples from distribution "example" folder
1121     JMenuItem label = new JMenuItem(tr("Built-in Examples"));
1122     label.setEnabled(false);
1123     menu.add(label);
1124     boolean found = addSketches(menu, BaseNoGui.getExamplesFolder());
1125     if (found) {
1126       menu.addSeparator();
1127     }
1128 
1129     // Libraries can come from 4 locations: collect info about all four
1130     File ideLibraryPath = BaseNoGui.getContentFile("libraries");
1131     File sketchbookLibraryPath = BaseNoGui.getSketchbookLibrariesFolder();
1132     File platformLibraryPath = null;
1133     File referencedPlatformLibraryPath = null;
1134     String boardId = null;
1135     String referencedPlatformName = null;
1136     String myArch = null;
1137     TargetPlatform targetPlatform = BaseNoGui.getTargetPlatform();
1138     if (targetPlatform != null) {
1139       myArch = targetPlatform.getId();
1140       boardId = BaseNoGui.getTargetBoard().getName();
1141       platformLibraryPath = new File(targetPlatform.getFolder(), "libraries");
1142       String core = BaseNoGui.getBoardPreferences().get("build.core", "arduino");
1143       if (core.contains(":")) {
1144         String refcore = core.split(":")[0];
1145         TargetPlatform referencedPlatform = BaseNoGui.getTargetPlatform(refcore, myArch);
1146         if (referencedPlatform != null) {
1147           referencedPlatformName = referencedPlatform.getPreferences().get("name");
1148           referencedPlatformLibraryPath = new File(referencedPlatform.getFolder(), "libraries");
1149         }
1150       }
1151     }
1152 
1153     // Divide the libraries into 7 lists, corresponding to the 4 locations
1154     // with the retired IDE libs further divided into their own list, and
1155     // any incompatible sketchbook libs further divided into their own list.
1156     // The 7th list of "other" libraries should always be empty, but serves
1157     // as a safety feature to prevent any library from vanishing.
1158     LibraryList allLibraries = new LibraryList(BaseNoGui.librariesIndexer.getInstalledLibraries());
1159     LibraryList ideLibs = new LibraryList();
1160     LibraryList retiredIdeLibs = new LibraryList();
1161     LibraryList platformLibs = new LibraryList();
1162     LibraryList referencedPlatformLibs = new LibraryList();
1163     LibraryList sketchbookLibs = new LibraryList();
1164     LibraryList sketchbookIncompatibleLibs = new LibraryList();
1165     LibraryList otherLibs = new LibraryList();
1166     for (UserLibrary lib : allLibraries) {
1167       // Get the library's location - used for sorting into categories
1168       File libraryLocation = lib.getInstalledFolder().getParentFile();
1169       // Is this library compatible?
1170       List<String> arch = lib.getArchitectures();
1171       boolean compatible;
1172       if (myArch == null || arch == null || arch.contains("*")) {
1173         compatible = true;
1174       } else {
1175         compatible = arch.contains(myArch);
1176       }
1177       // IDE Libaries (including retired)
1178       if (libraryLocation.equals(ideLibraryPath)) {
1179         if (compatible) {
1180           // only compatible IDE libs are shown
1181           boolean retired = false;
1182           List<String> types = lib.getTypes();
1183           if (types != null) retired = types.contains("Retired");
1184           if (retired) {
1185             retiredIdeLibs.add(lib);
1186           } else {
1187             ideLibs.add(lib);
1188           }
1189         }
1190       // Platform Libraries
1191       } else if (libraryLocation.equals(platformLibraryPath)) {
1192         // all platform libs are assumed to be compatible
1193         platformLibs.add(lib);
1194       // Referenced Platform Libraries
1195       } else if (libraryLocation.equals(referencedPlatformLibraryPath)) {
1196         // all referenced platform libs are assumed to be compatible
1197         referencedPlatformLibs.add(lib);
1198       // Sketchbook Libraries (including incompatible)
1199       } else if (libraryLocation.equals(sketchbookLibraryPath)) {
1200         if (compatible) {
1201           // libraries promoted from sketchbook (behave as builtin)
1202           if (lib.getTypes() != null && lib.getTypes().contains("Arduino")
1203               && lib.getArchitectures().contains("*")) {
1204             ideLibs.add(lib);
1205           } else {
1206             sketchbookLibs.add(lib);
1207           }
1208         } else {
1209           sketchbookIncompatibleLibs.add(lib);
1210         }
1211       // Other libraries of unknown type (should never occur)
1212       } else {
1213         otherLibs.add(lib);
1214       }
1215     }
1216 
1217     // Add examples from libraries
1218     if (!ideLibs.isEmpty()) {
1219       ideLibs.sort();
1220       label = new JMenuItem(tr("Examples for any board"));
1221       label.setEnabled(false);
1222       menu.add(label);
1223     }
1224     for (UserLibrary lib : ideLibs) {
1225       addSketchesSubmenu(menu, lib);
1226     }
1227 
1228     if (!retiredIdeLibs.isEmpty()) {
1229       retiredIdeLibs.sort();
1230       JMenu retired = new JMenu(tr("RETIRED"));
1231       menu.add(retired);
1232       for (UserLibrary lib : retiredIdeLibs) {
1233         addSketchesSubmenu(retired, lib);
1234       }
1235     }
1236 
1237     if (!platformLibs.isEmpty()) {
1238       menu.addSeparator();
1239       platformLibs.sort();
1240       label = new JMenuItem(I18n.format(tr("Examples for {0}"), boardId));
1241       label.setEnabled(false);
1242       menu.add(label);
1243       for (UserLibrary lib : platformLibs) {
1244         addSketchesSubmenu(menu, lib);
1245       }
1246     }
1247 
1248     if (!referencedPlatformLibs.isEmpty()) {
1249       menu.addSeparator();
1250       referencedPlatformLibs.sort();
1251       label = new JMenuItem(I18n.format(tr("Examples for {0}"), referencedPlatformName));
1252       label.setEnabled(false);
1253       menu.add(label);
1254       for (UserLibrary lib : referencedPlatformLibs) {
1255         addSketchesSubmenu(menu, lib);
1256       }
1257     }
1258 
1259     if (!sketchbookLibs.isEmpty()) {
1260       menu.addSeparator();
1261       sketchbookLibs.sort();
1262       label = new JMenuItem(tr("Examples from Custom Libraries"));
1263       label.setEnabled(false);
1264       menu.add(label);
1265       for (UserLibrary lib : sketchbookLibs) {
1266         addSketchesSubmenu(menu, lib);
1267       }
1268     }
1269 
1270     if (!sketchbookIncompatibleLibs.isEmpty()) {
1271       sketchbookIncompatibleLibs.sort();
1272       JMenu incompatible = new JMenu(tr("INCOMPATIBLE"));
1273       menu.add(incompatible);
1274       for (UserLibrary lib : sketchbookIncompatibleLibs) {
1275         addSketchesSubmenu(incompatible, lib);
1276       }
1277     }
1278 
1279     if (!otherLibs.isEmpty()) {
1280       menu.addSeparator();
1281       otherLibs.sort();
1282       label = new JMenuItem(tr("Examples from Other Libraries"));
1283       label.setEnabled(false);
1284       menu.add(label);
1285       for (UserLibrary lib : otherLibs) {
1286         addSketchesSubmenu(menu, lib);
1287       }
1288     }
1289   }
1290 
1291   private static String priorPlatformFolder;
1292   private static boolean newLibraryImported;
1293 
onBoardOrPortChange()1294   public void onBoardOrPortChange() {
1295     BaseNoGui.onBoardOrPortChange();
1296 
1297     // reload keywords when package/platform changes
1298     TargetPlatform tp = BaseNoGui.getTargetPlatform();
1299     if (tp != null) {
1300       String platformFolder = tp.getFolder().getAbsolutePath();
1301       if (priorPlatformFolder == null || !priorPlatformFolder.equals(platformFolder) || newLibraryImported) {
1302         pdeKeywords = new PdeKeywords();
1303         pdeKeywords.reload();
1304         priorPlatformFolder = platformFolder;
1305         newLibraryImported = false;
1306         for (Editor editor : editors) {
1307           editor.updateKeywords(pdeKeywords);
1308         }
1309       }
1310     }
1311 
1312     // Update editors status bar
1313     for (Editor editor : editors) {
1314       editor.onBoardOrPortChange();
1315     }
1316   }
1317 
openLibraryManager(final String filterText, String dropdownItem)1318   public void openLibraryManager(final String filterText, String dropdownItem) {
1319     if (contributionsSelfCheck != null) {
1320       contributionsSelfCheck.cancel();
1321     }
1322     @SuppressWarnings("serial")
1323     LibraryManagerUI managerUI = new LibraryManagerUI(activeEditor, libraryInstaller) {
1324       @Override
1325       protected void onIndexesUpdated() throws Exception {
1326         BaseNoGui.initPackages();
1327         rebuildBoardsMenu();
1328         rebuildProgrammerMenu();
1329         onBoardOrPortChange();
1330         updateUI();
1331         if (StringUtils.isNotEmpty(dropdownItem)) {
1332           selectDropdownItemByClassName(dropdownItem);
1333         }
1334         if (StringUtils.isNotEmpty(filterText)) {
1335           setFilterText(filterText);
1336         }
1337       }
1338     };
1339     managerUI.setLocationRelativeTo(activeEditor);
1340     managerUI.updateUI();
1341     managerUI.setVisible(true);
1342     // Manager dialog is modal, waits here until closed
1343 
1344     //handleAddLibrary();
1345     newLibraryImported = true;
1346     onBoardOrPortChange();
1347     rebuildImportMenu(Editor.importMenu);
1348     rebuildExamplesMenu(Editor.examplesMenu);
1349   }
1350 
openBoardsManager(final String filterText, String dropdownItem)1351   public void openBoardsManager(final String filterText, String dropdownItem) throws Exception {
1352     if (contributionsSelfCheck != null) {
1353       contributionsSelfCheck.cancel();
1354     }
1355     @SuppressWarnings("serial")
1356     ContributionManagerUI managerUI = new ContributionManagerUI(activeEditor, contributionInstaller) {
1357       @Override
1358       protected void onIndexesUpdated() throws Exception {
1359         BaseNoGui.initPackages();
1360         rebuildBoardsMenu();
1361         rebuildProgrammerMenu();
1362         updateUI();
1363         if (StringUtils.isNotEmpty(dropdownItem)) {
1364           selectDropdownItemByClassName(dropdownItem);
1365         }
1366         if (StringUtils.isNotEmpty(filterText)) {
1367           setFilterText(filterText);
1368         }
1369       }
1370     };
1371     managerUI.setLocationRelativeTo(activeEditor);
1372     managerUI.updateUI();
1373     managerUI.setVisible(true);
1374     // Installer dialog is modal, waits here until closed
1375 
1376     // Reload all boards (that may have been installed/updated/removed)
1377     BaseNoGui.initPackages();
1378     rebuildBoardsMenu();
1379     rebuildProgrammerMenu();
1380     onBoardOrPortChange();
1381   }
1382 
rebuildBoardsMenu()1383   public void rebuildBoardsMenu() throws Exception {
1384     boardsCustomMenus = new LinkedList<>();
1385 
1386     // The first custom menu is the "Board" selection submenu
1387     JMenu boardMenu = new JMenu(tr("Board"));
1388     boardMenu.putClientProperty("removeOnWindowDeactivation", true);
1389     MenuScroller.setScrollerFor(boardMenu);
1390 
1391     boardMenu.add(new JMenuItem(new AbstractAction(tr("Boards Manager...")) {
1392       public void actionPerformed(ActionEvent actionevent) {
1393         String filterText = "";
1394         String dropdownItem = "";
1395         if (actionevent instanceof Event) {
1396           filterText = ((Event) actionevent).getPayload().get("filterText").toString();
1397           dropdownItem = ((Event) actionevent).getPayload().get("dropdownItem").toString();
1398         }
1399         try {
1400           openBoardsManager(filterText, dropdownItem);
1401         } catch (Exception e) {
1402           //TODO show error
1403           e.printStackTrace();
1404         }
1405       }
1406     }));
1407     boardsCustomMenus.add(boardMenu);
1408 
1409     // If there are no platforms installed we are done
1410     if (BaseNoGui.packages.size() == 0)
1411       return;
1412 
1413     // Separate "Install boards..." command from installed boards
1414     boardMenu.add(new JSeparator());
1415 
1416     // Generate custom menus for all platforms
1417     Set<String> customMenusTitles = new HashSet<>();
1418     for (TargetPackage targetPackage : BaseNoGui.packages.values()) {
1419       for (TargetPlatform targetPlatform : targetPackage.platforms()) {
1420         customMenusTitles.addAll(targetPlatform.getCustomMenus().values());
1421       }
1422     }
1423     for (String customMenuTitle : customMenusTitles) {
1424       JMenu customMenu = new JMenu(tr(customMenuTitle));
1425       customMenu.putClientProperty("removeOnWindowDeactivation", true);
1426       boardsCustomMenus.add(customMenu);
1427     }
1428 
1429     List<JMenuItem> menuItemsToClickAfterStartup = new LinkedList<>();
1430 
1431     ButtonGroup boardsButtonGroup = new ButtonGroup();
1432     Map<String, ButtonGroup> buttonGroupsMap = new HashMap<>();
1433 
1434     // Cycle through all packages
1435     boolean first = true;
1436     for (TargetPackage targetPackage : BaseNoGui.packages.values()) {
1437       // For every package cycle through all platform
1438       for (TargetPlatform targetPlatform : targetPackage.platforms()) {
1439 
1440         // Add a separator from the previous platform
1441         if (!first)
1442           boardMenu.add(new JSeparator());
1443         first = false;
1444 
1445         // Add a title for each platform
1446         String platformLabel = targetPlatform.getPreferences().get("name");
1447         if (platformLabel != null && !targetPlatform.getBoards().isEmpty()) {
1448           JMenuItem menuLabel = new JMenuItem(tr(platformLabel));
1449           menuLabel.setEnabled(false);
1450           boardMenu.add(menuLabel);
1451         }
1452 
1453         // Cycle through all boards of this platform
1454         for (TargetBoard board : targetPlatform.getBoards().values()) {
1455           if (board.getPreferences().get("hide") != null)
1456             continue;
1457           JMenuItem item = createBoardMenusAndCustomMenus(boardsCustomMenus, menuItemsToClickAfterStartup,
1458                   buttonGroupsMap,
1459                   board, targetPlatform, targetPackage);
1460           boardMenu.add(item);
1461           boardsButtonGroup.add(item);
1462         }
1463       }
1464     }
1465 
1466     if (menuItemsToClickAfterStartup.isEmpty()) {
1467       menuItemsToClickAfterStartup.add(selectFirstEnabledMenuItem(boardMenu));
1468     }
1469 
1470     for (JMenuItem menuItemToClick : menuItemsToClickAfterStartup) {
1471       menuItemToClick.setSelected(true);
1472       menuItemToClick.getAction().actionPerformed(new ActionEvent(this, -1, ""));
1473     }
1474   }
1475 
createBoardMenusAndCustomMenus( final List<JMenu> boardsCustomMenus, List<JMenuItem> menuItemsToClickAfterStartup, Map<String, ButtonGroup> buttonGroupsMap, TargetBoard board, TargetPlatform targetPlatform, TargetPackage targetPackage)1476   private JRadioButtonMenuItem createBoardMenusAndCustomMenus(
1477           final List<JMenu> boardsCustomMenus, List<JMenuItem> menuItemsToClickAfterStartup,
1478           Map<String, ButtonGroup> buttonGroupsMap,
1479           TargetBoard board, TargetPlatform targetPlatform, TargetPackage targetPackage)
1480           throws Exception {
1481     String selPackage = PreferencesData.get("target_package");
1482     String selPlatform = PreferencesData.get("target_platform");
1483     String selBoard = PreferencesData.get("board");
1484 
1485     String boardId = board.getId();
1486     String packageName = targetPackage.getId();
1487     String platformName = targetPlatform.getId();
1488 
1489     // Setup a menu item for the current board
1490     @SuppressWarnings("serial")
1491     Action action = new AbstractAction(board.getName()) {
1492       public void actionPerformed(ActionEvent actionevent) {
1493         BaseNoGui.selectBoard((TargetBoard) getValue("b"));
1494         filterVisibilityOfSubsequentBoardMenus(boardsCustomMenus, (TargetBoard) getValue("b"), 1);
1495 
1496         onBoardOrPortChange();
1497         rebuildImportMenu(Editor.importMenu);
1498         rebuildExamplesMenu(Editor.examplesMenu);
1499       }
1500     };
1501     action.putValue("b", board);
1502 
1503     JRadioButtonMenuItem item = new JRadioButtonMenuItem(action);
1504 
1505     if (selBoard.equals(boardId) && selPackage.equals(packageName)
1506             && selPlatform.equals(platformName)) {
1507       menuItemsToClickAfterStartup.add(item);
1508     }
1509 
1510     PreferencesMap customMenus = targetPlatform.getCustomMenus();
1511     for (final String menuId : customMenus.keySet()) {
1512       String title = customMenus.get(menuId);
1513       JMenu menu = getBoardCustomMenu(tr(title));
1514 
1515       if (board.hasMenu(menuId)) {
1516         PreferencesMap boardCustomMenu = board.getMenuLabels(menuId);
1517         for (String customMenuOption : boardCustomMenu.keySet()) {
1518           @SuppressWarnings("serial")
1519           Action subAction = new AbstractAction(tr(boardCustomMenu.get(customMenuOption))) {
1520             public void actionPerformed(ActionEvent e) {
1521               PreferencesData.set("custom_" + menuId, ((TargetBoard) getValue("board")).getId() + "_" + getValue("custom_menu_option"));
1522               onBoardOrPortChange();
1523             }
1524           };
1525           subAction.putValue("board", board);
1526           subAction.putValue("custom_menu_option", customMenuOption);
1527 
1528           if (!buttonGroupsMap.containsKey(menuId)) {
1529             buttonGroupsMap.put(menuId, new ButtonGroup());
1530           }
1531 
1532           JRadioButtonMenuItem subItem = new JRadioButtonMenuItem(subAction);
1533           menu.add(subItem);
1534           buttonGroupsMap.get(menuId).add(subItem);
1535 
1536           String selectedCustomMenuEntry = PreferencesData.get("custom_" + menuId);
1537           if (selBoard.equals(boardId) && (boardId + "_" + customMenuOption).equals(selectedCustomMenuEntry)) {
1538             menuItemsToClickAfterStartup.add(subItem);
1539           }
1540         }
1541       }
1542     }
1543 
1544     return item;
1545   }
1546 
filterVisibilityOfSubsequentBoardMenus(List<JMenu> boardsCustomMenus, TargetBoard board, int fromIndex)1547   private void filterVisibilityOfSubsequentBoardMenus(List<JMenu> boardsCustomMenus, TargetBoard board,
1548                                                       int fromIndex) {
1549     for (int i = fromIndex; i < boardsCustomMenus.size(); i++) {
1550       JMenu menu = boardsCustomMenus.get(i);
1551       for (int m = 0; m < menu.getItemCount(); m++) {
1552         JMenuItem menuItem = menu.getItem(m);
1553         menuItem.setVisible(menuItem.getAction().getValue("board").equals(board));
1554       }
1555       menu.setVisible(ifThereAreVisibleItemsOn(menu));
1556 
1557       if (menu.isVisible()) {
1558         JMenuItem visibleSelectedOrFirstMenuItem = selectVisibleSelectedOrFirstMenuItem(menu);
1559         if (!visibleSelectedOrFirstMenuItem.isSelected()) {
1560           visibleSelectedOrFirstMenuItem.setSelected(true);
1561           visibleSelectedOrFirstMenuItem.getAction().actionPerformed(null);
1562         }
1563       }
1564     }
1565   }
1566 
ifThereAreVisibleItemsOn(JMenu menu)1567   private static boolean ifThereAreVisibleItemsOn(JMenu menu) {
1568     for (int i = 0; i < menu.getItemCount(); i++) {
1569       if (menu.getItem(i).isVisible()) {
1570         return true;
1571       }
1572     }
1573     return false;
1574   }
1575 
getBoardCustomMenu(String label)1576   private JMenu getBoardCustomMenu(String label) throws Exception {
1577     for (JMenu menu : boardsCustomMenus) {
1578       if (label.equals(menu.getText())) {
1579         return menu;
1580       }
1581     }
1582     throw new Exception("Custom menu not found!");
1583   }
1584 
getProgrammerMenus()1585   public List<JMenuItem> getProgrammerMenus() {
1586     return programmerMenus;
1587   }
1588 
selectVisibleSelectedOrFirstMenuItem(JMenu menu)1589   private static JMenuItem selectVisibleSelectedOrFirstMenuItem(JMenu menu) {
1590     JMenuItem firstVisible = null;
1591     for (int i = 0; i < menu.getItemCount(); i++) {
1592       JMenuItem item = menu.getItem(i);
1593       if (item != null && item.isVisible()) {
1594         if (item.isSelected()) {
1595           return item;
1596         }
1597         if (firstVisible == null) {
1598           firstVisible = item;
1599         }
1600       }
1601     }
1602 
1603     if (firstVisible != null) {
1604       return firstVisible;
1605     }
1606 
1607     throw new IllegalStateException("Menu has no enabled items");
1608   }
1609 
selectFirstEnabledMenuItem(JMenu menu)1610   private static JMenuItem selectFirstEnabledMenuItem(JMenu menu) {
1611     for (int i = 1; i < menu.getItemCount(); i++) {
1612       JMenuItem item = menu.getItem(i);
1613       if (item != null && item.isEnabled()) {
1614         return item;
1615       }
1616     }
1617     throw new IllegalStateException("Menu has no enabled items");
1618   }
1619 
rebuildProgrammerMenu()1620   public void rebuildProgrammerMenu() {
1621     programmerMenus = new LinkedList<>();
1622 
1623     ButtonGroup group = new ButtonGroup();
1624     for (TargetPackage targetPackage : BaseNoGui.packages.values()) {
1625       for (TargetPlatform targetPlatform : targetPackage.platforms()) {
1626         for (String programmer : targetPlatform.getProgrammers().keySet()) {
1627           String id = targetPackage.getId() + ":" + programmer;
1628 
1629           @SuppressWarnings("serial")
1630           AbstractAction action = new AbstractAction(targetPlatform.getProgrammer(programmer).get("name")) {
1631             public void actionPerformed(ActionEvent actionevent) {
1632               PreferencesData.set("programmer", "" + getValue("id"));
1633             }
1634           };
1635           action.putValue("id", id);
1636           JMenuItem item = new JRadioButtonMenuItem(action);
1637           if (PreferencesData.get("programmer").equals(id)) {
1638             item.setSelected(true);
1639           }
1640           group.add(item);
1641           programmerMenus.add(item);
1642         }
1643       }
1644     }
1645   }
1646 
1647   /**
1648    * Scan a folder recursively, and add any sketches found to the menu
1649    * specified. Set the openReplaces parameter to true when opening the sketch
1650    * should replace the sketch in the current window, or false when the
1651    * sketch should open in a new window.
1652    */
addSketches(JMenu menu, File folder)1653   protected boolean addSketches(JMenu menu, File folder) {
1654     if (folder == null)
1655       return false;
1656 
1657     if (!folder.isDirectory()) return false;
1658 
1659     File[] files = folder.listFiles();
1660     // If a bad folder or unreadable or whatever, this will come back null
1661     if (files == null) return false;
1662 
1663     // Alphabetize files, since it's not always alpha order
1664     Arrays.sort(files, new Comparator<File>() {
1665       @Override
1666       public int compare(File file, File file2) {
1667         return file.getName().compareToIgnoreCase(file2.getName());
1668       }
1669     });
1670 
1671     boolean ifound = false;
1672 
1673     for (File subfolder : files) {
1674       if (FileUtils.isSCCSOrHiddenFile(subfolder)) {
1675         continue;
1676       }
1677 
1678       if (!subfolder.isDirectory()) continue;
1679 
1680       if (addSketchesSubmenu(menu, subfolder.getName(), subfolder)) {
1681         ifound = true;
1682       }
1683     }
1684 
1685     return ifound;
1686   }
1687 
addSketchesSubmenu(JMenu menu, UserLibrary lib)1688   private boolean addSketchesSubmenu(JMenu menu, UserLibrary lib) {
1689     return addSketchesSubmenu(menu, lib.getName(), lib.getInstalledFolder());
1690   }
1691 
addSketchesSubmenu(JMenu menu, String name, File folder)1692   private boolean addSketchesSubmenu(JMenu menu, String name, File folder) {
1693 
1694     ActionListener listener = new ActionListener() {
1695       public void actionPerformed(ActionEvent e) {
1696         String path = e.getActionCommand();
1697         File file = new File(path);
1698         if (file.exists()) {
1699           try {
1700             handleOpen(file);
1701           } catch (Exception e1) {
1702             e1.printStackTrace();
1703           }
1704         } else {
1705           showWarning(tr("Sketch Does Not Exist"),
1706                   tr("The selected sketch no longer exists.\n"
1707                           + "You may need to restart Arduino to update\n"
1708                           + "the sketchbook menu."), null);
1709         }
1710       }
1711     };
1712 
1713     File entry = new File(folder, name + ".ino");
1714     if (!entry.exists() && (new File(folder, name + ".pde")).exists())
1715       entry = new File(folder, name + ".pde");
1716 
1717     // if a .pde file of the same prefix as the folder exists..
1718     if (entry.exists()) {
1719 
1720       if (!BaseNoGui.isSanitaryName(name)) {
1721         if (!builtOnce) {
1722           String complaining = I18n
1723                   .format(
1724                           tr("The sketch \"{0}\" cannot be used.\n"
1725                                   + "Sketch names must contain only basic letters and numbers\n"
1726                                   + "(ASCII-only with no spaces, "
1727                                   + "and it cannot start with a number).\n"
1728                                   + "To get rid of this message, remove the sketch from\n"
1729                                   + "{1}"), name, entry.getAbsolutePath());
1730           showMessage(tr("Ignoring sketch with bad name"), complaining);
1731         }
1732         return false;
1733       }
1734 
1735       JMenuItem item = new JMenuItem(name);
1736       item.addActionListener(listener);
1737       item.setActionCommand(entry.getAbsolutePath());
1738       menu.add(item);
1739       return true;
1740     }
1741 
1742     // don't create an extra menu level for a folder named "examples"
1743     if (folder.getName().equals("examples"))
1744       return addSketches(menu, folder);
1745 
1746     // not a sketch folder, but maybe a subfolder containing sketches
1747     JMenu submenu = new JMenu(name);
1748     boolean found = addSketches(submenu, folder);
1749     if (found) {
1750       menu.add(submenu);
1751       MenuScroller.setScrollerFor(submenu);
1752     }
1753     return found;
1754   }
1755 
addLibraries(JMenu menu, LibraryList libs)1756   protected void addLibraries(JMenu menu, LibraryList libs) throws IOException {
1757 
1758     LibraryList list = new LibraryList(libs);
1759     list.sort();
1760 
1761     for (UserLibrary lib : list) {
1762       @SuppressWarnings("serial")
1763       AbstractAction action = new AbstractAction(lib.getName()) {
1764         public void actionPerformed(ActionEvent event) {
1765           UserLibrary l = (UserLibrary) getValue("library");
1766           try {
1767             activeEditor.getSketchController().importLibrary(l);
1768           } catch (IOException e) {
1769             showWarning(tr("Error"), I18n.format("Unable to list header files in {0}", l.getSrcFolder()), e);
1770           }
1771         }
1772       };
1773       action.putValue("library", lib);
1774 
1775       // Add new element at the bottom
1776       JMenuItem item = new JMenuItem(action);
1777       item.putClientProperty("library", lib);
1778       menu.add(item);
1779 
1780       // XXX: DAM: should recurse here so that library folders can be nested
1781     }
1782   }
1783 
1784   /**
1785    * Given a folder, return a list of the header files in that folder (but not
1786    * the header files in its sub-folders, as those should be included from
1787    * within the header files at the top-level).
1788    */
headerListFromIncludePath(File path)1789   static public String[] headerListFromIncludePath(File path) throws IOException {
1790     String[] list = path.list(new OnlyFilesWithExtension(".h"));
1791     if (list == null) {
1792       throw new IOException();
1793     }
1794     return list;
1795   }
1796 
1797   /**
1798    * Show the About box.
1799    */
1800   @SuppressWarnings("serial")
handleAbout()1801   public void handleAbout() {
1802     final Image image = Theme.getLibImage("about", activeEditor,
1803                                           Theme.scale(475), Theme.scale(300));
1804     final Window window = new Window(activeEditor) {
1805       public void paint(Graphics graphics) {
1806         Graphics2D g = Theme.setupGraphics2D(graphics);
1807         g.drawImage(image, 0, 0, null);
1808 
1809         Font f = new Font("SansSerif", Font.PLAIN, Theme.scale(11));
1810         g.setFont(f);
1811         g.setColor(new Color(0,151,156));
1812         g.drawString(BaseNoGui.VERSION_NAME_LONG, Theme.scale(33), Theme.scale(20));
1813       }
1814     };
1815     window.addMouseListener(new MouseAdapter() {
1816       public void mousePressed(MouseEvent e) {
1817         window.dispose();
1818       }
1819     });
1820     int w = image.getWidth(activeEditor);
1821     int h = image.getHeight(activeEditor);
1822     Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
1823     window.setBounds((screen.width - w) / 2, (screen.height - h) / 2, w, h);
1824     window.setLocationRelativeTo(activeEditor);
1825     window.setVisible(true);
1826   }
1827 
1828 
1829   /**
1830    * Show the Preferences window.
1831    */
handlePrefs()1832   public void handlePrefs() {
1833     cc.arduino.view.preferences.Preferences dialog = new cc.arduino.view.preferences.Preferences(activeEditor, this);
1834     if (activeEditor != null) {
1835       dialog.setLocationRelativeTo(activeEditor);
1836     }
1837     dialog.setVisible(true);
1838   }
1839 
1840   // XXX: Remove this method and make librariesIndexer non-static
getLibraries()1841   static public LibraryList getLibraries() {
1842     return BaseNoGui.librariesIndexer.getInstalledLibraries();
1843   }
1844 
getBoardsCustomMenus()1845   public List<JMenu> getBoardsCustomMenus() {
1846     return boardsCustomMenus;
1847   }
1848 
getDefaultSketchbookFolderOrPromptForIt()1849   public File getDefaultSketchbookFolderOrPromptForIt() {
1850     File sketchbookFolder = BaseNoGui.getDefaultSketchbookFolder();
1851 
1852     if (sketchbookFolder == null && !isCommandLine()) {
1853       sketchbookFolder = promptSketchbookLocation();
1854     }
1855 
1856     // create the folder if it doesn't exist already
1857     boolean result = true;
1858     if (!sketchbookFolder.exists()) {
1859       result = sketchbookFolder.mkdirs();
1860     }
1861 
1862     if (!result) {
1863       showError(tr("You forgot your sketchbook"),
1864               tr("Arduino cannot run because it could not\n" +
1865                       "create a folder to store your sketchbook."), null);
1866     }
1867 
1868     return sketchbookFolder;
1869   }
1870 
1871 
1872   /**
1873    * Check for a new sketchbook location.
1874    */
promptSketchbookLocation()1875   static protected File promptSketchbookLocation() {
1876     File folder = null;
1877 
1878     folder = new File(System.getProperty("user.home"), "sketchbook");
1879     if (!folder.exists()) {
1880       folder.mkdirs();
1881       return folder;
1882     }
1883 
1884     String prompt = tr("Select (or create new) folder for sketches...");
1885     folder = selectFolder(prompt, null, null);
1886     if (folder == null) {
1887       System.exit(0);
1888     }
1889     return folder;
1890   }
1891 
1892 
1893   // .................................................................
1894 
1895 
1896   /**
1897    * Implements the cross-platform headache of opening URLs
1898    * TODO This code should be replaced by PApplet.link(),
1899    * however that's not a static method (because it requires
1900    * an AppletContext when used as an applet), so it's mildly
1901    * trickier than just removing this method.
1902    */
openURL(String url)1903   static public void openURL(String url) {
1904     try {
1905       BaseNoGui.getPlatform().openURL(url);
1906 
1907     } catch (Exception e) {
1908       showWarning(tr("Problem Opening URL"),
1909               I18n.format(tr("Could not open the URL\n{0}"), url), e);
1910     }
1911   }
1912 
1913 
1914   /**
1915    * Used to determine whether to disable the "Show Sketch Folder" option.
1916    *
1917    * @return true If a means of opening a folder is known to be available.
1918    */
openFolderAvailable()1919   static protected boolean openFolderAvailable() {
1920     return BaseNoGui.getPlatform().openFolderAvailable();
1921   }
1922 
1923 
1924   /**
1925    * Implements the other cross-platform headache of opening
1926    * a folder in the machine's native file browser.
1927    */
openFolder(File file)1928   static public void openFolder(File file) {
1929     try {
1930       BaseNoGui.getPlatform().openFolder(file);
1931 
1932     } catch (Exception e) {
1933       showWarning(tr("Problem Opening Folder"),
1934               I18n.format(tr("Could not open the folder\n{0}"), file.getAbsolutePath()), e);
1935     }
1936   }
1937 
1938 
1939   // .................................................................
1940 
1941 
selectFolder(String prompt, File folder, Component parent)1942   static public File selectFolder(String prompt, File folder, Component parent) {
1943     JFileChooser fc = new JFileChooser();
1944     fc.setDialogTitle(prompt);
1945     if (folder != null) {
1946       fc.setSelectedFile(folder);
1947     }
1948     fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
1949 
1950     int returned = fc.showOpenDialog(parent);
1951     if (returned == JFileChooser.APPROVE_OPTION) {
1952       return fc.getSelectedFile();
1953     }
1954     return null;
1955   }
1956 
1957 
1958   // .................................................................
1959 
1960 
1961   /**
1962    * Give this Frame an icon.
1963    */
setIcon(Frame frame)1964   static public void setIcon(Frame frame) {
1965     if (OSUtils.isMacOS()) {
1966       return;
1967     }
1968 
1969     List<Image> icons = Stream
1970       .of("16", "24", "32", "48", "64", "72", "96", "128", "256")
1971       .map(res -> "/lib/icons/" + res + "x" + res + "/apps/arduino.png")
1972       .map(path -> BaseNoGui.getContentFile(path).getAbsolutePath())
1973       .map(absPath -> Toolkit.getDefaultToolkit().createImage(absPath))
1974       .collect(Collectors.toList());
1975     frame.setIconImages(icons);
1976   }
1977 
1978 
1979   /**
1980    * Registers key events for a Ctrl-W and ESC with an ActionListener
1981    * that will take care of disposing the window.
1982    */
registerWindowCloseKeys(JRootPane root, ActionListener disposer)1983   static public void registerWindowCloseKeys(JRootPane root,
1984                                              ActionListener disposer) {
1985     KeyStroke stroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
1986     root.registerKeyboardAction(disposer, stroke,
1987             JComponent.WHEN_IN_FOCUSED_WINDOW);
1988 
1989     int modifiers = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
1990     stroke = KeyStroke.getKeyStroke('W', modifiers);
1991     root.registerKeyboardAction(disposer, stroke,
1992             JComponent.WHEN_IN_FOCUSED_WINDOW);
1993   }
1994 
1995 
1996   // .................................................................
1997 
1998 
showReference(String filename)1999   static public void showReference(String filename) {
2000     showReference("reference/www.arduino.cc/en", filename);
2001   }
2002 
showReference(String prefix, String filename)2003   static public void showReference(String prefix, String filename) {
2004     File referenceFolder = getContentFile(prefix);
2005     File referenceFile = new File(referenceFolder, filename);
2006     if (!referenceFile.exists())
2007       referenceFile = new File(referenceFolder, filename + ".html");
2008 
2009     if(referenceFile.exists()){
2010       openURL(referenceFile.getAbsolutePath());
2011     }else{
2012       showWarning(tr("Problem Opening URL"), I18n.format(tr("Could not open the URL\n{0}"), referenceFile), null);
2013     }
2014   }
2015 
showEdisonGettingStarted()2016   public static void showEdisonGettingStarted() {
2017     showReference("reference/Edison_help_files", "ArduinoIDE_guide_edison");
2018   }
2019 
showArduinoGettingStarted()2020   static public void showArduinoGettingStarted() {
2021     if (OSUtils.isMacOS()) {
2022       showReference("Guide/MacOSX");
2023     } else if (OSUtils.isWindows()) {
2024       showReference("Guide/Windows");
2025     } else {
2026       openURL("http://www.arduino.cc/playground/Learning/Linux");
2027     }
2028   }
2029 
showReference()2030   static public void showReference() {
2031     showReference("Reference/HomePage");
2032   }
2033 
2034 
showEnvironment()2035   static public void showEnvironment() {
2036     showReference("Guide/Environment");
2037   }
2038 
2039 
showTroubleshooting()2040   static public void showTroubleshooting() {
2041     showReference("Guide/Troubleshooting");
2042   }
2043 
2044 
showFAQ()2045   static public void showFAQ() {
2046     showReference("Main/FAQ");
2047   }
2048 
2049 
2050   // .................................................................
2051 
2052 
2053   /**
2054    * "No cookie for you" type messages. Nothing fatal or all that
2055    * much of a bummer, but something to notify the user about.
2056    */
showMessage(String title, String message)2057   static public void showMessage(String title, String message) {
2058     BaseNoGui.showMessage(title, message);
2059   }
2060 
2061 
2062   /**
2063    * Non-fatal error message with optional stack trace side dish.
2064    */
showWarning(String title, String message, Exception e)2065   static public void showWarning(String title, String message, Exception e) {
2066     BaseNoGui.showWarning(title, message, e);
2067   }
2068 
2069 
showError(String title, String message, Throwable e)2070   static public void showError(String title, String message, Throwable e) {
2071     showError(title, message, e, 1);
2072   }
2073 
showError(String title, String message, int exit_code)2074   static public void showError(String title, String message, int exit_code) {
2075     showError(title, message, null, exit_code);
2076   }
2077 
2078   /**
2079    * Show an error message that's actually fatal to the program.
2080    * This is an error that can't be recovered. Use showWarning()
2081    * for errors that allow P5 to continue running.
2082    */
showError(String title, String message, Throwable e, int exit_code)2083   static public void showError(String title, String message, Throwable e, int exit_code) {
2084     BaseNoGui.showError(title, message, e, exit_code);
2085   }
2086 
2087 
getContentFile(String name)2088   static public File getContentFile(String name) {
2089     return BaseNoGui.getContentFile(name);
2090   }
2091 
2092 
2093   // ...................................................................
2094 
2095 
2096   /**
2097    * Get the number of lines in a file by counting the number of newline
2098    * characters inside a String (and adding 1).
2099    */
countLines(String what)2100   static public int countLines(String what) {
2101     return BaseNoGui.countLines(what);
2102   }
2103 
2104 
2105   /**
2106    * Same as PApplet.loadBytes(), however never does gzip decoding.
2107    */
loadBytesRaw(File file)2108   static public byte[] loadBytesRaw(File file) throws IOException {
2109     int size = (int) file.length();
2110     FileInputStream input = null;
2111     try {
2112       input = new FileInputStream(file);
2113       byte buffer[] = new byte[size];
2114       int offset = 0;
2115       int bytesRead;
2116       while ((bytesRead = input.read(buffer, offset, size - offset)) != -1) {
2117         offset += bytesRead;
2118         if (bytesRead == 0) break;
2119       }
2120       return buffer;
2121     } finally {
2122       IOUtils.closeQuietly(input);
2123     }
2124   }
2125 
2126 
2127   /**
2128    * Read from a file with a bunch of attribute/value pairs
2129    * that are separated by = and ignore comments with #.
2130    */
readSettings(File inputFile)2131   static public HashMap<String, String> readSettings(File inputFile) {
2132     HashMap<String, String> outgoing = new HashMap<>();
2133     if (!inputFile.exists()) return outgoing;  // return empty hash
2134 
2135     String lines[] = PApplet.loadStrings(inputFile);
2136     for (int i = 0; i < lines.length; i++) {
2137       int hash = lines[i].indexOf('#');
2138       String line = (hash == -1) ?
2139               lines[i].trim() : lines[i].substring(0, hash).trim();
2140       if (line.length() == 0) continue;
2141 
2142       int equals = line.indexOf('=');
2143       if (equals == -1) {
2144         System.err.println("ignoring illegal line in " + inputFile);
2145         System.err.println("  " + line);
2146         continue;
2147       }
2148       String attr = line.substring(0, equals).trim();
2149       String valu = line.substring(equals + 1).trim();
2150       outgoing.put(attr, valu);
2151     }
2152     return outgoing;
2153   }
2154 
2155 
copyFile(File sourceFile, File targetFile)2156   static public void copyFile(File sourceFile,
2157                               File targetFile) throws IOException {
2158     InputStream from = null;
2159     OutputStream to = null;
2160     try {
2161       from = new BufferedInputStream(new FileInputStream(sourceFile));
2162       to = new BufferedOutputStream(new FileOutputStream(targetFile));
2163       byte[] buffer = new byte[16 * 1024];
2164       int bytesRead;
2165       while ((bytesRead = from.read(buffer)) != -1) {
2166         to.write(buffer, 0, bytesRead);
2167       }
2168       to.flush();
2169     } finally {
2170       IOUtils.closeQuietly(from);
2171       IOUtils.closeQuietly(to);
2172     }
2173 
2174     targetFile.setLastModified(sourceFile.lastModified());
2175   }
2176 
2177 
2178   /**
2179    * Grab the contents of a file as a string.
2180    */
loadFile(File file)2181   static public String loadFile(File file) throws IOException {
2182     return BaseNoGui.loadFile(file);
2183   }
2184 
2185 
2186   /**
2187    * Spew the contents of a String object out to a file.
2188    */
saveFile(String str, File file)2189   static public void saveFile(String str, File file) throws IOException {
2190     BaseNoGui.saveFile(str, file);
2191   }
2192 
2193 
2194   /**
2195    * Calculate the size of the contents of a folder.
2196    * Used to determine whether sketches are empty or not.
2197    * Note that the function calls itself recursively.
2198    */
calcFolderSize(File folder)2199   static public int calcFolderSize(File folder) {
2200     int size = 0;
2201 
2202     String files[] = folder.list();
2203     // null if folder doesn't exist, happens when deleting sketch
2204     if (files == null) return -1;
2205 
2206     for (int i = 0; i < files.length; i++) {
2207       if (files[i].equals(".") || (files[i].equals("..")) ||
2208               files[i].equals(".DS_Store")) continue;
2209       File fella = new File(folder, files[i]);
2210       if (fella.isDirectory()) {
2211         size += calcFolderSize(fella);
2212       } else {
2213         size += (int) fella.length();
2214       }
2215     }
2216     return size;
2217   }
2218 
handleAddLibrary()2219   public void handleAddLibrary() {
2220     JFileChooser fileChooser = new JFileChooser(System.getProperty("user.home"));
2221     fileChooser.setDialogTitle(tr("Select a zip file or a folder containing the library you'd like to add"));
2222     fileChooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
2223     fileChooser.setFileFilter(new FileNameExtensionFilter(tr("ZIP files or folders"), "zip"));
2224 
2225     Dimension preferredSize = fileChooser.getPreferredSize();
2226     fileChooser.setPreferredSize(new Dimension(preferredSize.width + 200, preferredSize.height + 200));
2227 
2228     int returnVal = fileChooser.showOpenDialog(activeEditor);
2229 
2230     if (returnVal != JFileChooser.APPROVE_OPTION) {
2231       return;
2232     }
2233 
2234     File sourceFile = fileChooser.getSelectedFile();
2235     File tmpFolder = null;
2236 
2237     try {
2238       // unpack ZIP
2239       if (!sourceFile.isDirectory()) {
2240         try {
2241           tmpFolder = FileUtils.createTempFolder();
2242           ZipDeflater zipDeflater = new ZipDeflater(sourceFile, tmpFolder);
2243           zipDeflater.deflate();
2244           File[] foldersInTmpFolder = tmpFolder.listFiles(new OnlyDirs());
2245           if (foldersInTmpFolder.length != 1) {
2246             throw new IOException(tr("Zip doesn't contain a library"));
2247           }
2248           sourceFile = foldersInTmpFolder[0];
2249         } catch (IOException e) {
2250           activeEditor.statusError(e);
2251           return;
2252         }
2253       }
2254 
2255       File libFolder = sourceFile;
2256       if (FileUtils.isSubDirectory(new File(PreferencesData.get("sketchbook.path")), libFolder)) {
2257         activeEditor.statusError(tr("A subfolder of your sketchbook is not a valid library"));
2258         return;
2259       }
2260 
2261       if (FileUtils.isSubDirectory(libFolder, new File(PreferencesData.get("sketchbook.path")))) {
2262         activeEditor.statusError(tr("You can't import a folder that contains your sketchbook"));
2263         return;
2264       }
2265 
2266       String libName = libFolder.getName();
2267       if (!BaseNoGui.isSanitaryName(libName)) {
2268         String mess = I18n.format(tr("The library \"{0}\" cannot be used.\n"
2269                         + "Library names must contain only basic letters and numbers.\n"
2270                         + "(ASCII only and no spaces, and it cannot start with a number)"),
2271                 libName);
2272         activeEditor.statusError(mess);
2273         return;
2274       }
2275 
2276       String[] headers;
2277       if (new File(libFolder, "library.properties").exists()) {
2278         headers = BaseNoGui.headerListFromIncludePath(UserLibrary.create(libFolder).getSrcFolder());
2279       } else {
2280         headers = BaseNoGui.headerListFromIncludePath(libFolder);
2281       }
2282       if (headers.length == 0) {
2283         activeEditor.statusError(tr("Specified folder/zip file does not contain a valid library"));
2284         return;
2285       }
2286 
2287       // copy folder
2288       File destinationFolder = new File(BaseNoGui.getSketchbookLibrariesFolder(), sourceFile.getName());
2289       if (!destinationFolder.mkdir()) {
2290         activeEditor.statusError(I18n.format(tr("A library named {0} already exists"), sourceFile.getName()));
2291         return;
2292       }
2293       try {
2294         FileUtils.copy(sourceFile, destinationFolder);
2295       } catch (IOException e) {
2296         activeEditor.statusError(e);
2297         return;
2298       }
2299       activeEditor.statusNotice(tr("Library added to your libraries. Check \"Include library\" menu"));
2300     } catch (IOException e) {
2301       // FIXME error when importing. ignoring :(
2302     } finally {
2303       // delete zip created temp folder, if exists
2304       newLibraryImported = true;
2305       FileUtils.recursiveDelete(tmpFolder);
2306     }
2307   }
2308 
getDiscoveryManager()2309   public static DiscoveryManager getDiscoveryManager() {
2310     return BaseNoGui.getDiscoveryManager();
2311   }
2312 
getActiveEditor()2313   public Editor getActiveEditor() {
2314     return activeEditor;
2315   }
2316 
hasActiveEditor()2317   public boolean hasActiveEditor() {
2318     return activeEditor != null;
2319   }
2320 
getEditors()2321   public List<Editor> getEditors() {
2322     return new LinkedList<>(editors);
2323   }
2324 
getPdeKeywords()2325   public PdeKeywords getPdeKeywords() {
2326     return pdeKeywords;
2327   }
2328 
getRecentSketchesMenuItems()2329   public List<JMenuItem> getRecentSketchesMenuItems() {
2330     return recentSketchesMenuItems;
2331   }
2332 
2333 }
2334