diff --git a/app/src/processing/app/Editor.java b/app/src/processing/app/Editor.java index 500f9a0c498..d14c7672d23 100644 --- a/app/src/processing/app/Editor.java +++ b/app/src/processing/app/Editor.java @@ -44,6 +44,7 @@ import processing.app.syntax.SketchTextArea; import processing.app.tools.MenuScroller; import processing.app.tools.Tool; +import processing.app.tools.WatchDir; import javax.swing.*; import javax.swing.event.*; @@ -73,12 +74,19 @@ import static processing.app.Theme.scale; import processing.app.helpers.FileUtils; +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. */ @SuppressWarnings("serial") -public class Editor extends JFrame implements RunnerListener { +public class Editor extends JFrame implements RunnerListener, FocusListener { public static final int MAX_TIME_AWAITING_FOR_RESUMING_SERIAL_MONITOR = 10000; @@ -201,6 +209,8 @@ public boolean test(SketchController sketch) { 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"); @@ -345,6 +355,21 @@ public void windowDeactivated(WindowEvent e) { if (!loaded) sketchController = null; } + @Override + public void focusGained(FocusEvent fe){ + if (watcher != null) { + watcher.interrupt(); + watcher = null; + } + } + + @Override + public void focusLost(FocusEvent fe){ + if (watcher == null) { + watcher = new Thread(task); + watcher.start(); + } + } /** * Handles files dragged & dropped from the desktop and into the editor @@ -1686,7 +1711,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( @@ -1695,7 +1720,7 @@ 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); } @@ -1964,6 +1989,25 @@ protected boolean handleOpenInternal(File sketchFile) { // Disable untitled setting from previous document, if any untitled = false; + // 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) { + System.err.println(x); + } + } + }; + + addFocusListener(this); + getTabs().forEach(tab -> tab.getScrollPane().addFocusListener(this)); + getTabs().forEach(tab -> tab.getTextArea().addFocusListener(this)); + // opening was successful return true; } diff --git a/app/src/processing/app/EditorTab.java b/app/src/processing/app/EditorTab.java index e209dc691a6..8b81f66e96d 100644 --- a/app/src/processing/app/EditorTab.java +++ b/app/src/processing/app/EditorTab.java @@ -51,6 +51,14 @@ import javax.swing.text.Document; import org.fife.ui.autocomplete.AutoCompletion; +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.fife.ui.rsyntaxtextarea.RSyntaxDocument; import org.fife.ui.rsyntaxtextarea.RSyntaxTextAreaEditorKit; import org.fife.ui.rsyntaxtextarea.RSyntaxUtilities; @@ -66,6 +74,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. @@ -125,6 +134,8 @@ public EditorTab(Editor editor, SketchFile file, String contents) // ac.setParamChoicesRenderer(new CompletionsRenderer()); // ac.setListCellRenderer(new CompletionsRenderer()); ac.install(textarea); + setFocusable(true); + setRequestFocusEnabled(true); } private RSyntaxDocument createDocument(String contents) { @@ -487,7 +498,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..bc2f35e74c9 --- /dev/null +++ b/app/src/processing/app/tools/WatchDir.java @@ -0,0 +1,194 @@ +/* + * 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 (IOException e) {} + } else if (kind == ENTRY_DELETE) { + List tabs = editor.getTabs(); + Iterator iter = tabs.iterator(); + while (iter.hasNext()) { + EditorTab tab = iter.next(); + if (name.getFileName().toString().equals(tab.getSketchFile().getFileName())) { + try { + editor.removeTab(tab.getSketchFile()); + } catch (IOException e) {} + } + } + } + editor.getTabs().forEach(tab -> 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; + } + } + } + } +}