diff --git a/app/src/processing/app/Base.java b/app/src/processing/app/Base.java index 49a8625e5..3c57ce5ef 100644 --- a/app/src/processing/app/Base.java +++ b/app/src/processing/app/Base.java @@ -1,5 +1,25 @@ /* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2012-23 The Processing Foundation + Copyright (c) 2004-12 Ben Fry and Casey Reas + Copyright (c) 2001-04 Massachusetts Institute of Technology + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + version 2, as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software Foundation, + Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 /* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + /* Part of the Processing project - http://processing.org @@ -60,7 +80,6 @@ public class Base { static private final int REVISION = Integer.parseInt(System.getProperty("processing.revision", "1295")); /** * This might be replaced by main() if there's a lib/version.txt file. - * */ static private String VERSION_NAME = System.getProperty("processing.version", "1295"); @@ -74,7 +93,6 @@ public class Base { */ static public boolean DEBUG = Boolean.parseBoolean(System.getenv().getOrDefault("DEBUG", "false")); - /** * is Processing being run from the command line (true) or from the GUI (false)? */ @@ -95,7 +113,7 @@ public class Base { /** List of currently active editors. */ final protected List editors = - Collections.synchronizedList(new ArrayList<>()); + Collections.synchronizedList(new ArrayList<>()); protected Editor activeEditor; /** A lone file menu to be used when all sketch windows are closed. */ @@ -111,14 +129,12 @@ public class Base { /** Only one built-in Mode these days, removing the extra fluff. */ private Mode coreMode; - // TODO can these be Set objects, or are they expected to be in order? private List contribModes; private List contribExamples; /** These aren't even dynamically loaded, they're hard-wired here. */ private List internalTools; - // TODO can these be Set objects, or are they expected to be in order? private List coreTools; private List contribTools; @@ -126,14 +142,12 @@ public class Base { private int updatesAvailable = 0; // Used by handleOpen(), this saves the chooser to remember the directory. - // Doesn't appear to be necessary with the AWT native file dialog. - // https://github.com/processing/processing/pull/2366 private JFileChooser openChooser; static public void main(final String[] args) { Messages.log("Starting Processing version" + VERSION_NAME + " revision "+ REVISION); EventQueue.invokeLater(() -> { - run(args); + run(args); }); } @@ -145,160 +159,101 @@ private static void run(String[] args) { try { createAndShowGUI(args); } catch (Throwable t) { - // Windows Defender has been insisting on destroying each new - // release by removing core.jar and other files. Yay! - // https://github.com/processing/processing/issues/5537 if (Platform.isWindows()) { String mess = t.getMessage(); String missing = null; if (mess.contains("Could not initialize class com.sun.jna.Native")) { - //noinspection SpellCheckingInspection missing = "jnidispatch.dll"; } else if (t instanceof NoClassDefFoundError && - mess.contains("processing/core/PApplet")) { - // Had to change how this was called - // https://github.com/processing/processing4/issues/154 + mess.contains("processing/core/PApplet")) { missing = "core.jar"; } if (missing != null) { Messages.showError("Necessary files are missing", - "A file required by Processing (" + missing + ") is missing.\n\n" + - "Make sure that you're not trying to run Processing from inside\n" + - "the .zip file you downloaded, and check that Windows Defender\n" + - "has not removed files from the Processing folder.\n\n" + - "(Defender sometimes flags parts of Processing as malware.\n" + - "It is not, but Microsoft has ignored our pleas for help.)", t); + "A file required by Processing (" + missing + ") is missing.\n\n" + + "Make sure that you're not trying to run Processing from inside\n" + + "the .zip file you downloaded, and check that Windows Defender\n" + + "has not removed files from the Processing folder.\n\n" + + "(Defender sometimes flags parts of Processing as malware.\n" + + "It is not, but Microsoft has ignored our pleas for help.)", t); } } Messages.showTrace("Unknown Problem", - "A serious error happened during startup. Please report:\n" + - "http://github.com/processing/processing4/issues/new", t, true); + "A serious error happened during startup. Please report:\n" + + "http://github.com/processing/processing4/issues/new", t, true); } } static private void createAndShowGUI(String[] args) { checkVersion(); - checkPortable(); - Platform.init(); - // call after Platform.init() because we need the settings folder Console.startup(); - - // Don't put anything above this line that might make GUI, - // because the platform has to be inited properly first. - - // Make sure a full JDK is installed - //initRequirements(); - - // Load the languages Language.init(); - - // run static initialization that grabs all the prefs Preferences.init(); + PreferencesEvents.onUpdated(Preferences::init); - PreferencesEvents.onUpdated(Preferences::init); - - // boolean flag indicating whether to create new server instance or not boolean createNewInstance = DEBUG || !SingleInstance.alreadyRunning(args); - - // free up resources by terminating the JVM - if(!createNewInstance){ + if (!createNewInstance) { System.exit(0); return; } - - // Set the look and feel before opening the window setLookAndFeel(); - - // Get the sketchbook path, and make sure it's set properly locateSketchbookFolder(); - - // Load colors for UI elements. This must happen after Preferences.init() - // (so that fonts are set) and locateSketchbookFolder() so that a - // theme.txt file in the user's sketchbook folder is picked up. Theme.init(); - - // Create a location for untitled sketches setupUntitleSketches(); Messages.log("About to create Base..."); try { - final Base base = new Base(args); - base.updateTheme(); - Messages.log("Base() constructor succeeded"); - - // Prevent more than one copy of the PDE from running. - SingleInstance.startServer(base); - - handleWelcomeScreen(base); - handleCrustyDisplay(); - handleTempCleaning(); - + final Base base = new Base(args); + base.updateTheme(); + Messages.log("Base() constructor succeeded"); + SingleInstance.startServer(base); + handleWelcomeScreen(base); + handleCrustyDisplay(); + handleTempCleaning(); } catch (Throwable t) { - // Catch-all to pick up badness during startup. - Throwable err = t; - if (t.getCause() != null) { - // Usually this is the more important piece of information. We'll - // show this one so that it's not truncated in the error window. - err = t.getCause(); - } - Messages.showTrace("We're off on the wrong foot", - "An error occurred during startup.", err, true); + Throwable err = t; + if (t.getCause() != null) { + err = t.getCause(); + } + Messages.showTrace("We're off on the wrong foot", + "An error occurred during startup.", err, true); } Messages.log("Done creating Base..."); } private static void setupUntitleSketches() { try { - // Users on a shared machine may also share a TEMP folder, - // which can cause naming collisions; use a UUID as the name - // for the subfolder to introduce another layer of indirection. - // https://github.com/processing/processing4/issues/549 - // The UUID also prevents collisions when restarting the - // software. Otherwise, after using up the a-z naming options - // it was not possible for users to restart (without manually - // finding and deleting the TEMP files). - // https://github.com/processing/processing4/issues/582 - String uuid = UUID.randomUUID().toString(); - untitledFolder = new File(Util.getProcessingTemp(), uuid); - + String uuid = UUID.randomUUID().toString(); + untitledFolder = new File(Util.getProcessingTemp(), uuid); } catch (IOException e) { - Messages.showError("Trouble without a name", - "Could not create a place to store untitled sketches.\n" + - "That's gonna prevent us from continuing.", e); + Messages.showError("Trouble without a name", + "Could not create a place to store untitled sketches.\n" + + "That's gonna prevent us from continuing.", e); } } private static void setLookAndFeel() { - try { - // Use native popups to avoid looking crappy on macOS - JPopupMenu.setDefaultLightWeightPopupEnabled(false); - - Platform.setLookAndFeel(); - Platform.setInterfaceZoom(); - } catch (Exception e) { - Messages.err("Error while setting up the interface", e); //$NON-NLS-1$ - } + try { + JPopupMenu.setDefaultLightWeightPopupEnabled(false); + Platform.setLookAndFeel(); + Platform.setInterfaceZoom(); + } catch (Exception e) { + Messages.err("Error while setting up the interface", e); + } } public void updateTheme() { try { FlatLaf laf = "dark".equals(Theme.get("laf.mode")) ? - new FlatDarkLaf() : new FlatLightLaf(); + new FlatDarkLaf() : new FlatLightLaf(); laf.setExtraDefaults(Collections.singletonMap("@accentColor", - Theme.get("laf.accent.color"))); - //UIManager.setLookAndFeel(laf); + Theme.get("laf.accent.color"))); FlatLaf.setup(laf); - // updateUI() will wipe out our custom components - // even if we do a setUI() call and invalidate/revalidate/repaint -// FlatLaf.updateUI(); -// System.out.println("FlatLaf.updateUI() should be done"); -// FlatLaf.updateUILater(); - } catch (Exception e) { e.printStackTrace(); } @@ -310,50 +265,24 @@ public void updateTheme() { ContributionManager.updateTheme(); for (Editor editor : getEditors()) { editor.updateTheme(); - -// Component sb = editor.getPdeTextArea(); -// sb.invalidate(); -// sb.revalidate(); -// sb.repaint(); } - - /* - Window[] windows = Window.getWindows(); - for (Window w : windows) { - SwingUtilities.updateComponentTreeUI(w); - } - */ } static private void handleWelcomeScreen(Base base) { - // Needs to be shown after the first editor window opens, so that it - // shows up on top, and doesn't prevent an editor window from opening. if (Preferences.getBoolean("welcome.four.show")) { - PDEWelcomeKt.showWelcomeScreen(base); + PDEWelcomeKt.showWelcomeScreen(base); } } - /** - * Temporary workaround as we try to sort out - * 231 - * and 226. - */ static private void handleCrustyDisplay() { - /* - System.out.println("retina is " + Toolkit.isRetina()); - System.out.println("system zoom " + Platform.getSystemZoom()); - System.out.println("java2d param is " + System.getProperty("sun.java2d.uiScale.enabled")); - System.out.println("toolkit res is " + java.awt.Toolkit.getDefaultToolkit().getScreenResolution()); - */ - if (Platform.isWindows()) { // only an issue on Windows + if (Platform.isWindows()) { if (!Toolkit.isRetina() && !Splash.getDisableHiDPI()) { int res = java.awt.Toolkit.getDefaultToolkit().getScreenResolution(); if (res % 96 != 0) { - // fractional dpi scaling on a low-res screen System.out.println("If the editor cursor is in the wrong place or the interface is blocky or fuzzy,"); - System.out.println("open Preferences and select the “Disable HiDPI Scaling” option to fix it."); + System.out.println("open Preferences and select the \u201cDisable HiDPI Scaling\u201d option to fix it."); } } } @@ -368,10 +297,6 @@ static private void handleTempCleaning() { } - /** - * Clean folders and files from the Processing subdirectory - * of the user's temp folder (java.io.tmpdir). - */ static public void cleanTempFolders() { try { final File tempDir = Util.getProcessingTemp(); @@ -381,12 +306,10 @@ static public void cleanTempFolders() { final long now = new Date().getTime(); final long diff = days * 24 * 60 * 60 * 1000L; File[] expiredFiles = - tempDir.listFiles(file -> (now - file.lastModified()) > diff); + tempDir.listFiles(file -> (now - file.lastModified()) > diff); if (expiredFiles != null) { - // Remove the files approved for deletion for (File file : expiredFiles) { try { - // move to trash or delete if unavailable Platform.deleteFile(file); } catch (IOException e) { e.printStackTrace(); @@ -399,137 +322,91 @@ static public void cleanTempFolders() { } } - /** - * Check for a version.txt file in the lib folder to override - */ - private static void checkVersion() { - File versionFile = Platform.getContentFile("lib/version.txt"); - if (versionFile != null && versionFile.exists()) { - String[] lines = PApplet.loadStrings(versionFile); - if (lines != null && lines.length > 0) { - if (!VERSION_NAME.equals(lines[0])) { - VERSION_NAME = lines[0]; - } - } + private static void checkVersion() { + File versionFile = Platform.getContentFile("lib/version.txt"); + if (versionFile != null && versionFile.exists()) { + String[] lines = PApplet.loadStrings(versionFile); + if (lines != null && lines.length > 0) { + if (!VERSION_NAME.equals(lines[0])) { + VERSION_NAME = lines[0]; } + } } + } - /** - * Check for portable settings.txt file in the lib folder - * to override the location of the settings folder. - */ - static void checkPortable() { - // Detect settings.txt in the lib folder for portable versions - File settingsFile = Platform.getContentFile("lib/settings.txt"); - if (settingsFile != null && settingsFile.exists()) { - try { - Settings portable = new Settings(settingsFile); - String path = portable.get("settings.path"); - File folder = new File(path); - boolean success = true; - if (!folder.exists()) { - success = folder.mkdirs(); - if (!success) { - Messages.err("Could not create " + folder + " to store settings."); - } - } - if (success) { - if (!folder.canRead()) { - Messages.err("Cannot read from " + folder); - } else if (!folder.canWrite()) { - Messages.err("Cannot write to " + folder); - } else { - settingsOverride = folder.getAbsoluteFile(); - } - } - } catch (IOException e) { - Messages.err("Error while reading the settings.txt file", e); - } + static void checkPortable() { + File settingsFile = Platform.getContentFile("lib/settings.txt"); + if (settingsFile != null && settingsFile.exists()) { + try { + Settings portable = new Settings(settingsFile); + String path = portable.get("settings.path"); + File folder = new File(path); + boolean success = true; + if (!folder.exists()) { + success = folder.mkdirs(); + if (!success) { + Messages.err("Could not create " + folder + " to store settings."); + } + } + if (success) { + if (!folder.canRead()) { + Messages.err("Cannot read from " + folder); + } else if (!folder.canWrite()) { + Messages.err("Cannot write to " + folder); + } else { + settingsOverride = folder.getAbsoluteFile(); + } } + } catch (IOException e) { + Messages.err("Error while reading the settings.txt file", e); + } } + } // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . - - /** - * @return the current revision number, safe to be used for update checks - */ static public int getRevision() { return REVISION; } - - /** - * @return something like 2.2.1 or 3.0b4 (or 0213 if it's not a release) - */ static public String getVersionName() { return VERSION_NAME; } - - public static void setCommandLine() { commandLine = true; } - static public boolean isCommandLine() { return commandLine; } - // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . public Base(String[] args) throws Exception { ContributionManager.init(this); - buildCoreModes(); rebuildContribModes(); rebuildContribExamples(); - rebuildToolList(); - - // Needs to happen after the sketchbook folder has been located. - // Also relies on the modes to be loaded, so it knows what can be - // marked as an example. + rebuildToolList(); Recent.init(this); - setupNextMode(); - - //contributionManagerFrame = new ContributionManagerDialog(); - - // Make sure ThinkDifferent has library examples too nextMode.rebuildLibraryList(); - - // Put this after loading the examples, so that building the default file - // menu works on Mac OS X (since it needs examplesFolder to be set). Platform.initBase(this); - - // check for updates UpdateCheck.doCheck(this); - - ContributionListing cl = ContributionListing.getInstance(); cl.downloadAvailableList(this, new ContribProgress(null)); - openFilesOrNew(args); - } private void openFilesOrNew(String[] args) { - // Check if there were previously opened sketches to be restored - // boolean opened = restoreSketches(); boolean opened = false; - // Check if any files were passed in on the command line for (int i = 0; i < args.length; i++) { Messages.logf("Parsing command line... args[%d] = '%s'", i, args[i]); String path = args[i]; - // Fix a problem with systems that use a non-ASCII languages. Paths are - // being passed in with 8.3 syntax, which makes the sketch loader code - // unhappy, since the sketch folder naming doesn't match up correctly. - // https://download.processing.org/bugzilla/1089.html if (Platform.isWindows()) { try { File file = new File(args[i]); @@ -544,7 +421,6 @@ private void openFilesOrNew(String[] args) { } } - // Create a new empty window (will be replaced with any files to be opened) if (!opened) { Messages.log("Calling handleNew() to open a new window"); handleNew(); @@ -554,31 +430,27 @@ private void openFilesOrNew(String[] args) { } private void setupNextMode() { - String lastModeIdentifier = Preferences.get("mode.last"); //$NON-NLS-1$ + String lastModeIdentifier = Preferences.get("mode.last"); if (lastModeIdentifier == null) { nextMode = getDefaultMode(); - Messages.log("Nothing set for last.sketch.mode, using default."); //$NON-NLS-1$ + Messages.log("Nothing set for last.sketch.mode, using default."); } else { for (Mode m : getModeList()) { if (m.getIdentifier().equals(lastModeIdentifier)) { - Messages.logf("Setting next mode to %s.", lastModeIdentifier); //$NON-NLS-1$ + Messages.logf("Setting next mode to %s.", lastModeIdentifier); nextMode = m; } } if (nextMode == null) { nextMode = getDefaultMode(); - Messages.logf("Could not find mode %s, using default.", lastModeIdentifier); //$NON-NLS-1$ + Messages.logf("Could not find mode %s, using default.", lastModeIdentifier); } } } - // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . - /** - * Limited file menu to be used on OS X when no sketches are open. - */ public JMenu initDefaultFileMenu() { defaultFileMenu = new JMenu(Language.text("menu.file")); @@ -601,55 +473,30 @@ public JMenu initDefaultFileMenu() { return defaultFileMenu; } - // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . - /** - * Tools require an 'Editor' object when they're instantiated, but - * the activeEditor will be null when the first Editor that opens is - * creating its Tools menu. This will temporarily set the activeEditor - * to the one that's opening so that we don't go all NPE on startup. - * If there's already an active editor, then this does nothing. - */ public void checkFirstEditor(Editor editor) { if (activeEditor == null) { activeEditor = editor; } } - - /** Returns the front most, active editor window. */ public Editor getActiveEditor() { return activeEditor; } - - /** Get the list of currently active editor windows. */ public List getEditors() { return editors; } - - /** - * Called when a window is activated. Because of variations in native - * windowing systems, no guarantees about changes to the focused and active - * Windows can be made. Never assume that this Window is the focused or - * active Window until this Window actually receives a WINDOW_GAINED_FOCUS - * or WINDOW_ACTIVATED event. - */ public void handleActivated(Editor whichEditor) { activeEditor = whichEditor; - - // set the current window to be the console that's getting output EditorConsole.setEditor(activeEditor); - - // make this the next mode to be loaded nextMode = whichEditor.getMode(); - Preferences.set("mode.last", nextMode.getIdentifier()); //$NON-NLS-1$ + Preferences.set("mode.last", nextMode.getIdentifier()); } - // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . @@ -658,19 +505,16 @@ public void refreshContribs(ContributionType ct) { for (Mode m : getModeList()) { m.rebuildImportMenu(); } - } else if (ct == ContributionType.MODE) { rebuildContribModes(); for (Editor editor : editors) { editor.rebuildModePopup(); } - } else if (ct == ContributionType.TOOL) { rebuildToolList(); for (Editor editor : editors) { populateToolsMenu(editor.getToolMenu()); } - } else if (ct == ContributionType.EXAMPLES) { rebuildContribExamples(); for (Mode m : getModeList()) { @@ -680,11 +524,6 @@ public void refreshContribs(ContributionType ct) { } - /** - * Get all the contributed Modes, Libraries, Tools, and Examples. - * Used by the Contribution Manager to report what's installed while - * checking for updates and available contributions. - */ public Set getInstalledContribs() { List modeContribs = getContribModes(); Set contributions = new HashSet<>(modeContribs); @@ -702,11 +541,6 @@ public Set getInstalledContribs() { public void tallyUpdatesAvailable() { - // Significant rewrite from previous version seen in - // https://github.com/processing/processing4/commit/a2e8cd7 - // Also counting all updates, not just those for the current Mode. - // (Too confusing otherwise, having different counts.) - Set installed = getInstalledContribs(); ContributionListing listing = ContributionListing.getInstance(); @@ -725,7 +559,6 @@ public void tallyUpdatesAvailable() { } } - // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . @@ -743,16 +576,13 @@ public List getModeList() { void buildCoreModes() { ModeContribution javaModeContrib = - ModeContribution.load(this, Platform.getContentFile("modes/java"), - getDefaultModeIdentifier()); + ModeContribution.load(this, Platform.getContentFile("modes/java"), + getDefaultModeIdentifier()); if (javaModeContrib == null) { Messages.showError("Startup Error", - "Could not load Java Mode, please reinstall Processing.", - new Exception("ModeContribution.load() was null")); - + "Could not load Java Mode, please reinstall Processing.", + new Exception("ModeContribution.load() was null")); } else { - // PDE X calls getModeList() while it's loading, so coreModes must be set - //coreModes = new Mode[] { javaModeContrib.getMode() }; coreMode = javaModeContrib.getMode(); } } @@ -763,11 +593,6 @@ public List getContribModes() { } - /** - * Instantiates and adds new contributed modes to the contribModes list. - * Checks for duplicates so the same mode isn't instantiated twice. Does not - * remove modes because modes can't be removed once they are instantiated. - */ void rebuildContribModes() { if (contribModes == null) { contribModes = new ArrayList<>(); @@ -778,8 +603,6 @@ void rebuildContribModes() { known.put(contrib.getFolder(), contrib); } File[] potential = ContributionType.MODE.listCandidates(modesFolder); - // If modesFolder does not exist or is inaccessible (folks might like to - // mess with folders then report it as a bug) 'potential' will be null. if (potential != null) { for (File folder : potential) { if (!known.containsKey(folder)) { @@ -799,13 +622,11 @@ void rebuildContribModes() { e.printStackTrace(); } } else { - known.remove(folder); // remove this item as already been seen + known.remove(folder); } } } - // This allows you to build and test a Mode from Eclipse - // -Dusemode=com.foo.FrobMode:/path/to/FrobMode final String useMode = System.getProperty("usemode"); if (useMode != null) { final String[] modeInfo = useMode.split(":", 2); @@ -837,7 +658,6 @@ static private File getModeContribFile(ModeContribution contrib, return null; } - // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . @@ -845,29 +665,23 @@ public List getCoreTools() { return coreTools; } - public List getContribTools() { return contribTools; } public void rebuildToolList() { - // Only do these once because the list of internal tools will never change if (internalTools == null) { internalTools = new ArrayList<>(); - initInternalTool(processing.app.tools.Archiver.class); initInternalTool(processing.app.tools.ColorSelector.class); initInternalTool(processing.app.tools.CreateFont.class); - if (Platform.isMacOS()) { initInternalTool(processing.app.tools.InstallCommander.class); } - initInternalTool(processing.app.tools.ThemeSelector.class); } - // Only init() these the first time they're loaded if (coreTools == null) { coreTools = ToolContribution.loadAll(Base.getToolsFolder()); for (Tool tool : coreTools) { @@ -875,33 +689,24 @@ public void rebuildToolList() { } } - // Reset the contributed tools and re-init() all of them. contribTools = ToolContribution.loadAll(Base.getSketchbookToolsFolder()); for (Tool tool : contribTools) { try { tool.init(this); - - // With the exceptions, we can't call statusError because the window - // isn't completely set up yet. Also not gonna pop up a warning because - // people may still be running different versions of Processing. - } catch (VerifyError | AbstractMethodError ve) { System.err.println("\"" + tool.getMenuTitle() + "\" is not " + - "compatible with this version of Processing"); + "compatible with this version of Processing"); Messages.err("Incompatible Tool found during tool.init()", ve); - } catch (NoSuchMethodError nsme) { System.err.println("\"" + tool.getMenuTitle() + "\" is not " + - "compatible with this version of Processing"); + "compatible with this version of Processing"); System.err.println("The " + nsme.getMessage() + " method no longer exists."); Messages.err("Incompatible Tool found during tool.init()", nsme); - } catch (NoClassDefFoundError ncdfe) { System.err.println("\"" + tool.getMenuTitle() + "\" is not " + - "compatible with this version of Processing"); + "compatible with this version of Processing"); System.err.println("The " + ncdfe.getMessage() + " class is no longer available."); Messages.err("Incompatible Tool found during tool.init()", ncdfe); - } catch (Error | Exception e) { System.err.println("An error occurred inside \"" + tool.getMenuTitle() + "\""); e.printStackTrace(); @@ -913,11 +718,9 @@ public void rebuildToolList() { protected void initInternalTool(Class toolClass) { try { final Tool tool = (Tool) - toolClass.getDeclaredConstructor().newInstance(); - + toolClass.getDeclaredConstructor().newInstance(); tool.init(this); internalTools.add(tool); - } catch (Exception e) { e.printStackTrace(); } @@ -932,7 +735,6 @@ public void clearToolMenus() { public void populateToolsMenu(JMenu toolsMenu) { - // If this is the first run, need to build out the lists if (internalTools == null) { rebuildToolList(); } @@ -958,69 +760,57 @@ public void populateToolsMenu(JMenu toolsMenu) { } JMenuItem manageTools = - new JMenuItem(Language.text("menu.tools.manage_tools")); + new JMenuItem(Language.text("menu.tools.manage_tools")); manageTools.addActionListener(e -> ContributionManager.openTools()); toolsMenu.add(manageTools); } - JMenuItem createToolItem(final Tool tool) { //, Map toolItems) { + JMenuItem createToolItem(final Tool tool) { String title = tool.getMenuTitle(); final JMenuItem item = new JMenuItem(title); item.addActionListener(e -> { try { tool.run(); - } catch (NoSuchMethodError | NoClassDefFoundError ne) { Messages.showWarning("Tool out of date", - tool.getMenuTitle() + " is not compatible with this version of Processing.\n" + - "Try updating the Mode or contact its author for a new version.", ne); + tool.getMenuTitle() + " is not compatible with this version of Processing.\n" + + "Try updating the Mode or contact its author for a new version.", ne); Messages.err("Incompatible tool found during tool.run()", ne); item.setEnabled(false); - } catch (Exception ex) { activeEditor.statusError("An error occurred inside \"" + tool.getMenuTitle() + "\""); ex.printStackTrace(); item.setEnabled(false); } }); - //toolItems.put(title, item); return item; } - // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . void rebuildContribExamples() { contribExamples = - ExamplesContribution.loadAll(getSketchbookExamplesFolder()); + ExamplesContribution.loadAll(getSketchbookExamplesFolder()); } - public List getContribExamples() { return contribExamples; } - // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . String getDefaultModeIdentifier() { - // Used to initialize coreModes, so cannot use coreModes[0].getIdentifier() return "processing.mode.java.JavaMode"; } - public Mode getDefaultMode() { - //return coreModes[0]; return coreMode; } - /** - * @return true if mode is changed within this window (false if new window) - */ public boolean changeMode(Mode mode) { Mode oldMode = activeEditor.getMode(); if (oldMode != mode) { @@ -1028,43 +818,27 @@ public boolean changeMode(Mode mode) { nextMode = mode; if (sketch.isModified()) { - // If sketch is modified, simpler to just open a new window. - // https://github.com/processing/processing4/issues/189 handleNew(); return false; - } else if (sketch.isUntitled()) { - // The current sketch is empty, just close and start fresh. - // (Otherwise the editor would lose its 'untitled' status.) - // Safe to do here because of the 'modified' check above. handleClose(activeEditor, true); handleNew(); - } else { - // If the current sketch contains file extensions that the new Mode - // can handle, then write a sketch.properties file with that Mode - // specified, and reopen. Currently, only used for Java <-> Android. if (mode.canEdit(sketch)) { - //final File props = new File(sketch.getFolder(), "sketch.properties"); - //saveModeSettings(props, nextMode); sketch.updateModeProperties(nextMode, getDefaultMode()); handleClose(activeEditor, true); Editor editor = handleOpen(sketch.getMainPath()); if (editor == null) { - // the Mode change failed (probably code that's out of date) - // re-open the sketch using the mode we were in before - //saveModeSettings(props, oldMode); sketch.updateModeProperties(oldMode, getDefaultMode()); handleOpen(sketch.getMainPath()); return false; } } else { - handleNew(); // create a new window with the new Mode + handleNew(); return false; } } } - // Against all (or at least most) odds, we were able to reassign the Mode return true; } @@ -1078,17 +852,12 @@ protected Mode findMode(String id) { return null; } - - /** - * Called when a Mode is uninstalled, in case it's the current Mode. - */ public void modeRemoved(Mode mode) { if (nextMode == mode) { nextMode = getDefaultMode(); } } - // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . @@ -1096,29 +865,17 @@ public void modeRemoved(Mode mode) { * Create a new untitled document in a new sketch window. */ public void handleNew() { -// long t1 = System.currentTimeMillis(); try { - // In 0126, untitled sketches will begin in the temp folder, - // and then moved to a new location because Save will default to Save As. - //File sketchbookDir = getSketchbookFolder(); File newbieDir = SketchName.nextFolder(untitledFolder); - - // User was told to go outside or other problem happened inside naming. if (newbieDir == null) return; - // Make the directory for the new sketch if (!newbieDir.mkdirs()) { throw new IOException("Could not create directory " + newbieDir); } - // Retrieve the sketch name from the folder name (not a great - // assumption for the future, but overkill to do otherwise for now.) String newbieName = newbieDir.getName(); - - // Add any template files from the Mode itself File newbieFile = nextMode.addTemplateFiles(newbieDir, newbieName); - // Create sketch properties file if it's not the default mode. if (!nextMode.equals(getDefaultMode())) { Sketch.updateModeProperties(newbieDir, nextMode, getDefaultMode()); } @@ -1128,8 +885,8 @@ public void handleNew() { } catch (IOException e) { Messages.showTrace("That's new to me", - "A strange and unexplainable error occurred\n" + - "while trying to create a new sketch.", e, false); + "A strange and unexplainable error occurred\n" + + "while trying to create a new sketch.", e, false); } } @@ -1139,28 +896,21 @@ public void handleNew() { */ public void handleOpenPrompt() { final StringList extensions = new StringList(); - - // Add support for pdez files extensions.append(SKETCH_BUNDLE_EXT); - // Add the extensions for each installed Mode for (Mode mode : getModeList()) { extensions.append(mode.getDefaultExtension()); - // not adding aux extensions b/c we're looking for the main } final String prompt = Language.text("open"); - if (Preferences.getBoolean("chooser.files.native")) { //$NON-NLS-1$ - // use the front-most window frame for placing file dialog + if (Preferences.getBoolean("chooser.files.native")) { FileDialog openDialog = - new FileDialog(activeEditor, prompt, FileDialog.LOAD); + new FileDialog(activeEditor, prompt, FileDialog.LOAD); - // Only show .pde files as eligible bachelors openDialog.setFilenameFilter((dir, name) -> { - // confirmed to be working properly [fry 110128] for (String ext : extensions) { - if (name.toLowerCase().endsWith("." + ext)) { //$NON-NLS-1$ + if (name.toLowerCase().endsWith("." + ext)) { return true; } } @@ -1184,14 +934,11 @@ public void handleOpenPrompt() { openChooser.setFileFilter(new javax.swing.filechooser.FileFilter() { public boolean accept(File file) { - // JFileChooser requires you to explicitly say yes to directories - // as well (unlike the AWT chooser). Useful, but... different. - // https://github.com/processing/processing/issues/1189 if (file.isDirectory()) { return true; } for (String ext : extensions) { - if (file.getName().toLowerCase().endsWith("." + ext)) { //$NON-NLS-1$ + if (file.getName().toLowerCase().endsWith("." + ext)) { return true; } } @@ -1215,7 +962,6 @@ private Editor openSketchBundle(String path) { untitledFolder.mkdirs(); File destFolder = File.createTempFile("zip", "tmp", untitledFolder); if (!destFolder.delete() || !destFolder.mkdirs()) { - // Hard to imagine why this would happen, but... System.err.println("Could not create temporary folder " + destFolder); return null; } @@ -1229,7 +975,7 @@ private Editor openSketchBundle(String path) { } } else { System.err.println("Expecting one folder inside " + - SKETCH_BUNDLE_EXT + " file, found " + fileList.length + "."); + SKETCH_BUNDLE_EXT + " file, found " + fileList.length + "."); } } else { System.err.println("Could not read " + destFolder); @@ -1237,7 +983,7 @@ private Editor openSketchBundle(String path) { } catch (IOException e) { e.printStackTrace(); } - return null; // no luck + return null; } @@ -1245,18 +991,17 @@ private void openContribBundle(String path) { EventQueue.invokeLater(() -> { Editor editor = getActiveEditor(); if (editor == null) { - // Shouldn't really happen, but if it's still null, it's a no-go Messages.showWarning("Failure is the only option", - "Please open an Editor window before installing an extension."); + "Please open an Editor window before installing an extension."); } else { File contribFile = new File(path); String baseName = contribFile.getName(); baseName = baseName.substring(0, baseName.length() - CONTRIB_BUNDLE_EXT.length()); int result = - Messages.showYesNoQuestion(editor, "How to Handle " + CONTRIB_BUNDLE_EXT, - "Install " + baseName + "?", - "Libraries, Modes, and Tools should
" + - "only be installed from trusted sources."); + Messages.showYesNoQuestion(editor, "How to Handle " + CONTRIB_BUNDLE_EXT, + "Install " + baseName + "?", + "Libraries, Modes, and Tools should
" + + "only be installed from trusted sources."); if (result == JOptionPane.YES_OPTION) { editor.statusNotice("Installing " + baseName + "..."); @@ -1264,13 +1009,11 @@ private void openContribBundle(String path) { new Thread(() -> { try { - // do the work of the actual install LocalContribution contrib = - AvailableContribution.install(this, new File(path)); + AvailableContribution.install(this, new File(path)); EventQueue.invokeLater(() -> { editor.stopIndeterminate(); - if (contrib != null) { editor.statusEmpty(); } else { @@ -1279,8 +1022,8 @@ private void openContribBundle(String path) { }); } catch (IOException e) { EventQueue.invokeLater(() -> - Messages.showWarning("Exception During Installation", - "Could not install contrib from " + path, e)); + Messages.showWarning("Exception During Installation", + "Could not install contrib from " + path, e)); } }).start(); } @@ -1289,20 +1032,16 @@ private void openContribBundle(String path) { } - /** - * Return true if it's an obvious sketch folder: only .pde files, - * and maybe a data folder. Dot files (.DS_Store, ._blah) are ignored. - */ private boolean smellsLikeSketchFolder(File folder) { File[] files = folder.listFiles(); - if (files == null) { // unreadable, assume badness + if (files == null) { return false; } for (File file : files) { String name = file.getName(); if (!(name.startsWith(".") || - name.toLowerCase().endsWith(".pde")) || - (file.isDirectory() && name.equals("data"))) { + name.toLowerCase().endsWith(".pde")) || + (file.isDirectory() && name.equals("data"))) { return false; } } @@ -1311,58 +1050,41 @@ private boolean smellsLikeSketchFolder(File folder) { private File moveLikeSketchFolder(File pdeFile, String baseName) throws IOException { - Object[] options = { - "Keep", "Move", "Cancel" - }; + Object[] options = { "Keep", "Move", "Cancel" }; String prompt = - "Would you like to keep “" + pdeFile.getParentFile().getName() + "” as the sketch folder,\n" + - "or move “" + pdeFile.getName() + "” to its own folder?\n" + - "(Usually, “" + pdeFile.getName() + "” would be stored inside a\n" + - "sketch folder named “" + baseName + "”.)"; - - int result = JOptionPane.showOptionDialog(null, - prompt, - "Keep it? Move it?", - JOptionPane.YES_NO_CANCEL_OPTION, - JOptionPane.QUESTION_MESSAGE, - null, - options, - options[0]); - - if (result == JOptionPane.YES_OPTION) { // keep - return pdeFile; + "Would you like to keep \u201c" + pdeFile.getParentFile().getName() + "\u201d as the sketch folder,\n" + + "or move \u201c" + pdeFile.getName() + "\u201d to its own folder?\n" + + "(Usually, \u201c" + pdeFile.getName() + "\u201d would be stored inside a\n" + + "sketch folder named \u201c" + baseName + "\u201d.)"; + + int result = JOptionPane.showOptionDialog(null, prompt, "Keep it? Move it?", + JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, + null, options, options[0]); - } else if (result == JOptionPane.NO_OPTION) { // move - // create properly named folder + if (result == JOptionPane.YES_OPTION) { + return pdeFile; + } else if (result == JOptionPane.NO_OPTION) { File properFolder = new File(pdeFile.getParent(), baseName); if (properFolder.exists()) { throw new IOException("A folder named \"" + baseName + "\" " + - "already exists. Cannot open sketch."); + "already exists. Cannot open sketch."); } if (!properFolder.mkdirs()) { throw new IOException("Could not create the sketch folder."); } - // copy the sketch inside File properPdeFile = new File(properFolder, pdeFile.getName()); Util.copyFile(pdeFile, properPdeFile); - - // remove the original file, so user doesn't get confused if (!pdeFile.delete()) { Messages.err("Could not delete " + pdeFile); } - - // update with the new path return properPdeFile; } - - // Catch all other cases, including Cancel or ESC return null; } /** * Handler for pde:// protocol URIs - * @param schemeUri the full URI, including pde:// */ public Editor handleScheme(String schemeUri) { var result = Schema.handleSchema(schemeUri, this); @@ -1372,19 +1094,17 @@ public Editor handleScheme(String schemeUri) { String location = schemeUri.substring(6); if (location.length() > 0) { - // if it leads with a slash, then it's a file url if (location.charAt(0) == '/') { File file = new File(location); if (file.exists()) { - handleOpen(location); // it's a full path to a file + handleOpen(location); } else { System.err.println(file + " does not exist."); } } else { - // turn it into an https url final String url = "https://" + location; if (location.toLowerCase().endsWith(".pdez") || - location.toLowerCase().endsWith(".pdex")) { + location.toLowerCase().endsWith(".pdex")) { String extension = location.substring(location.length() - 5); try { File tempFile = File.createTempFile("scheme", extension); @@ -1405,7 +1125,6 @@ public Editor handleScheme(String schemeUri) { /** * Open a sketch from the path specified. Do not use for untitled sketches. - * Note that the user may have selected/double-clicked any .pde in a sketch. */ public Editor handleOpen(String path) { if (path.startsWith("pde://")) { @@ -1414,10 +1133,9 @@ public Editor handleOpen(String path) { if (path.endsWith(SKETCH_BUNDLE_EXT)) { return openSketchBundle(path); - } else if (path.endsWith(CONTRIB_BUNDLE_EXT)) { openContribBundle(path); - return null; // never returning an Editor for a contrib + return null; } File pdeFile = new File(path); @@ -1426,15 +1144,10 @@ public Editor handleOpen(String path) { return null; } - // Cycle through open windows to make sure that it's not already open. for (Editor editor : editors) { - // User may have double-clicked any PDE in the sketch folder, - // so we have to check each open tab (not just the main one). - // https://github.com/processing/processing/issues/2506 for (SketchCode tab : editor.getSketch().getCode()) { if (tab.getFile().equals(pdeFile)) { editor.toFront(); - // move back to the top of the recent list Recent.append(editor); return editor; } @@ -1444,32 +1157,23 @@ public Editor handleOpen(String path) { File parentFolder = pdeFile.getParentFile(); try { - // read the sketch.properties file or get an empty Settings object Settings props = Sketch.loadProperties(parentFolder); if (!props.isEmpty()) { - // First check for the Mode, because it may not even be available String modeIdentifier = props.get("mode.id"); if (modeIdentifier != null) { if (modeIdentifier.equals("galsasson.mode.tweak.TweakMode")) { - // Tweak Mode has been built into Processing since 2015, - // but there are some old sketch.properties files out there. - // https://github.com/processing/processing4/issues/415 nextMode = getDefaultMode(); - - // Clean up sketch.properties and re-save or remove if necessary props.remove("mode"); props.remove("mode.id"); props.reckon(); - } else { - // sketch.properties specifies a Mode, see if it's available Mode mode = findMode(modeIdentifier); if (mode != null) { nextMode = mode; } else { ContributionManager.openModes(); Messages.showWarning("Missing Mode", - "You must first install " + props.get("mode") + " Mode to use this sketch."); + "You must first install " + props.get("mode") + " Mode to use this sketch."); return null; } } @@ -1477,107 +1181,58 @@ public Editor handleOpen(String path) { String main = props.get("main"); if (main != null) { - // this may be exactly what was passed, but override anyway String mainPath = new File(parentFolder, main).getAbsolutePath(); if (!path.equals(mainPath)) { - // for now, post a warning if the main was different System.out.println(path + " selected, but main is " + mainPath); } return handleOpenInternal(mainPath, false); } } else { - // Switch back to defaultMode, because a sketch.properties - // file is required whenever not using the default Mode. - // (Unless being called from, say, the Examples frame, which - // uses a version of this function that takes a Mode object.) nextMode = getDefaultMode(); } - // Do some checks to make sure the file can be opened, and identify the - // Mode that it's using. (In 4.0b8, this became the fall-through case.) - if (!Sketch.isSanitaryName(pdeFile.getName())) { Messages.showWarning("You're tricky, but not tricky enough", - pdeFile.getName() + " is not a valid name for sketch code.\n" + - "Better to stick to ASCII, no spaces, and make sure\n" + - "it doesn't start with a number.", null); + pdeFile.getName() + " is not a valid name for sketch code.\n" + + "Better to stick to ASCII, no spaces, and make sure\n" + + "it doesn't start with a number.", null); return null; } - // Check if the name of the file matches the parent folder name. String baseName = pdeFile.getName(); int dot = baseName.lastIndexOf('.'); if (dot == -1) { - // Shouldn't really be possible, right? System.err.println(pdeFile + " does not have an extension."); return null; } baseName = baseName.substring(0, dot); if (!baseName.equals(parentFolder.getName())) { - // Parent folder name does not match, and because sketch.properties - // did not exist or did not specify it above, need to determine main. - - // Check whether another .pde file has a matching name, and if so, - // switch to using that instead. Handles when a user selects a .pde - // file in the open dialog box that isn't the main tab. - // (Also important to use nextMode here, because the Mode - // may be set by sketch.properties when it's loaded above.) String filename = - parentFolder.getName() + "." + nextMode.getDefaultExtension(); + parentFolder.getName() + "." + nextMode.getDefaultExtension(); File mainFile = new File(parentFolder, filename); if (mainFile.exists()) { - // User was opening the wrong file in a legit sketch folder. pdeFile = mainFile; - } else if (smellsLikeSketchFolder(parentFolder)) { - // Looks like a sketch folder, set this as the main. props.set("main", pdeFile.getName()); - // Save for later use, then fall through. props.save(); - } else { - // If it's not an obvious sketch folder (just .pde files, - // maybe a data folder) prompt the user whether to - // 1) move sketch into its own folder, or - // 2) call this the main, and write sketch.properties. File newFile = moveLikeSketchFolder(pdeFile, baseName); - if (newFile == pdeFile) { - // User wanted to keep this sketch folder, so update the - // property for the main tab and write sketch.properties. props.set("main", newFile.getName()); props.save(); - } else if (newFile == null) { - // User canceled, so exit handleOpen() return null; - } else { - // User asked to move the sketch file pdeFile = newFile; } } } - // TODO Remove this selector? Seems too messy/precious. [fry 220205] - // Opting to remove in beta 7, because sketches that use another - // Mode should have a working sketch.properties. [fry 220302] - /* - // If the current Mode cannot open this file, try to find another. - if (!nextMode.canEdit(pdeFile)) { - - final Mode mode = promptForMode(pdeFile); - if (mode == null) { - return null; - } - nextMode = mode; - } - */ return handleOpenInternal(pdeFile.getAbsolutePath(), false); } catch (IOException e) { Messages.showWarning("sketch.properties", - "Error while reading sketch.properties from\n" + parentFolder, e); + "Error while reading sketch.properties from\n" + parentFolder, e); return null; } } @@ -1587,22 +1242,21 @@ public Editor handleOpen(String path) { * Open a (vetted) sketch location using a particular Mode. Used by the * Examples window, because Modes like Python and Android do not have * "sketch.properties" files in each example folder. + * The isExample flag prevents closing the initial untitled window, + * since keeping it open as a reference can be useful when browsing examples. */ public Editor handleOpenExample(String path, Mode mode) { nextMode = mode; - return handleOpenInternal(path, true); + return handleOpenInternal(path, true, true); // isExample = true } /** * Open the sketch associated with this .pde file in a new window * as an "Untitled" sketch. - * @param path Path to the pde file for the sketch in question - * @return the Editor object, so that properties (like 'untitled') - * can be set by the caller */ protected Editor handleOpenUntitled(String path) { - return handleOpenInternal(path, true); + return handleOpenInternal(path, true, false); } @@ -1611,115 +1265,118 @@ protected Editor handleOpenUntitled(String path) { * sketch file/folder must have been vetted, and nextMode set properly. */ protected Editor handleOpenInternal(String path, boolean untitled) { + return handleOpenInternal(path, untitled, false); + } + + + /** + * Internal function to actually open the sketch. + * @param path Path to the sketch file + * @param untitled Whether this sketch is untitled + * @param isExample Whether this is being opened from the Examples browser. + * If true, the initial untitled window is NOT closed, + * so it can serve as a reference while browsing examples. + * Fixes: https://github.com/processing/processing4/issues/1117 + */ + protected Editor handleOpenInternal(String path, boolean untitled, boolean isExample) { try { try { + // [fix #1117] If a real sketch is being opened (not an example, not + // a new untitled), and the only open editor is an untitled unmodified + // window, close it first so it doesn't clutter the workspace. + if (!isExample && !untitled && editors.size() == 1) { + Editor existing = editors.get(0); + if (existing.getSketch().isUntitled() && + !existing.getSketch().isModified()) { + Messages.log("Closing initial untitled window before opening: " + path); + existing.setVisible(false); + existing.dispose(); + editors.remove(existing); + activeEditor = null; + } + } + EditorState state = EditorState.nextEditor(editors); Editor editor = nextMode.createEditor(this, path, state); - // opened successfully, let's go to work editor.setUpdatesAvailable(updatesAvailable); editor.getSketch().setUntitled(untitled); editors.add(editor); Recent.append(editor); - // now that we're ready, show the window - // (don't do earlier, cuz we might move it based on a window being closed) editor.setVisible(true); return editor; } catch (EditorException ee) { - if (ee.getMessage() != null) { // null if the user canceled + if (ee.getMessage() != null) { Messages.showWarning("Error opening sketch", ee.getMessage(), ee); } } catch (NoSuchMethodError me) { Messages.showWarning("Mode out of date", - nextMode.getTitle() + " is not compatible with this version of Processing.\n" + - "Try updating the Mode or contact its author for a new version.", me); + nextMode.getTitle() + " is not compatible with this version of Processing.\n" + + "Try updating the Mode or contact its author for a new version.", me); } catch (Throwable t) { if (nextMode.equals(getDefaultMode())) { Messages.showTrace("Serious Problem", - "An unexpected, unknown, and unrecoverable error occurred\n" + - "while opening a new editor window. Please report this.", t, true); + "An unexpected, unknown, and unrecoverable error occurred\n" + + "while opening a new editor window. Please report this.", t, true); } else { Messages.showTrace("Mode Problems", - "A nasty error occurred while trying to use “" + nextMode.getTitle() + "”.\n" + - "It may not be compatible with this version of Processing.\n" + - "Try updating the Mode or contact its author for a new version.", t, false); + "A nasty error occurred while trying to use \u201c" + nextMode.getTitle() + "\u201d.\n" + + "It may not be compatible with this version of Processing.\n" + + "Try updating the Mode or contact its author for a new version.", t, false); } } if (editors.isEmpty()) { Mode defaultMode = getDefaultMode(); if (nextMode == defaultMode) { - // unreachable? hopefully? Messages.showError("Editor Problems", """ An error occurred while trying to change modes. We'll have to quit for now because it's an unfortunate bit of indigestion with the default Mode. """, null); } else { - // Don't leave the user hanging or the PDE locked up - // https://github.com/processing/processing/issues/4467 if (untitled) { nextMode = defaultMode; handleNew(); - return null; // ignored by any caller - + return null; } else { - // This null response will be kicked back to changeMode(), - // signaling it to re-open the sketch in the default Mode. return null; } } } } catch (Throwable t) { Messages.showTrace("Terrible News", - "A serious error occurred while " + - "trying to create a new editor window.", t, - nextMode == getDefaultMode()); // quit if default + "A serious error occurred while " + + "trying to create a new editor window.", t, + nextMode == getDefaultMode()); nextMode = getDefaultMode(); } return null; } - // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . - /** - * Close a sketch as specified by its editor window. - * @param editor Editor object of the sketch to be closed. - * @param preventQuit For platforms that must have a window open, - * prevent a quit because a new window will be opened - * (i.e. when upgrading or changing the Mode) - * @return true if succeeded in closing, false if canceled. - */ public boolean handleClose(Editor editor, boolean preventQuit) { if (!editor.checkModified()) { return false; } - // Close the running window, avoid window boogers with multiple sketches editor.internalCloseRunner(); if (editors.size() == 1) { if (Platform.isMacOS()) { - // If the central menu bar isn't supported on this macOS JVM, - // we have to do the old behavior. Yuck! if (defaultFileMenu == null) { Object[] options = { Language.text("prompt.ok"), Language.text("prompt.cancel") }; - int result = JOptionPane.showOptionDialog(editor, - Toolkit.formatMessage("Are you sure you want to Quit?", - "Closing the last open sketch will quit Processing."), - "Quit", - JOptionPane.YES_NO_OPTION, - JOptionPane.QUESTION_MESSAGE, - null, - options, - options[0]); + Toolkit.formatMessage("Are you sure you want to Quit?", + "Closing the last open sketch will quit Processing."), + "Quit", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, + null, options, options[0]); if (result == JOptionPane.NO_OPTION || - result == JOptionPane.CLOSED_OPTION) { + result == JOptionPane.CLOSED_OPTION) { return false; } } @@ -1727,16 +1384,14 @@ public boolean handleClose(Editor editor, boolean preventQuit) { if (defaultFileMenu == null) { if (preventQuit) { - // need to close this editor, ever so temporarily editor.setVisible(false); editor.dispose(); activeEditor = null; editors.remove(editor); } else { - // Since this wasn't an actual Quit event, call System.exit() System.exit(0); } - } else { // on OS X, update the default file menu + } else { editor.setVisible(false); editor.dispose(); defaultFileMenu.insert(Recent.getMenu(), 2); @@ -1745,8 +1400,6 @@ public boolean handleClose(Editor editor, boolean preventQuit) { } } else { - // More than one editor window open, - // proceed with closing the current window. editor.setVisible(false); editor.dispose(); editors.remove(editor); @@ -1755,32 +1408,14 @@ public boolean handleClose(Editor editor, boolean preventQuit) { } - /** - * Handler for File → Quit. Note that this is *only* for the - * File menu. On macOS, it will not call System.exit() because the - * application will handle that. If calling this from elsewhere, - * you'll need a System.exit() call on macOS. - * @return false if canceled, true otherwise. - */ public boolean handleQuit() { - // If quit is canceled, this will be replaced anyway - // by a later handleQuit() that is not canceled. -// storeSketches(); - if (handleQuitEach()) { - // make sure running sketches close before quitting for (Editor editor : editors) { editor.internalCloseRunner(); } - // Save out the current prefs state Preferences.save(); - - // Finished with this guy Console.shutdown(); - if (!Platform.isMacOS()) { - // If this was fired from the menu or an AppleEvent (the Finder), - // then Mac OS X will send the terminate signal itself. System.exit(0); } return true; @@ -1789,21 +1424,8 @@ public boolean handleQuit() { } - /** - * Attempt to close each open sketch in preparation for quitting. - * @return false if canceled along the way - */ protected boolean handleQuitEach() { -// int index = 0; for (Editor editor : editors) { -// if (editor.checkModified()) { -// // Update to the new/final sketch path for this fella -// storeSketchPath(editor, index); -// index++; -// -// } else { -// return false; -// } if (!editor.checkModified()) { return false; } @@ -1816,27 +1438,23 @@ public void handleRestart() { File app = Platform.getProcessingApp(); System.out.println(app); if (app.exists()) { - if (handleQuitEach()) { // only if everything saved + if (handleQuitEach()) { SingleInstance.clearRunning(); - - // Launch on quit Runtime.getRuntime().addShutdownHook(new Thread(() -> { try { - //Runtime.getRuntime().exec(app.getAbsolutePath()); System.out.println("launching"); Process p; if (Platform.isMacOS()) { p = Runtime.getRuntime().exec(new String[] { - // -n allows more than one instance to be opened at a time - "open", "-n", "-a", app.getAbsolutePath() + "open", "-n", "-a", app.getAbsolutePath() }); } else if (Platform.isLinux()) { p = Runtime.getRuntime().exec(new String[] { - app.getAbsolutePath() + app.getAbsolutePath() }); } else { p = Runtime.getRuntime().exec(new String[] { - "cmd", "/c", app.getAbsolutePath() + "cmd", "/c", app.getAbsolutePath() }); } System.out.println("launched with result " + p.waitFor()); @@ -1846,36 +1464,24 @@ public void handleRestart() { } })); handleQuit(); - // handleQuit() does not call System.exit() on macOS if (Platform.isMacOS()) { System.exit(0); } } } else { Messages.showWarning("Cannot Restart", - "Cannot automatically restart because the Processing\n" + - "application has been renamed. Please quit and then restart manually."); + "Cannot automatically restart because the Processing\n" + + "application has been renamed. Please quit and then restart manually."); } } - // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . -// /** -// * Asynchronous version of menu rebuild to be used on save and rename -// * to prevent the interface from locking up until the menus are done. -// */ -// protected void rebuildSketchbookMenusAsync() { -// EventQueue.invokeLater(this::rebuildSketchbookMenus); -// } - - public void showExamplesFrame() { nextMode.showExamplesFrame(); } - // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . @@ -1883,7 +1489,7 @@ public void showExamplesFrame() { public DefaultMutableTreeNode buildSketchbookTree() { DefaultMutableTreeNode sbNode = - new DefaultMutableTreeNode(Language.text("sketchbook.tree")); + new DefaultMutableTreeNode(Language.text("sketchbook.tree")); try { addSketches(sbNode, Base.getSketchbookFolder(), false); } catch (IOException e) { @@ -1892,29 +1498,12 @@ public DefaultMutableTreeNode buildSketchbookTree() { return sbNode; } - - /** Sketchbook has changed, update it on next viewing. */ public void rebuildSketchbookFrame() { if (sketchbookFrame != null) { sketchbookFrame.rebuild(); - /* - boolean visible = sketchbookFrame.isVisible(); - Rectangle bounds = null; - if (visible) { - bounds = sketchbookFrame.getBounds(); - sketchbookFrame.setVisible(false); - sketchbookFrame.dispose(); - } - sketchbookFrame = null; - if (visible) { - showSketchbookFrame(); - sketchbookFrame.setBounds(bounds); - } - */ } } - public void showSketchbookFrame() { if (sketchbookFrame == null) { sketchbookFrame = new SketchbookFrame(this); @@ -1922,21 +1511,12 @@ public void showSketchbookFrame() { sketchbookFrame.setVisible(); } - - /** - * Synchronous version of rebuild, used when the sketchbook folder has - * changed, so that the libraries are properly re-scanned before those menus - * (and the examples window) are rebuilt. - */ public void rebuildSketchbook() { for (Mode mode : getModeList()) { - mode.rebuildImportMenu(); // calls rebuildLibraryList + mode.rebuildImportMenu(); mode.rebuildToolbarMenu(); mode.rebuildExamplesFrame(); } - // Unlike libraries, examples, etc. the sketchbook is global - // (because you need to be able to open sketches from the Mode - // that you're not currently using). rebuildSketchbookFrame(); } @@ -1945,7 +1525,7 @@ public void populateSketchbookMenu(JMenu menu) { new Thread(() -> { boolean found = false; try { - found = addSketches(menu, getSketchbookFolder()); + found = addSketches(menu, getSketchbookFolder()); } catch (Exception e) { Messages.showWarning("Sketchbook Menu Error", "An error occurred while trying to list the sketchbook.", e); @@ -1959,44 +1539,22 @@ public void populateSketchbookMenu(JMenu menu) { } - /** - * Scan a folder recursively, and add any sketches found to the menu - * specified. Set the openReplaces parameter to true when opening the sketch - * should replace the sketch in the current window, or false when the - * sketch should open in a new window. - */ protected boolean addSketches(JMenu menu, File folder) { Messages.log("scanning " + folder.getAbsolutePath()); - // skip .DS_Store files, etc. (this shouldn't actually be necessary) - if (!folder.isDirectory()) { - return false; - } - - // Don't look inside the 'android' folders in the sketchbook - if (folder.getName().equals("android")) { - return false; - } - - if (folder.getName().equals("libraries")) { - return false; // let's not go there - } + if (!folder.isDirectory()) return false; + if (folder.getName().equals("android")) return false; + if (folder.getName().equals("libraries")) return false; if (folder.getName().equals("sdk")) { - // This could be Android's SDK folder. Let's double-check: File suspectSDKPath = new File(folder.getParent(), folder.getName()); - File expectedSDKPath = new File(getSketchbookFolder(), "android" + File.separator + "sdk"); + File expectedSDKPath = new File(getSketchbookFolder(), "android" + File.separator + "sdk"); if (expectedSDKPath.getAbsolutePath().equals(suspectSDKPath.getAbsolutePath())) { - return false; // Most likely the SDK folder, skip it + return false; } } String[] list = folder.list(); - // If a bad folder or unreadable or whatever, this will come back null - if (list == null) { - return false; - } - - // Alphabetize the list, since it's not always alpha order + if (list == null) return false; Arrays.sort(list, String.CASE_INSENSITIVE_ORDER); ActionListener listener = e -> { @@ -2004,26 +1562,17 @@ protected boolean addSketches(JMenu menu, File folder) { if (new File(path).exists()) { handleOpen(path); } else { - Messages.showWarning("Sketch Disappeared",""" + Messages.showWarning("Sketch Disappeared", """ The selected sketch no longer exists. You may need to restart Processing to update the sketchbook menu.""", null); } }; - // offers no speed improvement - //menu.addActionListener(listener); boolean found = false; - for (String name : list) { - if (name.charAt(0) == '.') { - continue; - } - - // TODO Is this necessary any longer? This seems gross [fry 210804] - if (name.equals("old")) { // Don't add old contributions - continue; - } + if (name.charAt(0) == '.') continue; + if (name.equals("old")) continue; File entry = new File(folder, name); File sketchFile = null; @@ -2040,11 +1589,8 @@ protected boolean addSketches(JMenu menu, File folder) { item.setActionCommand(sketchFile.getAbsolutePath()); menu.add(item); found = true; - } else if (entry.isDirectory()) { - // not a sketch folder, but may be a subfolder containing sketches JMenu submenu = new JMenu(name); - // needs to be separate var otherwise would set found to false boolean anything = addSketches(submenu, entry); if (anything) { menu.add(submenu); @@ -2056,59 +1602,23 @@ protected boolean addSketches(JMenu menu, File folder) { } - /** - * Mostly identical to the JMenu version above, however the rules are - * slightly different for how examples are handled, etc. - */ public boolean addSketches(DefaultMutableTreeNode node, File folder, boolean examples) throws IOException { Messages.log("scanning " + folder.getAbsolutePath()); - // skip .DS_Store files, etc. (this shouldn't actually be necessary) - if (!folder.isDirectory()) { - return false; - } + if (!folder.isDirectory()) return false; final String folderName = folder.getName(); - - // Don't look inside the 'android' folders in the sketchbook - if (folderName.equals("android")) { - return false; - } - - // Don't look inside the 'libraries' folders in the sketchbook - if (folderName.equals("libraries")) { - return false; - } - - // When building the sketchbook, don't show the contributed 'examples' - // like it's a subfolder. But when loading examples, allow the folder - // to be named 'examples'. - if (!examples && folderName.equals("examples")) { - return false; - } - -// // Conversely, when looking for examples, ignore the other folders -// // (to avoid going through hoops with the tree node setup). -// if (examples && !folderName.equals("examples")) { -// return false; -// } -// // Doesn't quite work because the parent will be 'examples', and we want -// // to walk inside that, but the folder itself will have a different name + if (folderName.equals("android")) return false; + if (folderName.equals("libraries")) return false; + if (!examples && folderName.equals("examples")) return false; String[] fileList = folder.list(); - // If a bad folder or unreadable or whatever, this will come back null - if (fileList == null) { - return false; - } - - // Alphabetize the list, since it's not always alpha order + if (fileList == null) return false; Arrays.sort(fileList, String.CASE_INSENSITIVE_ORDER); boolean found = false; for (String name : fileList) { - if (name.charAt(0) == '.') { // Skip hidden files - continue; - } + if (name.charAt(0) == '.') continue; File entry = new File(folder, name); File sketchFile = null; @@ -2121,15 +1631,11 @@ public boolean addSketches(DefaultMutableTreeNode node, File folder, if (sketchFile != null) { DefaultMutableTreeNode item = - new DefaultMutableTreeNode(new SketchReference(name, sketchFile)); - + new DefaultMutableTreeNode(new SketchReference(name, sketchFile)); node.add(item); found = true; - } else if (entry.isDirectory()) { - // not a sketch folder, but maybe a subfolder containing sketches DefaultMutableTreeNode subNode = new DefaultMutableTreeNode(name); - // needs to be separate var otherwise would set found to false boolean anything = addSketches(subNode, entry, examples); if (anything) { node.add(subNode); @@ -2140,120 +1646,61 @@ public boolean addSketches(DefaultMutableTreeNode node, File folder, return found; } - - /* - static private Mode findSketchMode(File folder, List modeList) { - try { - Settings props = Sketch.loadProperties(folder); - if (props != null) { - String id = props.get("mode.id"); - if (id != null) { - Mode mode = findMode(id); - if (mode != null) { - return mode; - } - } - } - } catch (IOException e) { - e.printStackTrace(); - } - for (Mode mode : modeList) { - // Test whether a .pde file of the same name as its parent folder exists. - String defaultName = folder.getName() + "." + mode.getDefaultExtension(); - File entry = new File(folder, defaultName); - if (entry.exists()) { - return mode; - } - } - return null; - } - */ - - // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . - /** - * Show the Preferences window. - */ public void handlePrefs() { - PDEPreferencesKt.show(); + PDEPreferencesKt.show(); } - // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . - /** - * Return a File from inside the Processing 'lib' folder. - */ @SuppressWarnings("RedundantThrows") static public File getLibFile(String filename) throws IOException { return new File(Platform.getContentFile("lib"), filename); } - - /** - * Return an InputStream for a file inside the Processing lib folder. - */ static public InputStream getLibStream(String filename) throws IOException { return new FileInputStream(getLibFile(filename)); } /** - * Get the directory that can store settings. (Library on OS X, App Data or - * something similar on Windows, a dot folder on Linux.) Removed this as a - * preference for 3.0a3 because we need this to be stable, but adding back - * for 4.0 beta 4 so that folks can do 'portable' versions again. - * - * @deprecated use processing.utils.Settings.getFolder() instead, this method will invoke AWT + * @deprecated use processing.utils.Settings.getFolder() instead */ static public File getSettingsFolder() { - var override = getSettingsOverride(); - if (override != null) { - return override; - } - try { - return processing.utils.Settings.getFolder(); - } catch (processing.utils.Settings.SettingsFolderException e) { - switch (e.getType()) { - case COULD_NOT_CREATE_FOLDER -> Messages.showError("Settings issues", - """ - Processing cannot run because it could not - create a folder to store your settings at - """ + e.getMessage(), null); - case WINDOWS_APPDATA_NOT_FOUND -> Messages.showError("Settings issues", - """ - Processing cannot run because it could not - find the AppData or LocalAppData folder on your system. - """, null); - case MACOS_LIBRARY_FOLDER_NOT_FOUND -> Messages.showError("Settings issues", - """ - Processing cannot run because it could not - find the Library folder on your system. - """, null); - case LINUX_CONFIG_FOLDER_NOT_FOUND -> Messages.showError("Settings issues", - """ - Processing cannot run because either your - XDG_CONFIG_HOME or SNAP_USER_COMMON is set - but the folder does not exist. - """, null); - case LINUX_SUDO_USER_ERROR -> Messages.showError("Settings issues", - """ - Processing cannot run because it was started - with sudo and Processing could not resolve - the original users home directory. - """, null); - default -> Messages.showTrace("An rare and unknowable thing happened", - """ - Could not get the settings folder. Please report: - http://github.com/processing/processing4/issues/new - """, - e, true); - } - } - throw new RuntimeException("Unreachable code in Base.getSettingsFolder()"); + var override = getSettingsOverride(); + if (override != null) { + return override; + } + try { + return processing.utils.Settings.getFolder(); + } catch (processing.utils.Settings.SettingsFolderException e) { + switch (e.getType()) { + case COULD_NOT_CREATE_FOLDER -> Messages.showError("Settings issues", + "Processing cannot run because it could not\n" + + "create a folder to store your settings at\n" + e.getMessage(), null); + case WINDOWS_APPDATA_NOT_FOUND -> Messages.showError("Settings issues", + "Processing cannot run because it could not\n" + + "find the AppData or LocalAppData folder on your system.", null); + case MACOS_LIBRARY_FOLDER_NOT_FOUND -> Messages.showError("Settings issues", + "Processing cannot run because it could not\n" + + "find the Library folder on your system.", null); + case LINUX_CONFIG_FOLDER_NOT_FOUND -> Messages.showError("Settings issues", + "Processing cannot run because either your\n" + + "XDG_CONFIG_HOME or SNAP_USER_COMMON is set\n" + + "but the folder does not exist.", null); + case LINUX_SUDO_USER_ERROR -> Messages.showError("Settings issues", + "Processing cannot run because it was started\n" + + "with sudo and Processing could not resolve\n" + + "the original users home directory.", null); + default -> Messages.showTrace("An rare and unknowable thing happened", + "Could not get the settings folder. Please report:\n" + + "http://github.com/processing/processing4/issues/new", e, true); + } + } + throw new RuntimeException("Unreachable code in Base.getSettingsFolder()"); } @@ -2261,31 +1708,22 @@ static public File getSettingsOverride() { return settingsOverride; } - - /** - * Convenience method to get a File object for the specified filename inside - * the settings folder. Used to get preferences and recent sketch files. - * @param filename A file inside the settings folder. - * @return filename wrapped as a File object inside the settings folder - */ static public File getSettingsFile(String filename) { return new File(getSettingsFolder(), filename); } - static public File getToolsFolder() { return Platform.getContentFile("tools"); } - static protected File sketchbookFolder; + static protected File sketchbookFolder; + static public void locateSketchbookFolder() { - // If a value is at least set, first check to see if the folder exists. - // If it doesn't, warn the user that the sketchbook folder is being reset. String sketchbookPath = Preferences.getSketchbookPath(); if (sketchbookPath != null) { sketchbookFolder = new File(sketchbookPath); if (!sketchbookFolder.exists()) { - Messages.showWarning("Sketchbook folder disappeared",""" + Messages.showWarning("Sketchbook folder disappeared", """ The sketchbook folder no longer exists. Processing will switch to the default sketchbook location, and create a new sketchbook folder if @@ -2295,21 +1733,18 @@ static public void locateSketchbookFolder() { } } - // If no path is set, get the default sketchbook folder for this platform if (sketchbookFolder == null) { sketchbookFolder = getDefaultSketchbookFolder(); Preferences.setSketchbookPath(sketchbookFolder.getAbsolutePath()); if (!sketchbookFolder.exists()) { if (!sketchbookFolder.mkdirs()) { Messages.showError("Could not create sketchbook", - "Unable to create a sketchbook folder at\n" + - sketchbookFolder + "\n" + - "Try creating a folder at that path and restart Processing.", null); + "Unable to create a sketchbook folder at\n" + + sketchbookFolder + "\n" + + "Try creating a folder at that path and restart Processing.", null); } } } - - // make sure the libraries/modes/tools directories exist makeSketchbookSubfolders(); } @@ -2322,65 +1757,49 @@ public void setSketchbookFolder(File folder) { } - /** - * Create the libraries, modes, tools, examples folders in the sketchbook. - */ @SuppressWarnings("ResultOfMethodCallIgnored") static protected void makeSketchbookSubfolders() { - // ignore result; mkdirs() will return false if the folder already exists getSketchbookLibrariesFolder().mkdirs(); getSketchbookToolsFolder().mkdirs(); getSketchbookModesFolder().mkdirs(); getSketchbookExamplesFolder().mkdirs(); getSketchbookTemplatesFolder().mkdirs(); - - /* - Messages.showWarning("Could not create folder", - "Could not create the libraries, tools, modes, examples, and templates\n" + - "folders inside " + sketchbookFolder + "\n" + - "Try creating them manually to determine the problem.", null); - */ } static public File getSketchbookFolder() { - var sketchbookPathOverride = System.getProperty("processing.sketchbook.folder"); - if (sketchbookPathOverride != null && !sketchbookPathOverride.isEmpty()) { - return new File(sketchbookPathOverride); - } - if (sketchbookFolder == null) { - locateSketchbookFolder(); - } + var sketchbookPathOverride = System.getProperty("processing.sketchbook.folder"); + if (sketchbookPathOverride != null && !sketchbookPathOverride.isEmpty()) { + return new File(sketchbookPathOverride); + } + if (sketchbookFolder == null) { + locateSketchbookFolder(); + } return sketchbookFolder; } - static public File getSketchbookLibrariesFolder() { - return new File(getSketchbookFolder(), "libraries"); + return new File(getSketchbookFolder(), "libraries"); } - static public File getSketchbookToolsFolder() { - return new File(getSketchbookFolder(), "tools"); + return new File(getSketchbookFolder(), "tools"); } - static public File getSketchbookModesFolder() { - return new File(getSketchbookFolder(), "modes"); + return new File(getSketchbookFolder(), "modes"); } - static public File getSketchbookExamplesFolder() { - return new File(getSketchbookFolder(), "examples"); + return new File(getSketchbookFolder(), "examples"); } - static public File getSketchbookTemplatesFolder() { - return new File(getSketchbookFolder(), "templates"); + return new File(getSketchbookFolder(), "templates"); } - @NotNull + @NotNull static protected File getDefaultSketchbookFolder() { File sketchbookFolder = null; try { @@ -2389,21 +1808,18 @@ static protected File getDefaultSketchbookFolder() { if (sketchbookFolder == null) { Messages.showError("No sketchbook", - "Problem while trying to get the sketchbook", null); - + "Problem while trying to get the sketchbook", null); } else { - // create the folder if it doesn't exist already boolean result = true; if (!sketchbookFolder.exists()) { result = sketchbookFolder.mkdirs(); } - if (!result) { Messages.showError("You forgot your sketchbook", - "Processing cannot run because it could not\n" + - "create a folder to store your sketchbook.", null); + "Processing cannot run because it could not\n" + + "create a folder to store your sketchbook.", null); } } return sketchbookFolder; } -} +} \ No newline at end of file