diff --git a/build.gradle b/build.gradle index 716adca..b6cb6db 100644 --- a/build.gradle +++ b/build.gradle @@ -1,107 +1,18 @@ -version = '1.0.0-SNAPSHOT' +subprojects { + apply plugin: 'java' + apply plugin: 'eclipse' -apply plugin: 'java' -apply plugin: 'eclipse' -apply plugin: 'maven' -apply plugin: 'signing' + version = '1.0.0-SNAPSHOT' -repositories { - mavenCentral() - maven { - url 'https://oss.sonatype.org/content/repositories/snapshots/' - } -} - -sourceCompatibility = '1.8' -targetCompatibility = '1.8' - -group = 'org.fxmisc.livedirs' - -dependencies { - compile group: 'org.reactfx', name: 'reactfx', version: '2.0-M4u1' -} - -javadoc { - // ignore missing Javadoc comments or tags - options.addStringOption('Xdoclint:all,-missing', '-quiet') -} - -task javadocJar(type: Jar, dependsOn: javadoc) { - classifier = 'javadoc' - from 'build/docs/javadoc' -} - -task sourcesJar(type: Jar) { - from sourceSets.main.allSource - classifier = 'sources' -} - -artifacts { - archives jar - - archives javadocJar - archives sourcesJar -} - -signing { - sign configurations.archives -} - -signArchives.onlyIf { - project.hasProperty('signing.keyId') && project.hasProperty('signing.password') && project.hasProperty('signing.secretKeyRingFile') -} - -def doUploadArchives = project.hasProperty('sonatypeUsername') && project.hasProperty('sonatypePassword') - -if(doUploadArchives) { - uploadArchives { - repositories.mavenDeployer { - beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } - - repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") { - authentication(userName: sonatypeUsername, password: sonatypePassword) - } - - snapshotRepository(url: 'https://oss.sonatype.org/content/repositories/snapshots') { - authentication(userName: sonatypeUsername, password: sonatypePassword) - } - - pom.project { - name 'LiveDirsFX' - packaging 'jar' - description 'Directory tree model for JavaFX that watches the filesystem for changes.' - url 'http://www.fxmisc.org/livedirs/' - - scm { - url 'scm:git@github.com:TomasMikula/LiveDirsFX.git' - connection 'scm:git@github.com:TomasMikula/LiveDirsFX.git' - developerConnection 'scm:git@github.com:TomasMikula/LiveDirsFX.git' - } - - licenses { - license { - name 'The BSD 2-Clause License' - url 'http://opensource.org/licenses/BSD-2-Clause' - distribution 'repo' - } - } - - developers { - developer { - name 'Tomas Mikula' - } - } - } + repositories { + mavenCentral() + maven { + url 'https://oss.sonatype.org/content/repositories/snapshots/' } } -} -uploadArchives.onlyIf { doUploadArchives } + sourceCompatibility = '1.8' + targetCompatibility = '1.8' -task fatJar(type: Jar, dependsOn: classes) { - appendix = 'fat' - from sourceSets.main.output - from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } + compileJava.options.deprecation = true } - -assemble.dependsOn fatJar diff --git a/livedirsfx-demo/build.gradle b/livedirsfx-demo/build.gradle new file mode 100644 index 0000000..fb23a20 --- /dev/null +++ b/livedirsfx-demo/build.gradle @@ -0,0 +1,27 @@ +group 'org.fxmisc.livedirs' + +dependencies { + compile project(":livedirsfx") + compile group: 'org.reactfx', name: 'reactfx', version: '2.0-M4u1' +} + +task fatJar(type: Jar, dependsOn: classes) { + appendix = 'fat' + from sourceSets.main.output + from { configurations.runtime.collect { it.isDirectory() ? it : zipTree(it) } } +} + +assemble.dependsOn fatJar + +task CheckBoxLiveDirs(type: JavaExec, dependsOn: classes) { + main = 'org.fxmisc.livedirs.demo.CheckBoxLiveDirs' + classpath = files(sourceSets.main.output, configurations.runtime) + description = 'Demonstrates a working LiveDirs instance that displays its items as though' + + 'they are CheckBoxTreeItems' +} + +task NormalLiveDirs(type: JavaExec, dependsOn: classes) { + main = 'org.fxmisc.livedirs.demo.NormalLiveDirs' + classpath = files(sourceSets.main.output, configurations.runtime) + description = 'Demonstrates a working LiveDirs instance that displays its items normally' +} \ No newline at end of file diff --git a/livedirsfx-demo/src/main/java/org/fxmisc/livedirs/demo/ChangeSource.java b/livedirsfx-demo/src/main/java/org/fxmisc/livedirs/demo/ChangeSource.java new file mode 100644 index 0000000..b50e807 --- /dev/null +++ b/livedirsfx-demo/src/main/java/org/fxmisc/livedirs/demo/ChangeSource.java @@ -0,0 +1,13 @@ +/** + * Created 2016 by Jordan Martinez + * + * The author dedicates this to the public domain + */ + +package org.fxmisc.livedirs.demo; + + +public enum ChangeSource { + INTERNAL, + EXTERNAL +} diff --git a/livedirsfx-demo/src/main/java/org/fxmisc/livedirs/demo/CheckBoxLiveDirs.java b/livedirsfx-demo/src/main/java/org/fxmisc/livedirs/demo/CheckBoxLiveDirs.java new file mode 100644 index 0000000..4c1feae --- /dev/null +++ b/livedirsfx-demo/src/main/java/org/fxmisc/livedirs/demo/CheckBoxLiveDirs.java @@ -0,0 +1,52 @@ +/** + * Created 2016 by Jordan Martinez + * + * The author dedicates this to the public domain + */ + +package org.fxmisc.livedirs.demo; + +import javafx.application.Application; +import javafx.application.Platform; +import javafx.scene.Scene; +import javafx.scene.control.TreeView; +import javafx.stage.Stage; +import org.fxmisc.livedirs.LiveDirs; +import org.fxmisc.livedirs.demo.checkbox.CheckBoxContentImpl; +import org.fxmisc.livedirs.demo.checkbox.TreeCellFactories; + +import java.io.IOException; +import java.nio.file.Paths; + +public class CheckBoxLiveDirs extends Application { + + public static void main(String[] args) { + launch(args); + } + + @Override + public void start(Stage primaryStage) { + TreeView view = new TreeView<>(); + view.setShowRoot(false); + view.setCellFactory(TreeCellFactories.checkBoxFactory()); + + try { + // create a LiveDirs instance for use on the JavaFX Application Thread + // and make it display its items as though they were CheckBoxTreeItems + LiveDirs dirs = new LiveDirs<>(ChangeSource.EXTERNAL, + CheckBoxContentImpl::getPath, CheckBoxContentImpl::new, Platform::runLater); + + // set directory to watch + dirs.addTopLevelDirectory(Paths.get(System.getProperty("user.home"), "Documents").toAbsolutePath()); + view.setRoot(dirs.model().getRoot()); + + // stop DirWatcher's thread + primaryStage.setOnCloseRequest(val -> dirs.dispose()); + } catch (IOException e) { + e.printStackTrace(); + } + + primaryStage.setScene(new Scene(view, 500, 500)); + primaryStage.show(); + } +} diff --git a/livedirsfx-demo/src/main/java/org/fxmisc/livedirs/demo/NormalLiveDirs.java b/livedirsfx-demo/src/main/java/org/fxmisc/livedirs/demo/NormalLiveDirs.java new file mode 100644 index 0000000..5e57c82 --- /dev/null +++ b/livedirsfx-demo/src/main/java/org/fxmisc/livedirs/demo/NormalLiveDirs.java @@ -0,0 +1,47 @@ +/** + * Created 2016 by Jordan Martinez + * + * The author dedicates this to the public domain + */ + +package org.fxmisc.livedirs.demo; + +import javafx.application.Application; +import javafx.scene.Scene; +import javafx.scene.control.TreeView; +import javafx.stage.Stage; +import org.fxmisc.livedirs.LiveDirs; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class NormalLiveDirs extends Application { + + public static void main(String[] args) { + launch(args); + } + + @Override + public void start(Stage primaryStage) { + TreeView view = new TreeView<>(); + view.setShowRoot(false); + + try { + // create a LiveDirs instance for use on the JavaFX Application Thread + LiveDirs dirs = LiveDirs.getInstance(ChangeSource.EXTERNAL); + + // set directory to watch + dirs.addTopLevelDirectory(Paths.get(System.getProperty("user.home"), "Documents").toAbsolutePath()); + view.setRoot(dirs.model().getRoot()); + + // stop DirWatcher's thread + primaryStage.setOnCloseRequest(val -> dirs.dispose()); + } catch (IOException e) { + e.printStackTrace(); + } + + primaryStage.setScene(new Scene(view, 500, 500)); + primaryStage.show(); + } +} diff --git a/livedirsfx-demo/src/main/java/org/fxmisc/livedirs/demo/checkbox/CheckBoxContent.java b/livedirsfx-demo/src/main/java/org/fxmisc/livedirs/demo/checkbox/CheckBoxContent.java new file mode 100644 index 0000000..21e031b --- /dev/null +++ b/livedirsfx-demo/src/main/java/org/fxmisc/livedirs/demo/checkbox/CheckBoxContent.java @@ -0,0 +1,33 @@ +/** + * Created 2016 by Jordan Martinez + * + * The author dedicates this to the public domain + */ + +package org.fxmisc.livedirs.demo.checkbox; + +import org.reactfx.value.Var; + +import java.nio.file.Path; + +public interface CheckBoxContent { + + /** The State of the {@link javafx.scene.control.CheckBox} */ + enum State { + UNCHECKED, + UNDEFINED, + CHECKED + } + + State getState(); + void setState(State value); + Var stateProperty(); + + Path getPath(); + void setPath(Path p); + + void lock(); + void unlock(); + boolean isLocked(); + +} diff --git a/livedirsfx-demo/src/main/java/org/fxmisc/livedirs/demo/checkbox/CheckBoxContentImpl.java b/livedirsfx-demo/src/main/java/org/fxmisc/livedirs/demo/checkbox/CheckBoxContentImpl.java new file mode 100644 index 0000000..b4ffee9 --- /dev/null +++ b/livedirsfx-demo/src/main/java/org/fxmisc/livedirs/demo/checkbox/CheckBoxContentImpl.java @@ -0,0 +1,35 @@ +/** + * Created 2016 by Jordan Martinez + * + * The author dedicates this to the public domain + */ + +package org.fxmisc.livedirs.demo.checkbox; + +import org.reactfx.value.Var; + +import java.nio.file.Path; + +/** + * A basic implementation of {@link CheckBoxContent} + */ +public class CheckBoxContentImpl implements CheckBoxContent { + + private final Var state = Var.newSimpleVar(State.UNCHECKED); + public final State getState() { return state.getValue(); } + public final void setState(State value) { state.setValue(value); } + public final Var stateProperty() { return state; } + + private Path path; + public final Path getPath() { return path; } + public final void setPath(Path p) { path = p; } + + private boolean locked = false; + public final boolean isLocked() { return locked; } + public final void lock() { locked = true; } + public final void unlock() { locked = false; } + + public CheckBoxContentImpl(Path p) { + path = p; + } +} diff --git a/livedirsfx-demo/src/main/java/org/fxmisc/livedirs/demo/checkbox/CheckBoxTreeCell.java b/livedirsfx-demo/src/main/java/org/fxmisc/livedirs/demo/checkbox/CheckBoxTreeCell.java new file mode 100644 index 0000000..2ea64b0 --- /dev/null +++ b/livedirsfx-demo/src/main/java/org/fxmisc/livedirs/demo/checkbox/CheckBoxTreeCell.java @@ -0,0 +1,133 @@ +/** + * Created 2016 by Jordan Martinez + * + * The author dedicates this to the public domain + */ + +package org.fxmisc.livedirs.demo.checkbox; + +import javafx.beans.InvalidationListener; +import javafx.scene.Node; +import javafx.scene.control.CheckBox; +import javafx.scene.control.TreeCell; +import javafx.scene.control.TreeItem; +import org.reactfx.Subscription; +import org.reactfx.value.Var; + +import java.util.function.BiConsumer; +import java.util.function.Function; + +public class CheckBoxTreeCell extends TreeCell { + + private final Recursive, CheckBoxContent.State>> UPDATE_DOWNWARDS = new Recursive<>(); + { + UPDATE_DOWNWARDS.f = (item, state) -> { + item.getValue().setState(state); + item.getChildren().forEach(child -> UPDATE_DOWNWARDS.f.accept(child, state)); + }; + } + + private final CheckBox checkBox = new CheckBox(); + private final Function stringConverter; + private Var state; + private Var select; + private Subscription intermediateState; + + private final InvalidationListener stateInvalidations = (obs) -> { + TreeItem treeItem = getTreeItem(); + if (treeItem != null) { + final TreeItem parentItem = treeItem.getParent(); + + // do upward call first + if (parentItem != null) { + CheckBoxContent value = parentItem.getValue(); + + if (value != null && !value.isLocked()) { + CheckBoxContent.State[] childrenStates = parentItem.getChildren() + .stream().map(v -> v.getValue().getState()) + .distinct() + .toArray(CheckBoxContent.State[]::new); + + /* + Due to `distinct()`, + if length > 1, + then children were 2+ of the 3 CheckBoxContent.State enum values + Thus, set to UNDEFINED + else + then children were all UNCHECKED or CHECKED. + Thus, set the current value to that State + */ + value.setState(childrenStates.length > 1 + ? CheckBoxContent.State.UNDEFINED + : childrenStates[0] + ); + } + } + + // then do downward call + C itemVal = treeItem.getValue(); + // when children's invalidation listeners are called, skip this item's update as it + // was the one the initiated the call. + itemVal.lock(); + CheckBoxContent.State state = itemVal.getState(); + if (state != CheckBoxContent.State.UNDEFINED) { + treeItem.getChildren().forEach(child -> UPDATE_DOWNWARDS.f.accept(child, state)); + } + // once finished, unlock so updates via one of its children will propogate through the tree + itemVal.unlock(); + } + }; + + public CheckBoxTreeCell() { + this((content) -> content.getPath().toString()); + } + + public CheckBoxTreeCell(Function stringConverter) { + super(); + this.stringConverter = stringConverter; + } + + @Override + protected void updateItem(C item, boolean empty) { + super.updateItem(item, empty); + + if (empty || item == null) { + setText(null); + checkBox.setGraphic(null); + setGraphic(null); + } else { + // update the text + setText(stringConverter.apply(getItem())); + + // update the graphic + TreeItem treeItem = getTreeItem(); + Node graphic = treeItem.getGraphic(); + checkBox.setGraphic( graphic != null ? graphic : null); + setGraphic(checkBox); + + // unbind properties + if (state != null) { + checkBox.selectedProperty().unbindBidirectional(select); + intermediateState.unsubscribe(); + state.removeListener(stateInvalidations); + } + + // rebind properties + state = treeItem.getValue().stateProperty(); + select = state.mapBidirectional( + s -> s == CheckBoxContent.State.CHECKED, + val -> val ? CheckBoxContent.State.CHECKED : CheckBoxContent.State.UNCHECKED + ); + checkBox.selectedProperty().bindBidirectional(select); + + // using checkBox.intermediateProperty().bind(state.map(s -> s == UNDEFINED)); results in a + // RunTimeException: a bounded property cannot be set + // So, get around it by feeding state values into it. + intermediateState = state.values() + .map(s -> s == CheckBoxContent.State.UNDEFINED) + .feedTo(checkBox.indeterminateProperty()); + + state.addListener(stateInvalidations); + } + } +} \ No newline at end of file diff --git a/livedirsfx-demo/src/main/java/org/fxmisc/livedirs/demo/checkbox/Recursive.java b/livedirsfx-demo/src/main/java/org/fxmisc/livedirs/demo/checkbox/Recursive.java new file mode 100644 index 0000000..adc53f4 --- /dev/null +++ b/livedirsfx-demo/src/main/java/org/fxmisc/livedirs/demo/checkbox/Recursive.java @@ -0,0 +1,19 @@ +/** + * Created 2016 by Jordan Martinez + * + * The author dedicates this to the public domain + */ + +package org.fxmisc.livedirs.demo.checkbox; + +/** + * Allows recursive {@link FunctionalInterface} calls. + * + *

See https://stackoverflow.com/questions/19429667/implement-recursive-lambda-function-using-java-8

+ * + * @param the {@link FunctionalInterface} to call recursively. + */ +public class Recursive { + + public I f; +} diff --git a/livedirsfx-demo/src/main/java/org/fxmisc/livedirs/demo/checkbox/TreeCellFactories.java b/livedirsfx-demo/src/main/java/org/fxmisc/livedirs/demo/checkbox/TreeCellFactories.java new file mode 100644 index 0000000..9c576ec --- /dev/null +++ b/livedirsfx-demo/src/main/java/org/fxmisc/livedirs/demo/checkbox/TreeCellFactories.java @@ -0,0 +1,32 @@ +/** + * Created 2016 by Jordan Martinez + * + * The author dedicates this to the public domain + */ + +package org.fxmisc.livedirs.demo.checkbox; + +import javafx.scene.control.TreeCell; +import javafx.scene.control.TreeView; +import javafx.util.Callback; + +import java.util.function.Function; + +/** + * A class that provides cell factories to help display a {@link org.fxmisc.livedirs.PathItem} + * as though it extended a subclass of {@link javafx.scene.control.TreeItem} + * or to provide additional functionality not present in the normal cell factory. + */ +public class TreeCellFactories { + + public static Callback, TreeCell> checkBoxFactory() { + return (view) -> new CheckBoxTreeCell(); + } + + public static Callback, TreeCell> checkBoxFactory( + Function stringConverter + ) { + return (view) -> new CheckBoxTreeCell<>(stringConverter); + } + +} diff --git a/livedirsfx/build.gradle b/livedirsfx/build.gradle new file mode 100644 index 0000000..5a6515a --- /dev/null +++ b/livedirsfx/build.gradle @@ -0,0 +1,93 @@ +apply plugin: 'maven' +apply plugin: 'signing' + +group = 'org.fxmisc.livedirs' + +dependencies { + compile group: 'org.reactfx', name: 'reactfx', version: '2.0-M4u1' +} + +javadoc { + // ignore missing Javadoc comments or tags + options.addStringOption('Xdoclint:all,-missing', '-quiet') +} + +task javadocJar(type: Jar, dependsOn: javadoc) { + classifier = 'javadoc' + from 'build/docs/javadoc' +} + +task sourcesJar(type: Jar) { + from sourceSets.main.allSource + classifier = 'sources' +} + +artifacts { + archives jar + + archives javadocJar + archives sourcesJar +} + +signing { + sign configurations.archives +} + +signArchives.onlyIf { + project.hasProperty('signing.keyId') && project.hasProperty('signing.password') && project.hasProperty('signing.secretKeyRingFile') +} + +def doUploadArchives = project.hasProperty('sonatypeUsername') && project.hasProperty('sonatypePassword') + +if(doUploadArchives) { + uploadArchives { + repositories.mavenDeployer { + beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } + + repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") { + authentication(userName: sonatypeUsername, password: sonatypePassword) + } + + snapshotRepository(url: 'https://oss.sonatype.org/content/repositories/snapshots') { + authentication(userName: sonatypeUsername, password: sonatypePassword) + } + + pom.project { + name 'LiveDirsFX' + packaging 'jar' + description 'Directory tree model for JavaFX that watches the filesystem for changes.' + url 'http://www.fxmisc.org/livedirs/' + + scm { + url 'scm:git@github.com:TomasMikula/LiveDirsFX.git' + connection 'scm:git@github.com:TomasMikula/LiveDirsFX.git' + developerConnection 'scm:git@github.com:TomasMikula/LiveDirsFX.git' + } + + licenses { + license { + name 'The BSD 2-Clause License' + url 'http://opensource.org/licenses/BSD-2-Clause' + distribution 'repo' + } + } + + developers { + developer { + name 'Tomas Mikula' + } + } + } + } + } +} + +uploadArchives.onlyIf { doUploadArchives } + +task fatJar(type: Jar, dependsOn: classes) { + appendix = 'fat' + from sourceSets.main.output + from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } +} + +assemble.dependsOn fatJar diff --git a/src/main/java/org/fxmisc/livedirs/CompletionStageWithDefaultExecutor.java b/livedirsfx/src/main/java/org/fxmisc/livedirs/CompletionStageWithDefaultExecutor.java similarity index 100% rename from src/main/java/org/fxmisc/livedirs/CompletionStageWithDefaultExecutor.java rename to livedirsfx/src/main/java/org/fxmisc/livedirs/CompletionStageWithDefaultExecutor.java diff --git a/src/main/java/org/fxmisc/livedirs/DirWatcher.java b/livedirsfx/src/main/java/org/fxmisc/livedirs/DirWatcher.java similarity index 100% rename from src/main/java/org/fxmisc/livedirs/DirWatcher.java rename to livedirsfx/src/main/java/org/fxmisc/livedirs/DirWatcher.java diff --git a/src/main/java/org/fxmisc/livedirs/DirectoryModel.java b/livedirsfx/src/main/java/org/fxmisc/livedirs/DirectoryModel.java similarity index 97% rename from src/main/java/org/fxmisc/livedirs/DirectoryModel.java rename to livedirsfx/src/main/java/org/fxmisc/livedirs/DirectoryModel.java index 9f73edd..4e42baa 100644 --- a/src/main/java/org/fxmisc/livedirs/DirectoryModel.java +++ b/livedirsfx/src/main/java/org/fxmisc/livedirs/DirectoryModel.java @@ -15,8 +15,9 @@ /** * Observable model of multiple directory trees. * @param type of initiator of changes to the model. + * @param type for {@link TreeItem#getValue()} */ -public interface DirectoryModel { +public interface DirectoryModel { /** * Factory to create graphics for {@link TreeItem}s in a @@ -108,7 +109,7 @@ public UpdateType getType() { * As a consequence, the returned TreeItem shall be used with * {@link TreeView#showRootProperty()} set to {@code false}. */ - TreeItem getRoot(); + TreeItem getRoot(); /** * Indicates whether this directory model contains the given path. diff --git a/src/main/java/org/fxmisc/livedirs/IOFacility.java b/livedirsfx/src/main/java/org/fxmisc/livedirs/IOFacility.java similarity index 100% rename from src/main/java/org/fxmisc/livedirs/IOFacility.java rename to livedirsfx/src/main/java/org/fxmisc/livedirs/IOFacility.java diff --git a/src/main/java/org/fxmisc/livedirs/InitiatorTrackingIOFacility.java b/livedirsfx/src/main/java/org/fxmisc/livedirs/InitiatorTrackingIOFacility.java similarity index 100% rename from src/main/java/org/fxmisc/livedirs/InitiatorTrackingIOFacility.java rename to livedirsfx/src/main/java/org/fxmisc/livedirs/InitiatorTrackingIOFacility.java diff --git a/src/main/java/org/fxmisc/livedirs/LiveDirs.java b/livedirsfx/src/main/java/org/fxmisc/livedirs/LiveDirs.java similarity index 82% rename from src/main/java/org/fxmisc/livedirs/LiveDirs.java rename to livedirsfx/src/main/java/org/fxmisc/livedirs/LiveDirs.java index fbbc313..e6d626a 100644 --- a/src/main/java/org/fxmisc/livedirs/LiveDirs.java +++ b/livedirsfx/src/main/java/org/fxmisc/livedirs/LiveDirs.java @@ -13,8 +13,10 @@ import java.util.List; import java.util.concurrent.CompletionStage; import java.util.concurrent.Executor; +import java.util.function.Function; import javafx.application.Platform; +import javafx.scene.control.TreeItem; import javafx.scene.control.TreeView; import org.reactfx.EventSource; @@ -34,41 +36,58 @@ *

The directory model can be used directly as a model for {@link TreeView}. * * @param type of the initiator of I/O actions. + * @param type for {@link TreeItem#getValue()} */ -public class LiveDirs { - - private final EventSource localErrors = new EventSource<>(); - private final EventStream errors; - private final Executor clientThreadExecutor; - private final DirWatcher dirWatcher; - private final LiveDirsModel model; - private final LiveDirsIO io; - private final I externalInitiator; +public class LiveDirs { /** * Creates a LiveDirs instance to be used from the JavaFX application * thread. + * + * @param externalInitiator object to represent an initiator of an external + * file-system change. + * @throws IOException + */ + public static LiveDirs getInstance(I externalInitiator) throws IOException { + return getInstance(externalInitiator, Platform::runLater); + } + + /** + * Creates a LiveDirs instance to be used from a designated thread. + * * @param externalInitiator object to represent an initiator of an external * file-system change. + * @param clientThreadExecutor executor to execute actions on the caller + * thread. Used to publish updates and errors on the caller thread. * @throws IOException */ - public LiveDirs(I externalInitiator) throws IOException { - this(externalInitiator, Platform::runLater); + public static LiveDirs getInstance(I externalInitiator, Executor clientThreadExecutor) throws IOException { + return new LiveDirs<>(externalInitiator, Function.identity(), Function.identity(), clientThreadExecutor); } + private final EventSource localErrors = new EventSource<>(); + private final EventStream errors; + private final Executor clientThreadExecutor; + private final DirWatcher dirWatcher; + private final LiveDirsModel model; + private final LiveDirsIO io; + private final I externalInitiator; + /** * Creates a LiveDirs instance to be used from a designated thread. + * @param projector converts the ({@link T}) {@link TreeItem#getValue()} into a {@link Path} object + * @param injector converts a given {@link Path} object into {@link T}. The reverse of {@code projector} * @param externalInitiator object to represent an initiator of an external * file-system change. * @param clientThreadExecutor executor to execute actions on the caller * thread. Used to publish updates and errors on the caller thread. * @throws IOException */ - public LiveDirs(I externalInitiator, Executor clientThreadExecutor) throws IOException { + public LiveDirs(I externalInitiator, Function projector, Function injector, Executor clientThreadExecutor) throws IOException { this.externalInitiator = externalInitiator; this.clientThreadExecutor = clientThreadExecutor; this.dirWatcher = new DirWatcher(clientThreadExecutor); - this.model = new LiveDirsModel<>(externalInitiator); + this.model = new LiveDirsModel<>(externalInitiator, projector, injector); this.io = new LiveDirsIO<>(dirWatcher, model, clientThreadExecutor); this.dirWatcher.signalledKeys().subscribe(this::processKey); @@ -83,7 +102,7 @@ public LiveDirs(I externalInitiator, Executor clientThreadExecutor) throws IOExc /** * Observable directory model. */ - public DirectoryModel model() { return model; } + public DirectoryModel model() { return model; } /** * Asynchronous I/O facility. All I/O operations performed by this facility @@ -226,7 +245,7 @@ private void refreshOrLogError(Path path) { }); } - private CompletionStage wrap(CompletionStage stage) { + private CompletionStage wrap(CompletionStage stage) { return new CompletionStageWithDefaultExecutor<>(stage, clientThreadExecutor); } } \ No newline at end of file diff --git a/src/main/java/org/fxmisc/livedirs/LiveDirsIO.java b/livedirsfx/src/main/java/org/fxmisc/livedirs/LiveDirsIO.java similarity index 96% rename from src/main/java/org/fxmisc/livedirs/LiveDirsIO.java rename to livedirsfx/src/main/java/org/fxmisc/livedirs/LiveDirsIO.java index 95b4eb2..260694b 100644 --- a/src/main/java/org/fxmisc/livedirs/LiveDirsIO.java +++ b/livedirsfx/src/main/java/org/fxmisc/livedirs/LiveDirsIO.java @@ -8,10 +8,10 @@ class LiveDirsIO implements InitiatorTrackingIOFacility { private final DirWatcher dirWatcher; - private final LiveDirsModel model; + private final LiveDirsModel model; private final Executor clientThreadExecutor; - public LiveDirsIO(DirWatcher dirWatcher, LiveDirsModel model, Executor clientThreadExecutor) { + public LiveDirsIO(DirWatcher dirWatcher, LiveDirsModel model, Executor clientThreadExecutor) { this.dirWatcher = dirWatcher; this.model = model; this.clientThreadExecutor = clientThreadExecutor; diff --git a/src/main/java/org/fxmisc/livedirs/LiveDirsModel.java b/livedirsfx/src/main/java/org/fxmisc/livedirs/LiveDirsModel.java similarity index 67% rename from src/main/java/org/fxmisc/livedirs/LiveDirsModel.java rename to livedirsfx/src/main/java/org/fxmisc/livedirs/LiveDirsModel.java index 17cd86a..dcbfd88 100644 --- a/src/main/java/org/fxmisc/livedirs/LiveDirsModel.java +++ b/livedirsfx/src/main/java/org/fxmisc/livedirs/LiveDirsModel.java @@ -4,6 +4,7 @@ import java.nio.file.attribute.FileTime; import java.util.Arrays; import java.util.List; +import java.util.function.Function; import java.util.stream.Stream; import javafx.scene.control.TreeItem; @@ -11,20 +12,24 @@ import org.reactfx.EventSource; import org.reactfx.EventStream; -class LiveDirsModel implements DirectoryModel { +class LiveDirsModel implements DirectoryModel { - private final TreeItem root = new TreeItem<>(); + private final TreeItem root = new TreeItem<>(); private final EventSource> creations = new EventSource<>(); private final EventSource> deletions = new EventSource<>(); private final EventSource> modifications = new EventSource<>(); private final EventSource errors = new EventSource<>(); private final Reporter reporter; private final I defaultInitiator; + private final Function projector; + private final Function injector; private GraphicFactory graphicFactory = DEFAULT_GRAPHIC_FACTORY; - public LiveDirsModel(I defaultInitiator) { + public LiveDirsModel(I defaultInitiator, Function projector, Function injector) { this.defaultInitiator = defaultInitiator; + this.projector = projector; + this.injector = injector; this.reporter = new Reporter() { @Override public void reportCreation(Path baseDir, Path relPath, I initiator) { @@ -48,7 +53,7 @@ public void reportError(Throwable error) { }; } - @Override public TreeItem getRoot() { return root; } + @Override public TreeItem getRoot() { return root; } @Override public EventStream> creations() { return creations; } @Override public EventStream> deletions() { return deletions; } @Override public EventStream> modifications() { return modifications; } @@ -63,42 +68,42 @@ public void setGraphicFactory(GraphicFactory factory) { @Override public boolean contains(Path path) { return topLevelAncestorStream(path).anyMatch(root -> - root.contains(root.getValue().relativize(path))); + root.contains(root.getPath().relativize(path))); } public boolean containsPrefixOf(Path path) { return root.getChildren().stream() - .anyMatch(item -> path.startsWith(item.getValue())); + .anyMatch(item -> path.startsWith(projector.apply(item.getValue()))); } void addTopLevelDirectory(Path dir) { - root.getChildren().add(new TopLevelDirItem<>(dir, graphicFactory, reporter)); + root.getChildren().add(new TopLevelDirItem<>(injector.apply(dir), graphicFactory, projector, injector, reporter)); } void updateModificationTime(Path path, FileTime lastModified, I initiator) { - for(TopLevelDirItem root: getTopLevelAncestorsNonEmpty(path)) { - Path relPath = root.getValue().relativize(path); + for(TopLevelDirItem root: getTopLevelAncestorsNonEmpty(path)) { + Path relPath = root.getPath().relativize(path); root.updateModificationTime(relPath, lastModified, initiator); } } void addDirectory(Path path, I initiator) { topLevelAncestorStream(path).forEach(root -> { - Path relPath = root.getValue().relativize(path); + Path relPath = root.getPath().relativize(path); root.addDirectory(relPath, initiator); }); } void addFile(Path path, I initiator, FileTime lastModified) { topLevelAncestorStream(path).forEach(root -> { - Path relPath = root.getValue().relativize(path); + Path relPath = root.getPath().relativize(path); root.addFile(relPath, lastModified, initiator); }); } void delete(Path path, I initiator) { - for(TopLevelDirItem root: getTopLevelAncestorsNonEmpty(path)) { - Path relPath = root.getValue().relativize(path); + for(TopLevelDirItem root: getTopLevelAncestorsNonEmpty(path)) { + Path relPath = root.getPath().relativize(path); root.remove(relPath, initiator); } } @@ -109,19 +114,19 @@ void sync(PathNode tree) { .forEach(root -> root.sync(tree, defaultInitiator)); } - private Stream> topLevelAncestorStream(Path path) { + private Stream> topLevelAncestorStream(Path path) { return root.getChildren().stream() - .filter(item -> path.startsWith(item.getValue())) - .map(item -> (TopLevelDirItem) item); + .filter(item -> path.startsWith(projector.apply(item.getValue()))) + .map(item -> (TopLevelDirItem) item); } - private List> getTopLevelAncestors(Path path) { + private List> getTopLevelAncestors(Path path) { return Arrays.asList(topLevelAncestorStream(path) - .>toArray(TopLevelDirItem[]::new)); + .>toArray(TopLevelDirItem[]::new)); } - private List> getTopLevelAncestorsNonEmpty(Path path) { - List> roots = getTopLevelAncestors(path); + private List> getTopLevelAncestorsNonEmpty(Path path) { + List> roots = getTopLevelAncestors(path); assert !roots.isEmpty() : "path resolved against a dir that was reported to be in the model does not have a top-level ancestor in the model"; return roots; } diff --git a/src/main/java/org/fxmisc/livedirs/PathItem.java b/livedirsfx/src/main/java/org/fxmisc/livedirs/PathItem.java similarity index 51% rename from src/main/java/org/fxmisc/livedirs/PathItem.java rename to livedirsfx/src/main/java/org/fxmisc/livedirs/PathItem.java index d698701..e19ccc2 100644 --- a/src/main/java/org/fxmisc/livedirs/PathItem.java +++ b/livedirsfx/src/main/java/org/fxmisc/livedirs/PathItem.java @@ -7,6 +7,7 @@ import java.util.HashSet; import java.util.NoSuchElementException; import java.util.Set; +import java.util.function.Function; import javafx.collections.ObservableList; import javafx.scene.Node; @@ -14,9 +15,15 @@ import org.fxmisc.livedirs.DirectoryModel.GraphicFactory; -abstract class PathItem extends TreeItem { - protected PathItem(Path path, Node graphic) { +abstract class PathItem extends TreeItem { + + private final Function projector; + protected final Function getProjector() { return projector; } + public final Path getPath() { return projector.apply(getValue()); } + + protected PathItem(T path, Node graphic, Function projector) { super(path, graphic); + this.projector = projector; } @Override @@ -26,26 +33,27 @@ public final boolean isLeaf() { public abstract boolean isDirectory(); - public FileItem asFileItem() { return (FileItem) this; } - public DirItem asDirItem() { return (DirItem) this; } + public FileItem asFileItem() { return (FileItem) this; } + public DirItem asDirItem() { return (DirItem) this; } - public PathItem getRelChild(Path relPath) { + public PathItem getRelChild(Path relPath) { assert relPath.getNameCount() == 1; - Path childValue = getValue().resolve(relPath); - for(TreeItem ch: getChildren()) { - if(ch.getValue().equals(childValue)) { - return (PathItem) ch; + Path childValue = getPath().resolve(relPath); + for(TreeItem ch: getChildren()) { + PathItem pathCh = (PathItem) ch; + if(pathCh.getPath().equals(childValue)) { + return pathCh; } } return null; } - protected PathItem resolve(Path relPath) { + protected PathItem resolve(Path relPath) { int len = relPath.getNameCount(); if(len == 0) { return this; } else { - PathItem child = getRelChild(relPath.getName(0)); + PathItem child = getRelChild(relPath.getName(0)); if(child == null) { return null; } else if(len == 1) { @@ -57,15 +65,15 @@ protected PathItem resolve(Path relPath) { } } -class FileItem extends PathItem { - public static FileItem create(Path path, FileTime lastModified, GraphicFactory graphicFactory) { - return new FileItem(path, lastModified, graphicFactory.createGraphic(path, false)); +class FileItem extends PathItem { + public static FileItem create(T path, FileTime lastModified, GraphicFactory graphicFactory, Function projector) { + return new FileItem<>(path, lastModified, graphicFactory.createGraphic(projector.apply(path), false), projector); } private FileTime lastModified; - private FileItem(Path path, FileTime lastModified, Node graphic) { - super(path, graphic); + private FileItem(T path, FileTime lastModified, Node graphic, Function projector) { + super(path, graphic, projector); this.lastModified = lastModified; } @@ -84,13 +92,19 @@ public boolean updateModificationTime(FileTime lastModified) { } } -class DirItem extends PathItem { - public static DirItem create(Path path, GraphicFactory graphicFactory) { - return new DirItem(path, graphicFactory.createGraphic(path, true)); +class DirItem extends PathItem { + + private final Function injector; + protected final Function getInjector() { return injector; } + public final T inject(Path path) { return injector.apply(path); } + + public static DirItem create(T path, GraphicFactory graphicFactory, Function projector, Function injector) { + return new DirItem<>(path, graphicFactory.createGraphic(projector.apply(path), true), projector, injector); } - protected DirItem(Path path, Node graphic) { - super(path, graphic); + protected DirItem(T path, Node graphic, Function projector, Function injector) { + super(path, graphic, projector); + this.injector = injector; } @Override @@ -98,29 +112,31 @@ public final boolean isDirectory() { return true; } - public FileItem addChildFile(Path fileName, FileTime lastModified, GraphicFactory graphicFactory) { + public FileItem addChildFile(Path fileName, FileTime lastModified, GraphicFactory graphicFactory) { assert fileName.getNameCount() == 1; int i = getFileInsertionIndex(fileName.toString()); - FileItem child = FileItem.create(getValue().resolve(fileName), lastModified, graphicFactory); + + FileItem child = FileItem.create(inject(getPath().resolve(fileName)), lastModified, graphicFactory, getProjector()); getChildren().add(i, child); return child; } - public DirItem addChildDir(Path dirName, GraphicFactory graphicFactory) { + public DirItem addChildDir(Path dirName, GraphicFactory graphicFactory) { assert dirName.getNameCount() == 1; int i = getDirInsertionIndex(dirName.toString()); - DirItem child = DirItem.create(getValue().resolve(dirName), graphicFactory); + + DirItem child = DirItem.create(inject(getPath().resolve(dirName)), graphicFactory, getProjector(), getInjector()); getChildren().add(i, child); return child; } private int getFileInsertionIndex(String fileName) { - ObservableList> children = getChildren(); + ObservableList> children = getChildren(); int n = children.size(); for(int i = 0; i < n; ++i) { - PathItem child = (PathItem) children.get(i); + PathItem child = (PathItem) children.get(i); if(!child.isDirectory()) { - String childName = child.getValue().getFileName().toString(); + String childName = child.getPath().getFileName().toString(); if(childName.compareToIgnoreCase(fileName) > 0) { return i; } @@ -130,12 +146,12 @@ private int getFileInsertionIndex(String fileName) { } private int getDirInsertionIndex(String dirName) { - ObservableList> children = getChildren(); + ObservableList> children = getChildren(); int n = children.size(); for(int i = 0; i < n; ++i) { - PathItem child = (PathItem) children.get(i); + PathItem child = (PathItem) children.get(i); if(child.isDirectory()) { - String childName = child.getValue().getFileName().toString(); + String childName = child.getPath().getFileName().toString(); if(childName.compareToIgnoreCase(dirName) > 0) { return i; } @@ -147,17 +163,17 @@ private int getDirInsertionIndex(String dirName) { } } -class ParentChild { - private final DirItem parent; - private final PathItem child; +class ParentChild { + private final DirItem parent; + private final PathItem child; - public ParentChild(DirItem parent, PathItem child) { + public ParentChild(DirItem parent, PathItem child) { this.parent = parent; this.child = child; } - public DirItem getParent() { return parent; } - public PathItem getChild() { return child; } + public DirItem getParent() { return parent; } + public PathItem getChild() { return child; } } interface Reporter { @@ -167,41 +183,41 @@ interface Reporter { void reportError(Throwable error); } -class TopLevelDirItem extends DirItem { +class TopLevelDirItem extends DirItem { private final GraphicFactory graphicFactory; private final Reporter reporter; - TopLevelDirItem(Path path, GraphicFactory graphicFactory, Reporter reporter) { - super(path, graphicFactory.createGraphic(path, true)); + TopLevelDirItem(T path, GraphicFactory graphicFactory, Function projector, Function injector, Reporter reporter) { + super(path, graphicFactory.createGraphic(projector.apply(path), true), projector, injector); this.graphicFactory = graphicFactory; this.reporter = reporter; } - private ParentChild resolveInParent(Path relPath) { + private ParentChild resolveInParent(Path relPath) { int len = relPath.getNameCount(); if(len == 0) { - return new ParentChild(null, this); + return new ParentChild<>(null, this); } else if(len == 1) { - if(getValue().resolve(relPath).equals(getValue())) { - return new ParentChild(null, this); + if(getPath().resolve(relPath).equals(getValue())) { + return new ParentChild<>(null, this); } else { - return new ParentChild(this, getRelChild(relPath.getName(0))); + return new ParentChild<>(this, getRelChild(relPath.getName(0))); } } else { - PathItem parent = resolve(relPath.subpath(0, len - 1)); + PathItem parent = resolve(relPath.subpath(0, len - 1)); if(parent == null || !parent.isDirectory()) { - return new ParentChild(null, null); + return new ParentChild<>(null, null); } else { - PathItem child = parent.getRelChild(relPath.getFileName()); - return new ParentChild(parent.asDirItem(), child); + PathItem child = parent.getRelChild(relPath.getFileName()); + return new ParentChild<>(parent.asDirItem(), child); } } } private void updateFile(Path relPath, FileTime lastModified, I initiator) { - PathItem item = resolve(relPath); + PathItem item = resolve(relPath); if(item == null || item.isDirectory()) { - sync(PathNode.file(getValue().resolve(relPath), lastModified), initiator); + sync(PathNode.file(getPath().resolve(relPath), lastModified), initiator); } } @@ -218,18 +234,18 @@ public void updateModificationTime(Path relPath, FileTime lastModified, I initia } public void addDirectory(Path relPath, I initiator) { - PathItem item = resolve(relPath); + PathItem item = resolve(relPath); if(item == null || !item.isDirectory()) { - sync(PathNode.directory(getValue().resolve(relPath), Collections.emptyList()), initiator); + sync(PathNode.directory(getPath().resolve(relPath), Collections.emptyList()), initiator); } } public void sync(PathNode tree, I initiator) { Path path = tree.getPath(); - Path relPath = getValue().relativize(path); - ParentChild pc = resolveInParent(relPath); - DirItem parent = pc.getParent(); - PathItem item = pc.getChild(); + Path relPath = getPath().relativize(path); + ParentChild pc = resolveInParent(relPath); + DirItem parent = pc.getParent(); + PathItem item = pc.getChild(); if(parent != null) { syncChild(parent, relPath.getFileName(), tree, initiator); } else if(item == null) { // neither path nor its parent present in model @@ -244,17 +260,17 @@ public void sync(PathNode tree, I initiator) { } } - private void syncContent(DirItem dir, PathNode tree, I initiator) { + private void syncContent(DirItem dir, PathNode tree, I initiator) { Set desiredChildren = new HashSet<>(); for(PathNode ch: tree.getChildren()) { desiredChildren.add(ch.getPath()); } - ArrayList> actualChildren = new ArrayList<>(dir.getChildren()); + ArrayList> actualChildren = new ArrayList<>(dir.getChildren()); // remove undesired children - for(TreeItem ch: actualChildren) { - if(!desiredChildren.contains(ch.getValue())) { + for(TreeItem ch: actualChildren) { + if(!desiredChildren.contains(getProjector().apply(ch.getValue()))) { removeNode(ch, null); } } @@ -265,48 +281,48 @@ private void syncContent(DirItem dir, PathNode tree, I initiator) { } } - private void syncChild(DirItem parent, Path childName, PathNode tree, I initiator) { - PathItem child = parent.getRelChild(childName); + private void syncChild(DirItem parent, Path childName, PathNode tree, I initiator) { + PathItem child = parent.getRelChild(childName); if(child != null && child.isDirectory() != tree.isDirectory()) { removeNode(child, null); } if(child == null) { if(tree.isDirectory()) { - DirItem dirChild = parent.addChildDir(childName, graphicFactory); - reporter.reportCreation(getValue(), getValue().relativize(dirChild.getValue()), initiator); + DirItem dirChild = parent.addChildDir(childName, graphicFactory); + reporter.reportCreation(getPath(), getPath().relativize(dirChild.getPath()), initiator); syncContent(dirChild, tree, initiator); } else { - FileItem fileChild = parent.addChildFile(childName, tree.getLastModified(), graphicFactory); - reporter.reportCreation(getValue(), getValue().relativize(fileChild.getValue()), initiator); + FileItem fileChild = parent.addChildFile(childName, tree.getLastModified(), graphicFactory); + reporter.reportCreation(getPath(), getPath().relativize(fileChild.getPath()), initiator); } } else { if(child.isDirectory()) { syncContent(child.asDirItem(), tree, initiator); } else { if(child.asFileItem().updateModificationTime(tree.getLastModified())) { - reporter.reportModification(getValue(), getValue().relativize(child.getValue()), initiator); + reporter.reportModification(getPath(), getPath().relativize(child.getPath()), initiator); } } } } public void remove(Path relPath, I initiator) { - PathItem item = resolve(relPath); + PathItem item = resolve(relPath); if(item != null) { removeNode(item, initiator); } } - private void removeNode(TreeItem node, I initiator) { + private void removeNode(TreeItem node, I initiator) { signalDeletionRecursively(node, initiator); node.getParent().getChildren().remove(node); } - private void signalDeletionRecursively(TreeItem node, I initiator) { - for(TreeItem child: node.getChildren()) { + private void signalDeletionRecursively(TreeItem node, I initiator) { + for(TreeItem child: node.getChildren()) { signalDeletionRecursively(child, initiator); } - reporter.reportDeletion(getValue(), getValue().relativize(node.getValue()), initiator); + reporter.reportDeletion(getPath(), getPath().relativize(getProjector().apply(node.getValue())), initiator); } private void raise(Throwable t) { diff --git a/src/main/resources/org/fxmisc/livedirs/file-16.png b/livedirsfx/src/main/resources/org/fxmisc/livedirs/file-16.png similarity index 100% rename from src/main/resources/org/fxmisc/livedirs/file-16.png rename to livedirsfx/src/main/resources/org/fxmisc/livedirs/file-16.png diff --git a/src/main/resources/org/fxmisc/livedirs/folder-16.png b/livedirsfx/src/main/resources/org/fxmisc/livedirs/folder-16.png similarity index 100% rename from src/main/resources/org/fxmisc/livedirs/folder-16.png rename to livedirsfx/src/main/resources/org/fxmisc/livedirs/folder-16.png diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..38820d2 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +include 'livedirsfx', 'livedirsfx-demo' +