diff --git a/app/src/processing/app/Editor.java b/app/src/processing/app/Editor.java index f0c7c19248c..e7ce2229ec7 100644 --- a/app/src/processing/app/Editor.java +++ b/app/src/processing/app/Editor.java @@ -110,6 +110,15 @@ import processing.app.tools.MenuScroller; import processing.app.tools.Tool; +import processing.app.tools.WatchDir; +import static java.nio.file.StandardWatchEventKinds.*; +import java.nio.file.WatchService; +import java.nio.file.WatchKey; +import java.nio.file.WatchEvent; +import java.nio.file.FileSystems; +import java.nio.file.Path; +import java.io.File; + /** * Main editor panel for the Processing Development Environment. */ @@ -124,6 +133,7 @@ public class Editor extends JFrame implements RunnerListener { private final Box upper; private ArrayList tabs = new ArrayList<>(); private int currentTabIndex = -1; + private static boolean watcherDisable = false; private static class ShouldSaveIfModified implements Predicate { @@ -238,6 +248,8 @@ public boolean test(SketchController controller) { private Runnable timeoutUploadHandler; private Map internalToolCache = new HashMap(); + protected Thread watcher = null; + protected Runnable task = null; public Editor(Base ibase, File file, int[] storedLocation, int[] defaultLocation, Platform platform) throws Exception { super("Arduino"); @@ -263,12 +275,20 @@ public void windowClosing(WindowEvent e) { // When bringing a window to front, let the Base know addWindowListener(new WindowAdapter() { public void windowActivated(WindowEvent e) { + if (watcher != null) { + watcher.interrupt(); + watcher = null; + } base.handleActivated(Editor.this); } // added for 1.0.5 // http://dev.processing.org/bugs/show_bug.cgi?id=1260 public void windowDeactivated(WindowEvent e) { + if (watcher == null) { + watcher = new Thread(task); + watcher.start(); + } List toolsMenuItemsToRemove = new LinkedList<>(); for (Component menuItem : toolsMenu.getMenuComponents()) { if (menuItem instanceof JComponent) { @@ -383,7 +403,6 @@ public void windowDeactivated(WindowEvent e) { EditorConsole.setCurrentEditorConsole(console); } - /** * Handles files dragged & dropped from the desktop and into the editor * window. Dragging files into the editor window is the same as using @@ -1534,7 +1553,7 @@ public void reorderTabs() { * the given file. * @throws IOException */ - protected void addTab(SketchFile file, String contents) throws IOException { + public synchronized void addTab(SketchFile file, String contents) throws IOException { EditorTab tab = new EditorTab(this, file, contents); tab.getTextArea().getDocument() .addDocumentListener(new DocumentTextChangeListener( @@ -1543,9 +1562,12 @@ protected void addTab(SketchFile file, String contents) throws IOException { reorderTabs(); } - protected void removeTab(SketchFile file) throws IOException { + public synchronized void removeTab(SketchFile file) throws IOException { int index = findTabIndex(file); tabs.remove(index); + if (index == currentTabIndex) { + currentTabIndex = currentTabIndex -1; + } } // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . @@ -1821,6 +1843,25 @@ protected boolean handleOpenInternal(File sketchFile) { // Disable untitled setting from previous document, if any untitled = false; + if (watcherDisable == true) { + return true; + } + + // Add FS watcher for current Editor instance + Path dir = file.toPath().getParent(); + + Editor instance = this; + + task = new Runnable() { + public void run() { + try { + new WatchDir(dir, true).processEvents(instance); + } catch (IOException x) { + watcherDisable = true; + } + } + }; + // opening was successful return true; } diff --git a/app/src/processing/app/EditorTab.java b/app/src/processing/app/EditorTab.java index 5e8f3e4bfcf..3b676c991e3 100644 --- a/app/src/processing/app/EditorTab.java +++ b/app/src/processing/app/EditorTab.java @@ -49,6 +49,14 @@ import javax.swing.text.DefaultCaret; import javax.swing.text.Document; +import static java.nio.file.StandardWatchEventKinds.*; +import java.nio.file.WatchService; +import java.nio.file.WatchKey; +import java.nio.file.WatchEvent; +import java.nio.file.FileSystems; +import java.nio.file.Path; +import java.io.File; + import org.apache.commons.lang3.StringUtils; import org.fife.ui.rsyntaxtextarea.RSyntaxDocument; import org.fife.ui.rsyntaxtextarea.RSyntaxTextAreaEditorKit; @@ -63,6 +71,7 @@ import processing.app.syntax.SketchTextArea; import processing.app.syntax.SketchTextAreaEditorKit; import processing.app.tools.DiscourseFormat; +import processing.app.tools.WatchDir; /** * Single tab, editing a single file, in the main window. @@ -486,7 +495,11 @@ public void setSelection(int start, int stop) { public int getScrollPosition() { return scrollPane.getVerticalScrollBar().getValue(); } - + + public RTextScrollPane getScrollPane() { + return scrollPane; + } + public void setScrollPosition(int pos) { scrollPane.getVerticalScrollBar().setValue(pos); } diff --git a/app/src/processing/app/tools/WatchDir.java b/app/src/processing/app/tools/WatchDir.java new file mode 100644 index 00000000000..8346c04014e --- /dev/null +++ b/app/src/processing/app/tools/WatchDir.java @@ -0,0 +1,198 @@ +/* + * Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of Oracle nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package processing.app.tools; + +import java.nio.file.*; +import static java.nio.file.StandardWatchEventKinds.*; +import static java.nio.file.LinkOption.*; +import java.nio.file.attribute.*; +import java.io.*; +import java.util.*; +import processing.app.Editor; +import processing.app.EditorTab; +import processing.app.Sketch; +import processing.app.SketchFile; +import processing.app.helpers.FileUtils; + +/** + * Example to watch a directory (or tree) for changes to files. + */ + +public class WatchDir { + + private final WatchService watcher; + private final Map keys; + private final boolean recursive; + private boolean trace = false; + + @SuppressWarnings("unchecked") + static WatchEvent cast(WatchEvent event) { + return (WatchEvent)event; + } + + /** + * Register the given directory with the WatchService + */ + private void register(Path dir) throws IOException { + WatchKey key = dir.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY); + if (trace) { + Path prev = keys.get(key); + if (prev == null) { + } else { + if (!dir.equals(prev)) { + } + } + } + keys.put(key, dir); + } + + /** + * Register the given directory, and all its sub-directories, with the + * WatchService. + */ + private void registerAll(final Path start) throws IOException { + // register directory and sub-directories + Files.walkFileTree(start, new SimpleFileVisitor() { + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) + throws IOException + { + register(dir); + return FileVisitResult.CONTINUE; + } + }); + } + + /** + * Creates a WatchService and registers the given directory + */ + public WatchDir(Path dir, boolean recursive) throws IOException { + this.watcher = FileSystems.getDefault().newWatchService(); + this.keys = new HashMap(); + this.recursive = recursive; + + if (recursive) { + registerAll(dir); + } else { + register(dir); + } + + // enable trace after initial registration + this.trace = true; + } + + /** + * Process all events for keys queued to the watcher + */ + public void processEvents(Editor editor) { + for (;;) { + + // wait for key to be signalled + WatchKey key; + try { + key = watcher.take(); + } catch (InterruptedException x) { + return; + } + + Path dir = keys.get(key); + if (dir == null) { + continue; + } + + for (WatchEvent event: key.pollEvents()) { + WatchEvent.Kind kind = event.kind(); + + // TBD - provide example of how OVERFLOW event is handled + if (kind == OVERFLOW) { + continue; + } + + // Context for directory entry event is the file name of entry + WatchEvent ev = cast(event); + Path name = ev.context(); + Path child = dir.resolve(name); + + // reload the tab content + if (kind == ENTRY_CREATE) { + try { + String filename = name.toString(); + FileUtils.SplitFile split = FileUtils.splitFilename(filename); + if (Sketch.EXTENSIONS.contains(split.extension.toLowerCase())) { + SketchFile sketch = editor.getSketch().addFile(filename); + editor.addTab(sketch, null); + } + } catch (Exception e) { + return; + } + } else if (kind == ENTRY_DELETE) { + try { + Thread.sleep(100); + int index = editor.getSketch().findFileIndex(child.toAbsolutePath().toFile()); + editor.removeTab(editor.getSketch().getFile(index)); + } catch (Exception e1) { + // Totally fine, if the sleep gets interrupted it means that + // the action was executed in the UI, not externally + return; + } + } + editor.getTabs().forEach(tab -> { + if (!tab.isModified()) { + tab.reload(); + } + }); + // if directory is created, and watching recursively, then + // register it and its sub-directories + if (recursive && (kind == ENTRY_CREATE)) { + try { + if (Files.isDirectory(child, NOFOLLOW_LINKS)) { + registerAll(child); + } + } catch (IOException x) { + // ignore to keep sample readbale + } + } + } + + // reset key and remove from set if directory no longer accessible + boolean valid = key.reset(); + if (!valid) { + keys.remove(key); + + // all directories are inaccessible + if (keys.isEmpty()) { + break; + } + } + } + } +}