diff --git a/app/.classpath b/app/.classpath index 27164be5b04..956c4a5c8f2 100644 --- a/app/.classpath +++ b/app/.classpath @@ -54,5 +54,6 @@ + diff --git a/app/lib/eventbus4j-0.0.1.jar b/app/lib/eventbus4j-0.0.1.jar new file mode 100755 index 00000000000..ba882e56e48 Binary files /dev/null and b/app/lib/eventbus4j-0.0.1.jar differ diff --git a/app/src/cc/arduino/contributions/BuiltInCoreIsNewerCheck.java b/app/src/cc/arduino/contributions/BuiltInCoreIsNewerCheck.java index d28d735e35f..b894cca767b 100644 --- a/app/src/cc/arduino/contributions/BuiltInCoreIsNewerCheck.java +++ b/app/src/cc/arduino/contributions/BuiltInCoreIsNewerCheck.java @@ -85,9 +85,7 @@ private void builtInPackageIsNewerCheck() throws InterruptedException { return; } - while (!base.hasActiveEditor()) { - Thread.sleep(100); - } + base.getWaitActiveEditor(); if (VersionComparator.greaterThan(installedBuiltIn.getParsedVersion(), installedNotBuiltIn.getParsedVersion())) { SwingUtilities.invokeLater(() -> { diff --git a/app/src/cc/arduino/contributions/ContributionsSelfCheck.java b/app/src/cc/arduino/contributions/ContributionsSelfCheck.java index 96fd987b099..614fe1610e6 100644 --- a/app/src/cc/arduino/contributions/ContributionsSelfCheck.java +++ b/app/src/cc/arduino/contributions/ContributionsSelfCheck.java @@ -125,7 +125,7 @@ public void run() { } SwingUtilities.invokeLater(() -> { - Editor ed = base.getActiveEditor(); + Editor ed = base.getWaitActiveEditor(); boolean accessibleIde = PreferencesData.getBoolean("ide.accessible"); if (accessibleIde) { notificationPopup = new NotificationPopup(ed, hyperlinkListener, text, false, this, button1Name, button2Name); diff --git a/app/src/cc/arduino/view/JMenuLazy.java b/app/src/cc/arduino/view/JMenuLazy.java new file mode 100755 index 00000000000..e1398ac9ffa --- /dev/null +++ b/app/src/cc/arduino/view/JMenuLazy.java @@ -0,0 +1,105 @@ +package cc.arduino.view; + +import java.awt.Component; +import java.util.LinkedList; + +import javax.swing.JMenu; +import javax.swing.JMenuItem; +import javax.swing.JPopupMenu; +import javax.swing.JSeparator; +import javax.swing.event.MenuEvent; +import javax.swing.event.MenuListener; + +/** + * Avoid slow rendering of menu while load full menu tree in background + * + * @author Ricardo JL Rufino - (ricardo.jl.rufino@gmail.com) + * @date 13 de mai de 2020 + */ +public class JMenuLazy extends JMenu { + + private LinkedList components = new LinkedList<>(); + + private boolean loading = false; + + public JMenuLazy(String s) { + super(s); + addMenuListener(menuListener); + } + + public void setLoading(boolean loading) { + this.loading = loading; + this.setEnabled(!loading); + } + + public boolean isLoading() { + return loading; + } + + @Override + public Component add(Component c) { + if (isLoading()) { + components.add(c); + return c; + } + return super.add(c); + } + + @Override + public JMenuItem add(JMenuItem c) { + if (isLoading()) { + components.add(c); + return c; + } + return super.add(c); + } + + @Override + public void addSeparator() { + if (isLoading()) { + components.add(new JPopupMenu.Separator()); + return; + } + super.addSeparator(); + } + + public void removeAll() { + super.removeAll(); + synchronized (components) { + components.clear(); + } + }; + + private MenuListener menuListener = new MenuListener() { + + @Override + public void menuSelected(MenuEvent e) { + if (!isLoading()) { + if (loading == false) { + if (!components.isEmpty()) { + for (Component component : components) { + if (component instanceof JSeparator) { + JMenuLazy.super.addSeparator(); + } else { + JMenuLazy.super.add(component); + + } + } + components.clear(); + } + } + } + } + + @Override + public void menuDeselected(MenuEvent e) { + + } + + @Override + public void menuCanceled(MenuEvent e) { + + } + }; + +} diff --git a/app/src/cc/arduino/view/SplashScreenHelper.java b/app/src/cc/arduino/view/SplashScreenHelper.java index 108c1c8b2f6..b6ebf3672f7 100644 --- a/app/src/cc/arduino/view/SplashScreenHelper.java +++ b/app/src/cc/arduino/view/SplashScreenHelper.java @@ -48,6 +48,8 @@ public class SplashScreenHelper { private final SplashScreen splash; private Rectangle2D.Double splashTextArea; private Graphics2D splashGraphics; + private int progress = 0; + private String text; public SplashScreenHelper(SplashScreen splash) { this.splash = splash; @@ -58,8 +60,19 @@ public SplashScreenHelper(SplashScreen splash) { desktopHints = null; } } + + public void setProgress(int progress) { + this.progress = progress; + splashText(text); + } + + public void splashText(String text, int progress) { + this.progress = progress; + splashText(text); + } public void splashText(String text) { + this.text = text; if (splash == null) { printText(text); return; @@ -76,10 +89,13 @@ public void splashText(String text) { eraseLastStatusText(); drawText(text); + + if(progress > 0) drawProgress(); ensureTextIsDiplayed(); } + private void ensureTextIsDiplayed() { synchronized (SplashScreen.class) { if (splash.isVisible()) { @@ -88,6 +104,14 @@ private void ensureTextIsDiplayed() { } } + + private void drawProgress() { + splashGraphics.setPaint(new Color(0, 100, 104)); + splashGraphics.setStroke(new BasicStroke(2)); + int w = (int)(splashTextArea.getWidth()*(progress/100.0f)); + splashGraphics.drawLine( (int) splashTextArea.getX(), (int) splashTextArea.getY(), (int) splashTextArea.getX() + w, (int) splashTextArea.getY()); + } + private void drawText(String str) { splashGraphics.setPaint(Color.BLACK); FontMetrics metrics = splashGraphics.getFontMetrics(); diff --git a/app/src/cc/arduino/view/preferences/Preferences.java b/app/src/cc/arduino/view/preferences/Preferences.java index 005d2f83e54..d1f139616ef 100644 --- a/app/src/cc/arduino/view/preferences/Preferences.java +++ b/app/src/cc/arduino/view/preferences/Preferences.java @@ -135,6 +135,7 @@ private void initComponents() { checkUpdatesBox = new javax.swing.JCheckBox(); saveVerifyUploadBox = new javax.swing.JCheckBox(); accessibleIDEBox = new javax.swing.JCheckBox(); + checkRestoreSketch = new javax.swing.JCheckBox(); jLabel1 = new javax.swing.JLabel(); jLabel2 = new javax.swing.JLabel(); scaleSpinner = new javax.swing.JSpinner(); @@ -284,6 +285,9 @@ public void mouseEntered(java.awt.event.MouseEvent evt) { accessibleIDEBox.setText(tr("Use accessibility features")); checkboxesContainer.add(accessibleIDEBox); + + checkRestoreSketch.setText(tr("Ask to restore sketches")); + checkboxesContainer.add(checkRestoreSketch); jLabel1.setText(tr("Interface scale:")); @@ -718,6 +722,7 @@ private void autoScaleCheckBoxItemStateChanged(java.awt.event.ItemEvent evt) {// private javax.swing.JButton browseButton; private javax.swing.JCheckBox checkUpdatesBox; private javax.swing.JCheckBox accessibleIDEBox; + private javax.swing.JCheckBox checkRestoreSketch; private javax.swing.JPanel checkboxesContainer; private javax.swing.JComboBox comboLanguage; private javax.swing.JLabel comboLanguageLabel; @@ -832,6 +837,10 @@ private void savePreferencesData() { PreferencesData.setBoolean("update.check", checkUpdatesBox.isSelected()); PreferencesData.setBoolean("ide.accessible", accessibleIDEBox.isSelected()); + + if(checkRestoreSketch.isSelected()) { + PreferencesData.set("last.sketch.restore", "ask"); + } PreferencesData.set("boardsmanager.additional.urls", additionalBoardsManagerField.getText().replace("\r\n", "\n").replace("\r", "\n").replace("\n", ",")); @@ -912,6 +921,8 @@ private void showPreferencesData() { } accessibleIDEBox.setSelected(PreferencesData.getBoolean("ide.accessible")); + + checkRestoreSketch.setSelected(PreferencesData.get("last.sketch.restore", "ask").equals("ask")); saveVerifyUploadBox.setSelected(PreferencesData.getBoolean("editor.save_on_verify")); diff --git a/app/src/log4j2.xml b/app/src/log4j2.xml index 64f6b8f063f..9213b320841 100644 --- a/app/src/log4j2.xml +++ b/app/src/log4j2.xml @@ -1,5 +1,5 @@ - + diff --git a/app/src/processing/app/Base.java b/app/src/processing/app/Base.java index 34193e07dbf..90e77739d4e 100644 --- a/app/src/processing/app/Base.java +++ b/app/src/processing/app/Base.java @@ -40,15 +40,29 @@ import cc.arduino.packages.DiscoveryManager; import cc.arduino.packages.Uploader; import cc.arduino.view.Event; +import cc.arduino.view.JMenuLazy; import cc.arduino.view.JMenuUtils; import cc.arduino.view.SplashScreenHelper; + import com.github.zafarkhaja.semver.Version; +import com.ricardojlrufino.eventbus.EventBus; +import com.ricardojlrufino.eventbus.EventBusListener; +import com.ricardojlrufino.eventbus.EventDispatcher; +import com.ricardojlrufino.eventbus.EventHandler; +import com.ricardojlrufino.eventbus.EventMessage; +import com.ricardojlrufino.eventbus.dispatcher.DebounceEventDispatcher; +import com.ricardojlrufino.eventbus.dispatcher.SingleThreadEventDispatcher; + import org.apache.commons.compress.utils.IOUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + import processing.app.debug.TargetBoard; import processing.app.debug.TargetPackage; import processing.app.debug.TargetPlatform; import processing.app.helpers.*; +import processing.app.helpers.filefilters.ExamplesFilter; import processing.app.helpers.filefilters.OnlyDirs; import processing.app.helpers.filefilters.OnlyFilesWithExtension; import processing.app.javax.swing.filechooser.FileNameExtensionFilter; @@ -68,10 +82,11 @@ import java.io.*; import java.util.List; import java.util.Timer; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.*; import java.util.logging.Handler; import java.util.logging.Level; -import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -86,11 +101,13 @@ * files and images, etc) that comes from that. */ public class Base { - + private static final int RECENT_SKETCHES_MAX_SIZE = 10; private static boolean commandLine; public static volatile Base INSTANCE; + + public static Logger log; public static Map FIND_DIALOG_STATE = new HashMap<>(); private final ContributionInstaller contributionInstaller; @@ -124,6 +141,9 @@ public class Base { private PdeKeywords pdeKeywords; private final List recentSketchesMenuItems = new LinkedList<>(); + + // Executor to load / reload menus in backgroud. + private ExecutorService menuExecutor = Executors.newCachedThreadPool(); static public void main(String args[]) throws Exception { if (!OSUtils.isWindows()) { @@ -160,11 +180,17 @@ public static boolean isMacOsAboutMenuItemPresent() { } static public void initLogger() { + + System.setProperty("log4j.dir", BaseNoGui.getSettingsFolder().getAbsolutePath()); + + log = LogManager.getLogger(Base.class); // init log4j + + // JAVA UTIL LOGS .... Handler consoleHandler = new ConsoleLogger(); consoleHandler.setLevel(Level.ALL); consoleHandler.setFormatter(new LogFormatter("%1$tl:%1$tM:%1$tS [%4$7s] %2$s: %5$s%n")); - Logger globalLogger = Logger.getLogger(Logger.GLOBAL_LOGGER_NAME); + java.util.logging.Logger globalLogger = java.util.logging.Logger.getLogger(java.util.logging.Logger.GLOBAL_LOGGER_NAME); globalLogger.setLevel(consoleHandler.getLevel()); // Remove default @@ -172,7 +198,7 @@ static public void initLogger() { for(Handler handler : handlers) { globalLogger.removeHandler(handler); } - Logger root = Logger.getLogger(""); + java.util.logging.Logger root = java.util.logging.Logger.getLogger(""); handlers = root.getHandlers(); for(Handler handler : handlers) { root.removeHandler(handler); @@ -180,11 +206,104 @@ static public void initLogger() { globalLogger.addHandler(consoleHandler); - Logger.getLogger("cc.arduino.packages.autocomplete").setParent(globalLogger); - Logger.getLogger("br.com.criativasoft.cpluslibparser").setParent(globalLogger); - Logger.getLogger(Base.class.getPackage().getName()).setParent(globalLogger); + java.util.logging.Logger.getLogger("cc.arduino.packages.autocomplete").setParent(globalLogger); + java.util.logging.Logger.getLogger("br.com.criativasoft.cpluslibparser").setParent(globalLogger); + java.util.logging.Logger.getLogger(Base.class.getPackage().getName()).setParent(globalLogger); } + + /** + * Configuration of internal events launched by the application. + * Only certain types of events will need a specific dispatcher, such as those that need a debounce. + * The other types of events do not need a dispatcher, they will use the standard dispatcher {@link SingleThreadEventDispatcher} + */ + public void initEvents() { + + // Avoids call same method multiple time... + EventBus.configDispatcher(UIEvents.MenuBoardSettingsChanged.class, new DebounceEventDispatcher(1000)); + EventBus.configDispatcher(UIEvents.TriggerFixBoardOrPortChange.class, new DebounceEventDispatcher(200)); + + EventBus.addBusListener(new EventBusListener() { + + @Override + public void onError(Exception e, E event, EventHandler handler) { + + BaseNoGui.showWarning("Event Handler Exception", e.getMessage(), e); + + } + + /** + * Called before the event is "scheduled" to run. It is the responsibility of the dispatcher to accept or not the event. + * This method is executed on the same thread as the method that triggered the event, thus allowing tracking. + * NOTE: It may be that events here are not actually executed, as they can be ignored by the dispatcher + */ + @Override + public void beforeDispatch(E event, EventHandler handler, EventDispatcher eventDispatcher) { + +// // Here you can enable to see which methods are launching the events. +// if (event instanceof UIEvents.TriggerFixBoardOrPortChange) { +// EventBusUtils.printStack(event, handler); +// } + } + + @Override + public void eventIgnored(E event, EventDispatcher eventDispatcher,String reason) { + + log.debug("[IGNORED] Event: " + event + ", Reason: '"+reason ); + + if (UIEvents.BoardOrPortChange.class == event.getClass()) { + // EventBusUtils.printStack(); + } + + } + + }); + + initEventsHandlers(); + } + + public void initEventsHandlers() { + + // ==== MenuBoardSettingsChanged ==== + // Custom menus from Board clicked. + // This method is also called at the first startup of the menu + EventBus.register(UIEvents.MenuBoardSettingsChanged.class, event -> { + + onBoardOrPortChange(); + + }); + + // There are several places in the IDE that call onBoardOrPortChange indiscriminately, causing poor performance. + // This wind will serve to debounce the method. + EventBus.register(UIEvents.TriggerFixBoardOrPortChange.class, event -> { + + _onBoardOrPortChange(); + + }); + + + // Rebuild Menus. + EventBus.register(UIEvents.LibraryIndexUpdated.class, event -> { + + Base.this.rebuildImportMenu(Editor.importMenu); + Base.this.rebuildExamplesMenu(Editor.examplesMenu); + + }); + + // Update PdeKeywords + EventBus.register(UIEvents.LibraryIndexUpdated.class, event -> { + + boolean reload = getPdeKeywords().reloadIfNeed(); + + if (reload) { + for (Editor editor : editors) { + editor.updateKeywords(getPdeKeywords()); + } + } + + }); + + } static protected boolean isCommandLine() { return commandLine; @@ -205,8 +324,6 @@ public Base(String[] args) throws Exception { BaseNoGui.initLogger(); - initLogger(); - BaseNoGui.initPlatform(); BaseNoGui.getPlatform().init(); @@ -224,7 +341,8 @@ public Base(String[] args) throws Exception { if (parser.isGuiMode()) { System.out.println("Set log4j store directory " + BaseNoGui.getSettingsFolder().getAbsolutePath()); } - System.setProperty("log4j.dir", BaseNoGui.getSettingsFolder().getAbsolutePath()); + + initLogger(); BaseNoGui.checkInstallationFolder(); @@ -249,6 +367,9 @@ public Base(String[] args) throws Exception { // Setup the theme coloring fun Theme.init(); System.setProperty("swing.aatext", PreferencesData.get("editor.antialias", "true")); + + // Setup event system + initEvents(); // Set the look and feel before opening the window try { @@ -263,7 +384,7 @@ public Base(String[] args) throws Exception { splash = new SplashScreenHelper(null); } - splash.splashText(tr("Loading configuration...")); + splash.splashText(tr("Loading configuration..."), 10); BaseNoGui.initVersion(); @@ -274,12 +395,12 @@ public Base(String[] args) throws Exception { untitledFolder = FileUtils.createTempFolder("untitled" + new Random().nextInt(Integer.MAX_VALUE), ".tmp"); DeleteFilesOnShutdown.add(untitledFolder); - splash.splashText(tr("Initializing packages...")); + splash.splashText(tr("Initializing packages..."), 20); BaseNoGui.initPackages(); parser.getUploadPort().ifPresent(BaseNoGui::selectSerialPort); - splash.splashText(tr("Preparing boards...")); + splash.splashText(tr("Preparing boards..."), 30); if (!isCommandLine()) { rebuildBoardsMenu(); @@ -293,9 +414,6 @@ public Base(String[] args) throws Exception { // Setup board-dependent variables. onBoardOrPortChange(); - pdeKeywords = new PdeKeywords(); - pdeKeywords.reload(); - final GPGDetachedSignatureVerifier gpgDetachedSignatureVerifier = new GPGDetachedSignatureVerifier(); contributionInstaller = new ContributionInstaller(BaseNoGui.getPlatform(), gpgDetachedSignatureVerifier); libraryInstaller = new LibraryInstaller(BaseNoGui.getPlatform(), gpgDetachedSignatureVerifier); @@ -458,7 +576,7 @@ public Base(String[] args) throws Exception { // No errors exit gracefully System.exit(0); } else if (parser.isGuiMode()) { - splash.splashText(tr("Starting...")); + splash.splashText(tr("Starting..."), 80); for (String path : parser.getFilenames()) { // Correctly resolve relative paths @@ -492,11 +610,8 @@ public Base(String[] args) throws Exception { // Check if there were previously opened sketches to be restored restoreSketches(); - - // Create a new empty window (will be replaced with any files to be opened) - if (editors.isEmpty()) { - handleNew(); - } + + splash.setProgress(90); // last 10% is not predictable, but ide should open fast from here new Thread(new BuiltInCoreIsNewerCheck(this)).start(); @@ -534,13 +649,27 @@ private void installKeyboardInputMap() { * * @throws Exception */ - protected boolean restoreSketches() throws Exception { + protected void restoreSketches() throws Exception { // Iterate through all sketches that were open last time p5 was running. // If !windowPositionValid, then ignore the coordinates found for each. + + String restoreOption = PreferencesData.get("last.sketch.restore", "ask"); + // Always New + if (restoreOption.equals("blank")) { + handleNew(); + return; + } + // Save the sketch path and window placement for each open sketch int count = PreferencesData.getInteger("last.sketch.count"); - int opened = 0; + + if (count <= 0) { + handleNew(); + return; + } + + ArrayList options = new ArrayList<>(); for (int i = count - 1; i >= 0; i--) { String path = PreferencesData.get("last.sketch" + i + ".path"); if (path == null) { @@ -554,13 +683,84 @@ protected boolean restoreSketches() throws Exception { // path unchanged. } } - int[] location = retrieveSketchLocation("" + i); - // If file did not exist, null will be returned for the Editor - if (handleOpen(new File(path), location, nextEditorLocation(), false, false) != null) { - opened++; - } + + options.add(new File(path)); + } + + // Show dialog + + JPanel restore = new JPanel(); + restore.setLayout(new BorderLayout()); + JPanel checkBoxPanel = new JPanel(); + checkBoxPanel.setBorder(BorderFactory.createTitledBorder(tr("Select to restore"))); + List checkboxList = new LinkedList<>(); + for (File opt : options) { + JCheckBox box = new JCheckBox(opt.getName()); + box.setActionCommand(opt.getAbsolutePath()); + box.setSelected(true); + checkboxList.add(box); + checkBoxPanel.add(box); + } + restore.add(checkBoxPanel); + JCheckBox alwaysAskCB = new JCheckBox(tr("Always ask")); + restore.add(alwaysAskCB, BorderLayout.SOUTH); + + int chosenOption; + + if (restoreOption.equals("ask")) { + alwaysAskCB.setSelected(true); + + // Allow set icon on JOptionPane + JFrame frame = new JFrame(); + setIcon(frame); + + chosenOption = JOptionPane.showConfirmDialog(frame, restore, tr("Restore Sketchs ?"), JOptionPane.YES_NO_OPTION); + + }else { // restoreOption = restore + chosenOption = JOptionPane.YES_OPTION; + } + + // Save preferences + if (alwaysAskCB.isSelected()) { + PreferencesData.set("last.sketch.restore", "ask"); + }else { + if (chosenOption == JOptionPane.YES_OPTION) + PreferencesData.set("last.sketch.restore", "restore"); + else { + PreferencesData.set("last.sketch.restore", "blank"); + } + } + + if (chosenOption == JOptionPane.CLOSED_OPTION) { + System.err.println("Exiting..."); + System.exit(0); + } + + if (chosenOption == JOptionPane.YES_OPTION) { + + Runnable runnable = new Runnable() { + public void run() { + try { + for (int j = 0; j < checkboxList.size(); j++) { + JCheckBox checkbox = checkboxList.get(j); + if (checkbox.isSelected()) { + int[] location = retrieveSketchLocation("" + j); + // If file did not exist, null will be returned for the Editor + handleOpen(new File(checkbox.getActionCommand()), location, nextEditorLocation(), false, false); + } + } + + } catch (Exception e) { + e.printStackTrace(); + } + } + }; + + new Thread(runnable).start(); + + }else { + handleNew(); } - return (opened > 0); } /** @@ -761,7 +961,7 @@ protected File createNewUntitled() throws IOException { int multiples = index / 26; - if(multiples > 0){ + if (multiples > 0){ newbieName = ((char) ('a' + (multiples-1))) + "" + ((char) ('a' + (index % 26))) + ""; }else{ newbieName = ((char) ('a' + index)) + ""; @@ -1044,14 +1244,10 @@ protected boolean handleQuitEach() { public void rebuildSketchbookMenus() { //System.out.println("async enter"); //new Exception().printStackTrace(); - SwingUtilities.invokeLater(new Runnable() { - public void run() { - //System.out.println("starting rebuild"); - rebuildSketchbookMenu(Editor.sketchbookMenu); - rebuildToolbarMenu(Editor.toolbarMenu); - //System.out.println("done with rebuild"); - } - }); + //System.out.println("starting rebuild"); + rebuildSketchbookMenu(Editor.sketchbookMenu); + rebuildToolbarMenu(Editor.toolbarMenu); + //System.out.println("done with rebuild"); //System.out.println("async exit"); } @@ -1073,29 +1269,46 @@ public void actionPerformed(ActionEvent e) { }); menu.add(item); menu.addSeparator(); + + // Execute in backgroud thread, no need UI thread becouse no rendering needed + menuExecutor.execute(() -> { + // Add a list of all sketches and subfolders + boolean sketches = addSketches(menu, BaseNoGui.getSketchbookFolder()); + if (sketches) menu.addSeparator(); + + // Add each of the subfolders of examples directly to the menu + boolean found = addSketches(menu, BaseNoGui.getExamplesFolder()); + if (found) menu.addSeparator(); + }); - // Add a list of all sketches and subfolders - boolean sketches = addSketches(menu, BaseNoGui.getSketchbookFolder()); - if (sketches) menu.addSeparator(); - - // Add each of the subfolders of examples directly to the menu - boolean found = addSketches(menu, BaseNoGui.getExamplesFolder()); - if (found) menu.addSeparator(); } - - protected void rebuildSketchbookMenu(JMenu menu) { + protected void rebuildSketchbookMenu(JMenuLazy menu) { + + // Avoid call twice from "Editor.buildMenuBar" + if (menu.isLoading()) return; + + menu.setLoading(true); // mark as not enabled + menu.removeAll(); - addSketches(menu, BaseNoGui.getSketchbookFolder()); - - JMenu librariesMenu = JMenuUtils.findSubMenuWithLabel(menu, "libraries"); - if (librariesMenu != null) { - menu.remove(librariesMenu); - } - JMenu hardwareMenu = JMenuUtils.findSubMenuWithLabel(menu, "hardware"); - if (hardwareMenu != null) { - menu.remove(hardwareMenu); - } + + // Execute in backgroud thread, no need UI thread becouse no rendering needed + menuExecutor.submit(() -> { + + addSketches(menu, BaseNoGui.getSketchbookFolder()); + + JMenu librariesMenu = JMenuUtils.findSubMenuWithLabel(menu, "libraries"); + if (librariesMenu != null) { + menu.remove(librariesMenu); + } + JMenu hardwareMenu = JMenuUtils.findSubMenuWithLabel(menu, "hardware"); + if (hardwareMenu != null) { + menu.remove(hardwareMenu); + } + + SwingUtilities.invokeLater(() -> menu.setLoading(false)); + }); + } private LibraryList getSortedLibraries() { @@ -1122,10 +1335,7 @@ public void rebuildImportMenu(JMenu importMenu) { addLibraryMenuItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { Base.this.handleAddLibrary(); - BaseNoGui.librariesIndexer.rescanLibraries(); - Base.this.onBoardOrPortChange(); - Base.this.rebuildImportMenu(Editor.importMenu); - Base.this.rebuildExamplesMenu(Editor.examplesMenu); + Base.this.rescanLibraries(); } }); importMenu.add(addLibraryMenuItem); @@ -1169,201 +1379,241 @@ public void actionPerformed(ActionEvent event) { } } - public void rebuildExamplesMenu(JMenu menu) { + /** + * Rescan libraries in backgroud. + * Fire: {@link LibraryIndexUpdated} + */ + protected void rescanLibraries() { + + menuExecutor.execute(new Runnable() { + @Override + public void run() { + BaseNoGui.librariesIndexer.rescanLibraries(); + EventBus.notify(new UIEvents.LibraryIndexUpdated()); + } + }); + + } + + public void rebuildExamplesMenu(JMenuLazy menu) { if (menu == null) { return; } - + + // Avoid call twice from "Editor.buildMenuBar" + if (menu.isLoading()) return; + + menu.setLoading(true); + menu.removeAll(); + + // Execute in backgroud thread, no need UI thread becouse no rendering needed + menuExecutor.execute(() -> { + + // Add examples from distribution "example" folder + JMenuItem label = new JMenuItem(tr("Built-in Examples")); + label.setEnabled(false); + menu.add(label); + boolean found = addSketches(menu, BaseNoGui.getExamplesFolder()); + if (found) { + menu.addSeparator(); + } - // Add examples from distribution "example" folder - JMenuItem label = new JMenuItem(tr("Built-in Examples")); - label.setEnabled(false); - menu.add(label); - boolean found = addSketches(menu, BaseNoGui.getExamplesFolder()); - if (found) { - menu.addSeparator(); - } - - // Libraries can come from 4 locations: collect info about all four - String boardId = null; - String referencedPlatformName = null; - String myArch = null; - TargetPlatform targetPlatform = BaseNoGui.getTargetPlatform(); - if (targetPlatform != null) { - myArch = targetPlatform.getId(); - boardId = BaseNoGui.getTargetBoard().getName(); - String core = BaseNoGui.getBoardPreferences().get("build.core", "arduino"); - if (core.contains(":")) { - String refcore = core.split(":")[0]; - TargetPlatform referencedPlatform = BaseNoGui.getTargetPlatform(refcore, myArch); - if (referencedPlatform != null) { - referencedPlatformName = referencedPlatform.getPreferences().get("name"); + // Libraries can come from 4 locations: collect info about all four + String boardId = null; + String referencedPlatformName = null; + String myArch = null; + TargetPlatform targetPlatform = BaseNoGui.getTargetPlatform(); + if (targetPlatform != null) { + myArch = targetPlatform.getId(); + boardId = BaseNoGui.getTargetBoard().getName(); + String core = BaseNoGui.getBoardPreferences().get("build.core", "arduino"); + if (core.contains(":")) { + String refcore = core.split(":")[0]; + TargetPlatform referencedPlatform = BaseNoGui.getTargetPlatform(refcore, myArch); + if (referencedPlatform != null) { + referencedPlatformName = referencedPlatform.getPreferences().get("name"); + } + } } - } - } - // Divide the libraries into 7 lists, corresponding to the 4 locations - // with the retired IDE libs further divided into their own list, and - // any incompatible sketchbook libs further divided into their own list. - // The 7th list of "other" libraries should always be empty, but serves - // as a safety feature to prevent any library from vanishing. - LibraryList allLibraries = BaseNoGui.librariesIndexer.getInstalledLibraries(); - LibraryList ideLibs = new LibraryList(); - LibraryList retiredIdeLibs = new LibraryList(); - LibraryList platformLibs = new LibraryList(); - LibraryList referencedPlatformLibs = new LibraryList(); - LibraryList sketchbookLibs = new LibraryList(); - LibraryList sketchbookIncompatibleLibs = new LibraryList(); - LibraryList otherLibs = new LibraryList(); - for (UserLibrary lib : allLibraries) { - // Get the library's location - used for sorting into categories - Location location = lib.getLocation(); - // Is this library compatible? - List arch = lib.getArchitectures(); - boolean compatible; - if (myArch == null || arch == null || arch.contains("*")) { - compatible = true; - } else { - compatible = arch.contains(myArch); - } - // IDE Libaries (including retired) - if (location == Location.IDE_BUILTIN) { - if (compatible) { - // only compatible IDE libs are shown - if (lib.getTypes().contains("Retired")) { - retiredIdeLibs.add(lib); + // Divide the libraries into 7 lists, corresponding to the 4 locations + // with the retired IDE libs further divided into their own list, and + // any incompatible sketchbook libs further divided into their own list. + // The 7th list of "other" libraries should always be empty, but serves + // as a safety feature to prevent any library from vanishing. + LibraryList allLibraries = BaseNoGui.librariesIndexer.getInstalledLibraries(); + LibraryList ideLibs = new LibraryList(); + LibraryList retiredIdeLibs = new LibraryList(); + LibraryList platformLibs = new LibraryList(); + LibraryList referencedPlatformLibs = new LibraryList(); + LibraryList sketchbookLibs = new LibraryList(); + LibraryList sketchbookIncompatibleLibs = new LibraryList(); + LibraryList otherLibs = new LibraryList(); + for (UserLibrary lib : allLibraries) { + // Get the library's location - used for sorting into categories + Location location = lib.getLocation(); + // Is this library compatible? + List arch = lib.getArchitectures(); + boolean compatible; + if (myArch == null || arch == null || arch.contains("*")) { + compatible = true; } else { - ideLibs.add(lib); + compatible = arch.contains(myArch); } - } - // Platform Libraries - } else if (location == Location.CORE) { - // all platform libs are assumed to be compatible - platformLibs.add(lib); - // Referenced Platform Libraries - } else if (location == Location.REFERENCED_CORE) { - // all referenced platform libs are assumed to be compatible - referencedPlatformLibs.add(lib); - // Sketchbook Libraries (including incompatible) - } else if (location == Location.SKETCHBOOK) { - if (compatible) { - // libraries promoted from sketchbook (behave as builtin) - if (!lib.getTypes().isEmpty() && lib.getTypes().contains("Arduino") - && lib.getArchitectures().contains("*")) { - ideLibs.add(lib); + // IDE Libaries (including retired) + if (location == Location.IDE_BUILTIN) { + if (compatible) { + // only compatible IDE libs are shown + if (lib.getTypes().contains("Retired")) { + retiredIdeLibs.add(lib); + } else { + ideLibs.add(lib); + } + } + // Platform Libraries + } else if (location == Location.CORE) { + // all platform libs are assumed to be compatible + platformLibs.add(lib); + // Referenced Platform Libraries + } else if (location == Location.REFERENCED_CORE) { + // all referenced platform libs are assumed to be compatible + referencedPlatformLibs.add(lib); + // Sketchbook Libraries (including incompatible) + } else if (location == Location.SKETCHBOOK) { + if (compatible) { + // libraries promoted from sketchbook (behave as builtin) + if (!lib.getTypes().isEmpty() && lib.getTypes().contains("Arduino") + && lib.getArchitectures().contains("*")) { + ideLibs.add(lib); + } else { + sketchbookLibs.add(lib); + } + } else { + sketchbookIncompatibleLibs.add(lib); + } + // Other libraries of unknown type (should never occur) } else { - sketchbookLibs.add(lib); + otherLibs.add(lib); } - } else { - sketchbookIncompatibleLibs.add(lib); } - // Other libraries of unknown type (should never occur) - } else { - otherLibs.add(lib); - } - } - // Add examples from libraries - if (!ideLibs.isEmpty()) { - ideLibs.sort(); - label = new JMenuItem(tr("Examples for any board")); - label.setEnabled(false); - menu.add(label); - } - for (UserLibrary lib : ideLibs) { - addSketchesSubmenu(menu, lib); - } + // Add examples from libraries + if (!ideLibs.isEmpty()) { + ideLibs.sort(); + label = new JMenuItem(tr("Examples for any board")); + label.setEnabled(false); + menu.add(label); + } + for (UserLibrary lib : ideLibs) { + addSketchesSubmenu(menu, lib); + } - if (!retiredIdeLibs.isEmpty()) { - retiredIdeLibs.sort(); - JMenu retired = new JMenu(tr("RETIRED")); - menu.add(retired); - for (UserLibrary lib : retiredIdeLibs) { - addSketchesSubmenu(retired, lib); - } - } + if (!retiredIdeLibs.isEmpty()) { + retiredIdeLibs.sort(); + JMenu retired = new JMenu(tr("RETIRED")); + menu.add(retired); + for (UserLibrary lib : retiredIdeLibs) { + addSketchesSubmenu(retired, lib); + } + } - if (!platformLibs.isEmpty()) { - menu.addSeparator(); - platformLibs.sort(); - label = new JMenuItem(format(tr("Examples for {0}"), boardId)); - label.setEnabled(false); - menu.add(label); - for (UserLibrary lib : platformLibs) { - addSketchesSubmenu(menu, lib); - } - } + if (!platformLibs.isEmpty()) { + menu.addSeparator(); + platformLibs.sort(); + label = new JMenuItem(format(tr("Examples for {0}"), boardId)); + label.setEnabled(false); + menu.add(label); + for (UserLibrary lib : platformLibs) { + addSketchesSubmenu(menu, lib); + } + } - if (!referencedPlatformLibs.isEmpty()) { - menu.addSeparator(); - referencedPlatformLibs.sort(); - label = new JMenuItem(format(tr("Examples for {0}"), referencedPlatformName)); - label.setEnabled(false); - menu.add(label); - for (UserLibrary lib : referencedPlatformLibs) { - addSketchesSubmenu(menu, lib); - } - } + if (!referencedPlatformLibs.isEmpty()) { + menu.addSeparator(); + referencedPlatformLibs.sort(); + label = new JMenuItem(format(tr("Examples for {0}"), referencedPlatformName)); + label.setEnabled(false); + menu.add(label); + for (UserLibrary lib : referencedPlatformLibs) { + addSketchesSubmenu(menu, lib); + } + } - if (!sketchbookLibs.isEmpty()) { - menu.addSeparator(); - sketchbookLibs.sort(); - label = new JMenuItem(tr("Examples from Custom Libraries")); - label.setEnabled(false); - menu.add(label); - for (UserLibrary lib : sketchbookLibs) { - addSketchesSubmenu(menu, lib); - } - } + if (!sketchbookLibs.isEmpty()) { + menu.addSeparator(); + sketchbookLibs.sort(); + label = new JMenuItem(tr("Examples from Custom Libraries")); + label.setEnabled(false); + menu.add(label); + for (UserLibrary lib : sketchbookLibs) { + addSketchesSubmenu(menu, lib); + } + } - if (!sketchbookIncompatibleLibs.isEmpty()) { - sketchbookIncompatibleLibs.sort(); - JMenu incompatible = new JMenu(tr("INCOMPATIBLE")); - MenuScroller.setScrollerFor(incompatible); - menu.add(incompatible); - for (UserLibrary lib : sketchbookIncompatibleLibs) { - addSketchesSubmenu(incompatible, lib); - } - } + if (!sketchbookIncompatibleLibs.isEmpty()) { + sketchbookIncompatibleLibs.sort(); + JMenu incompatible = new JMenu(tr("INCOMPATIBLE")); + MenuScroller.setScrollerFor(incompatible); + menu.add(incompatible); + for (UserLibrary lib : sketchbookIncompatibleLibs) { + addSketchesSubmenu(incompatible, lib); + } + } - if (!otherLibs.isEmpty()) { - menu.addSeparator(); - otherLibs.sort(); - label = new JMenuItem(tr("Examples from Other Libraries")); - label.setEnabled(false); - menu.add(label); - for (UserLibrary lib : otherLibs) { - addSketchesSubmenu(menu, lib); - } - } + if (!otherLibs.isEmpty()) { + menu.addSeparator(); + otherLibs.sort(); + label = new JMenuItem(tr("Examples from Other Libraries")); + label.setEnabled(false); + menu.add(label); + for (UserLibrary lib : otherLibs) { + addSketchesSubmenu(menu, lib); + } + } + + SwingUtilities.invokeLater(() -> { + menu.setLoading(false); + }); + }); + } private static String priorPlatformFolder; - private static boolean newLibraryImported; - + + /** + * Trigger: {@link UIEvents.LibraryIndexUpdated} and {@link UIEvents.BoardOrPortChange} + */ public void onBoardOrPortChange() { - BaseNoGui.onBoardOrPortChange(); + EventBus.notify(new UIEvents.TriggerFixBoardOrPortChange()); + } + // this will be called from: UIEvents.TriggerFixBoardOrPortChange + private void _onBoardOrPortChange() { + // reload keywords when package/platform changes TargetPlatform tp = BaseNoGui.getTargetPlatform(); + + log.debug("BoardOrPortChange : board = {}" , tp.getId()); + if (tp != null) { String platformFolder = tp.getFolder().getAbsolutePath(); - if (priorPlatformFolder == null || !priorPlatformFolder.equals(platformFolder) || newLibraryImported) { - pdeKeywords = new PdeKeywords(); - pdeKeywords.reload(); + if (priorPlatformFolder == null || !priorPlatformFolder.equals(platformFolder)) { priorPlatformFolder = platformFolder; - newLibraryImported = false; - for (Editor editor : editors) { - editor.updateKeywords(pdeKeywords); - } + getPdeKeywords().setNeedReload(true); } } - - // Update editors status bar - for (Editor editor : editors) { - editor.onBoardOrPortChange(); - } + + // TODO: Here it would be better to load it in the background. + // For some listeners are only interested in changing the board, not in libraries. + BaseNoGui.onBoardOrPortChange(); + + // Notify interested parties such as: rebuildExamplesMenu | PdeKeywords.reload() + EventBus.notify(new UIEvents.LibraryIndexUpdated()); + + // NOTE Before events: 'for' in editors[i].onBoardOrPortChange(); + EventBus.notify(new UIEvents.BoardOrPortChange()); + } public void openLibraryManager(final String filterText, String dropdownItem) { @@ -1393,10 +1643,8 @@ protected void onIndexesUpdated() throws Exception { // Manager dialog is modal, waits here until closed //handleAddLibrary(); - newLibraryImported = true; + getPdeKeywords().setNeedReload(true); onBoardOrPortChange(); - rebuildImportMenu(Editor.importMenu); - rebuildExamplesMenu(Editor.examplesMenu); } public void openBoardsManager(final String filterText, String dropdownItem) throws Exception { @@ -1434,121 +1682,132 @@ protected void onIndexesUpdated() throws Exception { public void rebuildBoardsMenu() throws Exception { boardsCustomMenus = new LinkedList<>(); - // The first custom menu is the "Board" selection submenu - JMenu boardMenu = new JMenu(tr("Board")); - boardMenu.putClientProperty("removeOnWindowDeactivation", true); - MenuScroller.setScrollerFor(boardMenu).setTopFixedCount(1); - - boardMenu.add(new JMenuItem(new AbstractAction(tr("Boards Manager...")) { - public void actionPerformed(ActionEvent actionevent) { - String filterText = ""; - String dropdownItem = ""; - if (actionevent instanceof Event) { - Event e = ((Event) actionevent); - filterText = e.getPayload().get("filterText").toString(); - dropdownItem = e.getPayload().get("dropdownItem").toString(); - } - try { - openBoardsManager(filterText, dropdownItem); - } catch (Exception e) { - //TODO show error - e.printStackTrace(); - } - } - })); - boardsCustomMenus.add(boardMenu); + // Execute in backgroud thread, no need UI thread because no rendering needed + menuExecutor.execute(() -> { + // The first custom menu is the "Board" selection submenu + JMenu boardMenu = new JMenu(tr("Board")); + boardMenu.putClientProperty("removeOnWindowDeactivation", true); + MenuScroller.setScrollerFor(boardMenu).setTopFixedCount(1); + + boardMenu.add(new JMenuItem(new AbstractAction(tr("Boards Manager...")) { + public void actionPerformed(ActionEvent actionevent) { + String filterText = ""; + String dropdownItem = ""; + if (actionevent instanceof Event) { + Event e = ((Event) actionevent); + filterText = e.getPayload().get("filterText").toString(); + dropdownItem = e.getPayload().get("dropdownItem").toString(); + } + try { + openBoardsManager(filterText, dropdownItem); + } catch (Exception e) { + //TODO show error + e.printStackTrace(); + } + } + })); + boardsCustomMenus.add(boardMenu); - // If there are no platforms installed we are done - if (BaseNoGui.packages.size() == 0) - return; + // If there are no platforms installed we are done + if (BaseNoGui.packages.size() == 0) + return; - // Separate "Install boards..." command from installed boards - boardMenu.add(new JSeparator()); - - // Generate custom menus for all platforms - for (TargetPackage targetPackage : BaseNoGui.packages.values()) { - for (TargetPlatform targetPlatform : targetPackage.platforms()) { - for (String customMenuTitle : targetPlatform.getCustomMenus().values()) { - JMenu customMenu = new JMenu(tr(customMenuTitle)); - customMenu.putClientProperty("platform", getPlatformUniqueId(targetPlatform)); - customMenu.putClientProperty("removeOnWindowDeactivation", true); - boardsCustomMenus.add(customMenu); + // Separate "Install boards..." command from installed boards + boardMenu.add(new JSeparator()); + + // Generate custom menus for all platforms + for (TargetPackage targetPackage : BaseNoGui.packages.values()) { + for (TargetPlatform targetPlatform : targetPackage.platforms()) { + for (String customMenuTitle : targetPlatform.getCustomMenus().values()) { + JMenu customMenu = new JMenu(tr(customMenuTitle)); + customMenu.putClientProperty("platform", getPlatformUniqueId(targetPlatform)); + customMenu.putClientProperty("removeOnWindowDeactivation", true); + boardsCustomMenus.add(customMenu); + } + } } - } - } - - List menuItemsToClickAfterStartup = new LinkedList<>(); - - ButtonGroup boardsButtonGroup = new ButtonGroup(); - Map buttonGroupsMap = new HashMap<>(); - List platformMenus = new ArrayList<>(); - - // Cycle through all packages - for (TargetPackage targetPackage : BaseNoGui.packages.values()) { - // For every package cycle through all platform - for (TargetPlatform targetPlatform : targetPackage.platforms()) { - - // Add a title for each platform - String platformLabel = targetPlatform.getPreferences().get("name"); - if (platformLabel == null) - platformLabel = targetPackage.getId() + "-" + targetPlatform.getId(); - - // add an hint that this core lives in sketchbook - if (targetPlatform.isInSketchbook()) - platformLabel += " (in sketchbook)"; - - JMenu platformBoardsMenu = new JMenu(platformLabel); - MenuScroller.setScrollerFor(platformBoardsMenu); - platformMenus.add(platformBoardsMenu); - - // Cycle through all boards of this platform - for (TargetBoard board : targetPlatform.getBoards().values()) { - if (board.getPreferences().get("hide") != null) - continue; - JMenuItem item = createBoardMenusAndCustomMenus(boardsCustomMenus, menuItemsToClickAfterStartup, - buttonGroupsMap, - board, targetPlatform, targetPackage); - platformBoardsMenu.add(item); - boardsButtonGroup.add(item); + List menuItemsToClickAfterStartup = new LinkedList<>(); + + ButtonGroup boardsButtonGroup = new ButtonGroup(); + Map buttonGroupsMap = new HashMap<>(); + + List platformMenus = new ArrayList<>(); + + // Cycle through all packages + for (TargetPackage targetPackage : BaseNoGui.packages.values()) { + // For every package cycle through all platform + for (TargetPlatform targetPlatform : targetPackage.platforms()) { + + // Add a title for each platform + String platformLabel = targetPlatform.getPreferences().get("name"); + if (platformLabel == null) + platformLabel = targetPackage.getId() + "-" + targetPlatform.getId(); + + // add an hint that this core lives in sketchbook + if (targetPlatform.isInSketchbook()) + platformLabel += " (in sketchbook)"; + + JMenu platformBoardsMenu = new JMenu(platformLabel); + MenuScroller.setScrollerFor(platformBoardsMenu); + platformMenus.add(platformBoardsMenu); + + // Cycle through all boards of this platform + for (TargetBoard board : targetPlatform.getBoards().values()) { + if (board.getPreferences().get("hide") != null) + continue; + + try { + JMenuItem item = createBoardMenusAndCustomMenus(boardsCustomMenus, menuItemsToClickAfterStartup, + buttonGroupsMap, + board, targetPlatform, targetPackage); + platformBoardsMenu.add(item); + boardsButtonGroup.add(item); + + } catch (Exception e) { + e.printStackTrace(); + } + } + } } - } - } - platformMenus.sort((x,y) -> x.getText().compareToIgnoreCase(y.getText())); + platformMenus.sort((x,y) -> x.getText().compareToIgnoreCase(y.getText())); - JMenuItem firstBoardItem = null; - if (platformMenus.size() == 1) { - // When just one platform exists, add the board items directly, - // rather than using a submenu - for (Component boardItem : platformMenus.get(0).getMenuComponents()) { - boardMenu.add(boardItem); - if (firstBoardItem == null) - firstBoardItem = (JMenuItem)boardItem; - } - } else { - // For multiple platforms, use submenus - for (JMenu platformMenu : platformMenus) { - if (firstBoardItem == null && platformMenu.getItemCount() > 0) - firstBoardItem = platformMenu.getItem(0); - boardMenu.add(platformMenu); - } - } + JMenuItem firstBoardItem = null; + if (platformMenus.size() == 1) { + // When just one platform exists, add the board items directly, + // rather than using a submenu + for (Component boardItem : platformMenus.get(0).getMenuComponents()) { + boardMenu.add(boardItem); + if (firstBoardItem == null) + firstBoardItem = (JMenuItem)boardItem; + } + } else { + // For multiple platforms, use submenus + for (JMenu platformMenu : platformMenus) { + if (firstBoardItem == null && platformMenu.getItemCount() > 0) + firstBoardItem = platformMenu.getItem(0); + boardMenu.add(platformMenu); + } + } - if (firstBoardItem == null) { - throw new IllegalStateException("No available boards"); - } + if (firstBoardItem == null) { + throw new IllegalStateException("No available boards"); + } - // If there is no current board yet (first startup, or selected - // board no longer defined), select first available board. - if (menuItemsToClickAfterStartup.isEmpty()) { - menuItemsToClickAfterStartup.add(firstBoardItem); - } + // If there is no current board yet (first startup, or selected + // board no longer defined), select first available board. + if (menuItemsToClickAfterStartup.isEmpty()) { + menuItemsToClickAfterStartup.add(firstBoardItem); + } - for (JMenuItem menuItemToClick : menuItemsToClickAfterStartup) { - menuItemToClick.setSelected(true); - menuItemToClick.getAction().actionPerformed(new ActionEvent(this, -1, "")); - } + for (JMenuItem menuItemToClick : menuItemsToClickAfterStartup) { + menuItemToClick.setSelected(true); + menuItemToClick.getAction().actionPerformed(new ActionEvent(this, -1, "")); + } + + }); + } private String getPlatformUniqueId(TargetPlatform platform) { @@ -1572,12 +1831,12 @@ private JRadioButtonMenuItem createBoardMenusAndCustomMenus( @SuppressWarnings("serial") Action action = new AbstractAction(board.getName()) { public void actionPerformed(ActionEvent actionevent) { + BaseNoGui.selectBoard((TargetBoard) getValue("b")); + filterVisibilityOfSubsequentBoardMenus(boardsCustomMenus, (TargetBoard) getValue("b"), 1); onBoardOrPortChange(); - rebuildImportMenu(Editor.importMenu); - rebuildExamplesMenu(Editor.examplesMenu); rebuildProgrammerMenu(); } }; @@ -1601,8 +1860,11 @@ public void actionPerformed(ActionEvent actionevent) { @SuppressWarnings("serial") Action subAction = new AbstractAction(tr(boardCustomMenu.get(customMenuOption))) { public void actionPerformed(ActionEvent e) { + PreferencesData.set("custom_" + menuId, ((List) getValue("board")).get(0).getId() + "_" + getValue("custom_menu_option")); - onBoardOrPortChange(); + + EventBus.notify(new UIEvents.MenuBoardSettingsChanged(((List) getValue("board")).get(0), (String) getValue("custom_menu_option"))); + } }; List boards = (List) subAction.getValue("board"); @@ -1631,7 +1893,7 @@ public void actionPerformed(ActionEvent e) { return item; } - + private void filterVisibilityOfSubsequentBoardMenus(List boardsCustomMenus, TargetBoard board, int fromIndex) { for (int i = fromIndex; i < boardsCustomMenus.size(); i++) { @@ -1768,7 +2030,7 @@ public int compare(File file, File file2) { boolean ifound = false; for (File subfolder : files) { - if (!FileUtils.isSCCSOrHiddenFile(subfolder) && subfolder.isDirectory() + if (subfolder.isDirectory() && !FileUtils.isSCCSOrHiddenFile(subfolder) && addSketchesSubmenu(menu, subfolder.getName(), subfolder)) { ifound = true; } @@ -1777,7 +2039,22 @@ && addSketchesSubmenu(menu, subfolder.getName(), subfolder)) { } private boolean addSketchesSubmenu(JMenu menu, UserLibrary lib) { - return addSketchesSubmenu(menu, lib.getName(), lib.getInstalledFolder()); + + JMenu submenu = new JMenu(lib.getName()); + + // Compatibility mode: not all community libraries are following the specification, look for common names found. + File[] list = lib.getInstalledFolder().listFiles(new ExamplesFilter()); + + // By spec only exist 1 Examples folder, on top level. + if (list.length > 0) { + if (addSketches(submenu, list[0])) { + menu.add(submenu); + MenuScroller.setScrollerFor(submenu); + return true; + } + } + + return false; } private boolean addSketchesSubmenu(JMenu menu, String name, File folder) { @@ -1844,34 +2121,6 @@ public void actionPerformed(ActionEvent e) { return found; } - protected void addLibraries(JMenu menu, LibraryList libs) throws IOException { - - LibraryList list = new LibraryList(libs); - list.sort(); - - for (UserLibrary lib : list) { - @SuppressWarnings("serial") - AbstractAction action = new AbstractAction(lib.getName()) { - public void actionPerformed(ActionEvent event) { - UserLibrary l = (UserLibrary) getValue("library"); - try { - activeEditor.getSketchController().importLibrary(l); - } catch (IOException e) { - showWarning(tr("Error"), format("Unable to list header files in {0}", l.getSrcFolder()), e); - } - } - }; - action.putValue("library", lib); - - // Add new element at the bottom - JMenuItem item = new JMenuItem(action); - item.putClientProperty("library", lib); - menu.add(item); - - // XXX: DAM: should recurse here so that library folders can be nested - } - } - /** * Given a folder, return a list of the header files in that folder (but not * the header files in its sub-folders, as those should be included from @@ -2132,7 +2381,7 @@ static public File selectFolder(String prompt, File folder, Component parent) { /** * Give this Frame an icon. */ - static public void setIcon(Frame frame) { + static public void setIcon(Window frame) { if (OSUtils.isMacOS()) { return; } @@ -2177,7 +2426,7 @@ static public void showReference(String prefix, String filename) { if (!referenceFile.exists()) referenceFile = new File(referenceFolder, filename + ".html"); - if(referenceFile.exists()){ + if (referenceFile.exists()){ openURL(referenceFile.getAbsolutePath()); }else{ showWarning(tr("Problem Opening URL"), format(tr("Could not open the URL\n{0}"), referenceFile), null); @@ -2474,7 +2723,7 @@ public void handleAddLibrary() { // FIXME error when importing. ignoring :( } finally { // delete zip created temp folder, if exists - newLibraryImported = true; + getPdeKeywords().setNeedReload(true); FileUtils.recursiveDelete(tmpFolder); } } @@ -2486,6 +2735,19 @@ public static DiscoveryManager getDiscoveryManager() { public Editor getActiveEditor() { return activeEditor; } + + public Editor getWaitActiveEditor() { + while (getActiveEditor() == null) { + try { + Thread.sleep(200); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + return getActiveEditor(); + } + + public boolean hasActiveEditor() { return activeEditor != null; @@ -2496,6 +2758,7 @@ public List getEditors() { } public PdeKeywords getPdeKeywords() { + if (pdeKeywords == null) pdeKeywords = new PdeKeywords() ; // make method safe, while load in backgroud.. return pdeKeywords; } diff --git a/app/src/processing/app/Editor.java b/app/src/processing/app/Editor.java index 2ec29c498cb..2f84b4fd240 100644 --- a/app/src/processing/app/Editor.java +++ b/app/src/processing/app/Editor.java @@ -80,12 +80,11 @@ import javax.swing.event.MenuEvent; import javax.swing.event.MenuListener; import javax.swing.text.BadLocationException; -import javax.swing.text.Document; -import javax.swing.text.Element; import org.fife.ui.rsyntaxtextarea.folding.FoldManager; import com.jcraft.jsch.JSchException; +import com.ricardojlrufino.eventbus.EventBus; import cc.arduino.CompilerProgressListener; import cc.arduino.packages.BoardPort; @@ -93,6 +92,7 @@ import cc.arduino.packages.Uploader; import cc.arduino.packages.uploaders.SerialUploader; import cc.arduino.view.GoToLineNumber; +import cc.arduino.view.JMenuLazy; import cc.arduino.view.StubMenuListener; import cc.arduino.view.findreplace.FindReplace; import jssc.SerialPortException; @@ -104,6 +104,7 @@ import processing.app.helpers.OSUtils; import processing.app.helpers.PreferencesMapException; import processing.app.helpers.StringReplacer; +import processing.app.helpers.UIEvents; import processing.app.legacy.PApplet; import processing.app.syntax.PdeKeywords; import processing.app.syntax.SketchTextArea; @@ -189,8 +190,8 @@ public boolean test(SketchController controller) { // these menus are shared so that they needn't be rebuilt for all windows // each time a sketch is created, renamed, or moved. static JMenu toolbarMenu; - static JMenu sketchbookMenu; - static JMenu examplesMenu; + static JMenuLazy sketchbookMenu; + static JMenuLazy examplesMenu; static JMenu importMenu; private static JMenu portMenu; @@ -381,6 +382,25 @@ public void windowDeactivated(WindowEvent e) { // default the console output to the last opened editor EditorConsole.setCurrentEditorConsole(console); + + // Init EventBus handlers + registerEventBus(); + } + + + private void registerEventBus() { + + EventBus.register(this, UIEvents.BoardOrPortChange.class, event -> { + onBoardOrPortChange(); + }); + + } + + @Override + public void dispose() { + super.dispose(); + EventBus.unregisterHandlers(this); + EventBus.notify(new UIEvents.EditorClosed()); } @@ -600,16 +620,15 @@ private JMenu buildFileMenu() { fileMenu.add(recentSketchesMenu); if (sketchbookMenu == null) { - sketchbookMenu = new JMenu(tr("Sketchbook")); + sketchbookMenu = new JMenuLazy(tr("Sketchbook")); MenuScroller.setScrollerFor(sketchbookMenu); base.rebuildSketchbookMenu(sketchbookMenu); } fileMenu.add(sketchbookMenu); if (examplesMenu == null) { - examplesMenu = new JMenu(tr("Examples")); + examplesMenu = new JMenuLazy(tr("Examples")); MenuScroller.setScrollerFor(examplesMenu); - base.rebuildExamplesMenu(examplesMenu); } fileMenu.add(examplesMenu); @@ -1003,7 +1022,7 @@ private void addInternalTools(JMenu menu) { private void selectSerialPort(String name) { - if(portMenu == null) { + if (portMenu == null) { System.out.println(tr("serialMenu is null")); return; } @@ -2166,8 +2185,8 @@ public void run() { } public void handleSerial() { - if(serialPlotter != null) { - if(serialPlotter.isClosed()) { + if (serialPlotter != null) { + if (serialPlotter.isClosed()) { serialPlotter = null; } else { statusError(tr("Serial monitor not available while plotter is open")); @@ -2276,8 +2295,8 @@ public void handleSerial() { } public void handlePlotter() { - if(serialMonitor != null) { - if(serialMonitor.isClosed()) { + if (serialMonitor != null) { + if (serialMonitor.isClosed()) { serialMonitor = null; } else { statusError(tr("Plotter not available while serial monitor is open")); diff --git a/app/src/processing/app/NewBoardListener.java b/app/src/processing/app/NewBoardListener.java index 7e0fe61d708..79938b5b92f 100644 --- a/app/src/processing/app/NewBoardListener.java +++ b/app/src/processing/app/NewBoardListener.java @@ -55,13 +55,7 @@ public void propertyChange(PropertyChangeEvent event) { @Override public void run() { - while (base.getActiveEditor() == null) { - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } + base.getWaitActiveEditor(); BaseNoGui.addPropertyChangeListener(this); checkForNewBoardAttached(); } diff --git a/app/src/processing/app/helpers/UIEvents.java b/app/src/processing/app/helpers/UIEvents.java new file mode 100755 index 00000000000..6f6e234cfec --- /dev/null +++ b/app/src/processing/app/helpers/UIEvents.java @@ -0,0 +1,73 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* + PdeKeywords - handles text coloring and links to html reference + Part of the Processing project - http://processing.org + + Copyright (c) 2004-06 Ben Fry and Casey Reas + Copyright (c) 2001-04 Massachusetts Institute of Technology + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software Foundation, + Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +*/ + +package processing.app.helpers; + +import com.ricardojlrufino.eventbus.EventMessage; + +import processing.app.debug.TargetBoard; + +/** + * Events for UI Interface. + * + * @author Ricardo JL Rufino - (ricardo.jl.rufino@gmail.com) + * @date 21 de mai de 2020 + */ +public interface UIEvents { + + public static class BoardOrPortChange implements EventMessage { } + + public static class LibraryIndexUpdated implements EventMessage {} + + public static class PlatformChage implements EventMessage {} + + public static class EditorClosed implements EventMessage {} + + /** + * Please do not use this event. It should only be used internally by the + * Base.onBoardOrPortChange method. Use: {@link BoardOrPortChange} + **/ + public static class TriggerFixBoardOrPortChange implements EventMessage { + } + + public static class MenuBoardSettingsChanged implements EventMessage { + private String custosOption; + private TargetBoard board; + + public MenuBoardSettingsChanged(TargetBoard board, String custosOption) { + this.board = board; + this.custosOption = custosOption; + } + + public TargetBoard getBoard() { + return board; + } + + public String getCustosOption() { + return custosOption; + } + + } + +} diff --git a/app/src/processing/app/syntax/PdeKeywords.java b/app/src/processing/app/syntax/PdeKeywords.java index 838800b3d5d..4fa394af067 100644 --- a/app/src/processing/app/syntax/PdeKeywords.java +++ b/app/src/processing/app/syntax/PdeKeywords.java @@ -45,6 +45,8 @@ public class PdeKeywords { private static final Map KNOWN_TOKEN_TYPES = new HashMap<>(); private static final Pattern ALPHA = Pattern.compile("\\w"); + + private boolean needReload = true; static { KNOWN_TOKEN_TYPES.put("RESERVED_WORD", TokenTypes.RESERVED_WORD); @@ -99,6 +101,23 @@ public void reload() { Base.showError("Problem loading keywords", "Could not load keywords.txt,\nplease re-install Arduino.", e); System.exit(1); } + + needReload = false; + } + + public boolean reloadIfNeed() { + + if (needReload) { + reload(); + return true; + }else { + return false; + } + + } + + public void setNeedReload(boolean needReload) { + this.needReload = needReload; } private void parseKeywordsTxt(File input) throws Exception { @@ -129,6 +148,7 @@ private void parseKeywordsTxt(File input) throws Exception { if (pieces.length >= 3) { parseHTMLReferenceFileName(pieces[2], keyword); } + if (pieces.length >= 4) { parseRSyntaxTextAreaTokenType(pieces[3], keyword); } @@ -173,9 +193,11 @@ private void parseRSyntaxTextAreaTokenType(String tokenTypeAsString, String keyw } private void parseHTMLReferenceFileName(String piece, String keyword) { - String htmlFilename = piece.trim(); - if (htmlFilename.length() > 0) { - keywordToReference.put(keyword, htmlFilename); + if (piece != null && keyword != null && !piece.isEmpty()) { + String htmlFilename = piece.trim(); + if (htmlFilename.length() > 0) { + keywordToReference.put(keyword, htmlFilename); + } } } diff --git a/app/src/processing/app/tools/MenuScroller.java b/app/src/processing/app/tools/MenuScroller.java index 9e9aacbcafd..b403f12a356 100644 --- a/app/src/processing/app/tools/MenuScroller.java +++ b/app/src/processing/app/tools/MenuScroller.java @@ -289,7 +289,7 @@ public MenuScroller(JPopupMenu menu, int scrollCount, int interval, upItem = new MenuScrollItem(MenuIcon.UP, -1); downItem = new MenuScrollItem(MenuIcon.DOWN, +1); - setScrollCount(scrollCount); + this.scrollCount = scrollCount; setInterval(interval); setTopFixedCount(topFixedCount); setBottomFixedCount(bottomFixedCount); diff --git a/app/test/cc/arduino/contributions/UpdatableLibraryTest.java b/app/test/cc/arduino/contributions/UpdatableLibraryTest.java index e06c12710bf..0dab3531cd1 100644 --- a/app/test/cc/arduino/contributions/UpdatableLibraryTest.java +++ b/app/test/cc/arduino/contributions/UpdatableLibraryTest.java @@ -36,8 +36,7 @@ public void testUpdatableLibrary() throws Exception { LibrariesIndexer indexer = new LibrariesIndexer(index_SD_only); BaseNoGui.librariesIndexer = indexer; indexer.parseIndex(); - indexer.setLibrariesFolders(folders); - indexer.rescanLibraries(); + indexer.setLibrariesFoldersAndRescan(folders); ContributedLibrary sdLib = indexer.getIndex().getInstalled("SD").get(); assertTrue("SD lib is installed", sdLib.isLibraryInstalled()); @@ -46,7 +45,7 @@ public void testUpdatableLibrary() throws Exception { assertTrue(ContributionsSelfCheck.checkForUpdatableLibraries()); folders.add(new UserLibraryFolder(SD121, Location.SKETCHBOOK)); - indexer.setLibrariesFolders(folders); + indexer.setLibrariesFoldersAndRescan(folders); sdLib = indexer.getIndex().getInstalled("SD").get(); assertTrue("SD lib is installed", sdLib.isLibraryInstalled()); @@ -63,8 +62,7 @@ public void testUpdatableLibraryWithBundled() throws Exception { LibrariesIndexer indexer = new LibrariesIndexer(index_Bridge_only); BaseNoGui.librariesIndexer = indexer; indexer.parseIndex(); - indexer.setLibrariesFolders(folders); - indexer.rescanLibraries(); + indexer.setLibrariesFoldersAndRescan(folders); ContributedLibrary l = indexer.getIndex().getInstalled("Bridge").get(); assertTrue("Bridge lib is installed", l.isLibraryInstalled()); @@ -73,7 +71,7 @@ public void testUpdatableLibraryWithBundled() throws Exception { assertTrue(ContributionsSelfCheck.checkForUpdatableLibraries()); folders.add(new UserLibraryFolder(Bridge170, Location.SKETCHBOOK)); - indexer.setLibrariesFolders(folders); + indexer.setLibrariesFoldersAndRescan(folders); l = indexer.getIndex().getInstalled("Bridge").get(); assertTrue("Bridge lib is installed", l.isLibraryInstalled()); diff --git a/arduino-core/src/cc/arduino/contributions/libraries/LibrariesIndexer.java b/arduino-core/src/cc/arduino/contributions/libraries/LibrariesIndexer.java index fb0b0c76abe..20169af056d 100644 --- a/arduino-core/src/cc/arduino/contributions/libraries/LibrariesIndexer.java +++ b/arduino-core/src/cc/arduino/contributions/libraries/LibrariesIndexer.java @@ -113,9 +113,13 @@ private void parseIndex(File file) throws IOException { } } - public void setLibrariesFolders(List folders) { - librariesFolders = folders; - rescanLibraries(); + public void setLibrariesFolders( List folders ) { + this.librariesFolders = folders; + } + + public void setLibrariesFoldersAndRescan( List folders ) { + setLibrariesFolders(folders); + rescanLibraries(); } public List getLibrariesFolders() { @@ -142,7 +146,7 @@ public void setArchitecturePriority(String arch) { priorityComparator = new UserLibraryPriorityComparator(arch); } - public void rescanLibraries() { + public synchronized void rescanLibraries() { // Clear all installed flags installedLibraries.clear(); diff --git a/arduino-core/src/processing/app/BaseNoGui.java b/arduino-core/src/processing/app/BaseNoGui.java index c47a82d69b8..4cf07691bb1 100644 --- a/arduino-core/src/processing/app/BaseNoGui.java +++ b/arduino-core/src/processing/app/BaseNoGui.java @@ -678,6 +678,7 @@ static public void onBoardOrPortChange() { if (getTargetPlatform() != null) { librariesIndexer.setArchitecturePriority(getTargetPlatform().getId()); } + librariesIndexer.rescanLibraries(); populateImportToLibraryTable(); diff --git a/arduino-core/src/processing/app/helpers/filefilters/ExamplesFilter.java b/arduino-core/src/processing/app/helpers/filefilters/ExamplesFilter.java new file mode 100755 index 00000000000..8bf90e699c6 --- /dev/null +++ b/arduino-core/src/processing/app/helpers/filefilters/ExamplesFilter.java @@ -0,0 +1,16 @@ +package processing.app.helpers.filefilters; + +import java.io.File; + +public class ExamplesFilter extends OnlyDirs { + + + @Override + public boolean accept( File dir , String name ) { + + if (!super.accept(dir, name)) return false; + + return name.equalsIgnoreCase("example") || name.equalsIgnoreCase("examples") ; + } + +}