Move fxcomps into this repository

This commit is contained in:
Christopher Schnick 2022-11-30 19:23:44 +01:00
parent 74691c5a03
commit ca5dd74086
22 changed files with 1076 additions and 0 deletions

21
fxcomps/LICENSE.md Normal file
View file

@ -0,0 +1,21 @@
### MIT License
Copyright (c) 2021 Christopher Schnick
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

139
fxcomps/README.md Normal file
View file

@ -0,0 +1,139 @@
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.xpipe/fxcomps/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.xpipe/fxcomps)
[![javadoc](https://javadoc.io/badge2/io.xpipe/fxcomps/javadoc.svg)](https://javadoc.io/doc/io.xpipe/fxcomps)
[![Build Status](https://github.com/xpipe-io/fxcomps/actions/workflows/publish.yml/badge.svg)](https://github.com/xpipe-io/fxcomps/actions/workflows/publish.yml)
# FxComps - Compound Components for JavaFX
The FxComps library provides a new approach to creating JavaFX interfaces and
offers a quicker and more robust user interface development workflow.
This library is compatible and can be used with any other JavaFX library.
## Principles
#### A comp is a Node/Region factory, not just another fancy wrapper for existing classes
It is advantageous to define a certain component to be a factory
that can create an instances of a JavaFX Node each time it is called.
By using this factory architecture, the scene contents can
be rebuilt entirely by invoking the root component factory.
See the [hot reload](#Hot-Reload) section on how this can be used.
Of course, if a component is a compound component that has children,
the parent factory has to incorporate the child factories into its creation process.
This can be done in fxcomps.
#### A comp should produce a transparent representation of Regions and Controls
In JavaFX, using skins allows for flexibility when generating the look and feel for a control.
One limitation of this approach is that the generated node tree is not very transparent
for developers who are especially interested in styling it.
This is caused by the fact that a skin does not expose the information required to style
it completely or even alter it without creating a new Skin class.
A comp should be designed to allow developers to easily expose as much information
about the produced node tree structure using the CompStructure class.
In case you don't want to expose the detailed structure of your comp,
you can also just use a very simple structure.
#### A comp should produce a Region instead of a Node
In practice, working with the very abstract node class comes with its fair share of limitations.
It is much easier to work with region instances, as they have various width and height properties.
Since pretty much every Node is also a Region, the main focus of comps are regions.
In case you are dealing with Nodes that are not Regions, like an ImageView or WebView,
you can still wrap them inside for example a StackPane to obtain a Region again that you can work with.
#### The generation process of a comp can be augmented
As comps are factories, any changes that should be applied to all produced
Node instances must be integrated into the factory pipeline.
This can be achieved with the Augment class, which allows you
to alter the produced node after the base factory has finished.
#### Properties used by Comps should be managed by the user, not the Comp itself
This allows Comps to only be a thin wrapper around already existing
Observables/Properties and gives the user the ability to complete control the handling of Properties.
This approach is also required for the next point.
#### A comp should not break when used Observables are updated from a thread that is not the platform thread
One common limitation of using JavaFX is that many things break when
calling any method from another thread that is not the platform thread.
While in many cases these issues can be mitigated by wrapping a problematic call in a Platform.runLater(...),
some problematic instances are harder to fix, for example Observable bindings.
In JavaFX, there is currently no way to propagate changes of an Observable
to other bound Observables only using the platform thread, when the original change was made from a different thread.
The FxComps library provides a solution with the PlatformThread.sync(...) methods and strongly encourages that
Comps make use of these methods in combination with user-managed properties
to allow for value changes for Observables from any thread without issue.
## Hot reload
The reason a Comp is designed to be a factory is to allow for hot
reloading your created GUI in conjunction with the hot-reload functionality in your IDE:
````java
void setupReload(Scene scene, Comp<?> content) {
var contentR = content.createRegion();
scene.addEventHandler(KeyEvent.KEY_PRESSED, event -> {
if (event.getCode().equals(KeyCode.F5)) {
var newContentR = content.createRegion();
scene.setRoot(newContentR);
event.consume();
}
});
}
````
If you for example bind your IDE Hot Reload to F4 and your Scene reload listener to F5,
you can almost instantly apply any changes made to your GUI code without restarting.
You can also implement a similar solution to also reload your stylesheets and translations.
## Library contents
Aside from the base classes needed to implement the principles listed above,
this library also comes with a few very basic Comp implementations and some Augments.
These are very general implementations and can be seen as example implementations.
#### Comps
- [HorizontalComp](src/main/java/io/xpipe/fxcomps/comp/HorizontalComp.java) /
[VerticalComp](src/main/java/io/xpipe/fxcomps/comp/VerticalComp.java): Simple Comp implementation to create a
HBox/VBox using Comps as input
- [StackComp](src/main/java/io/xpipe/fxcomps/comp/StackComp.java): Simple Comp implementation to easily create a stack
pane using Comps as input
- [StackComp](src/main/java/io/xpipe/fxcomps/comp/LabelComp.java): Simple Comp implementation for a label
#### Augments
- [GrowAugment](src/main/java/io/xpipe/fxcomps/augment/GrowAugment.java): Binds the width/height of a Comp to its
parent, adjusted for parent padding
- [PopupMenuComp](src/main/java/io/xpipe/fxcomps/augment/PopupMenuAugment.java): Allows you to show a context menu when
a comp is left-clicked in addition to right-click
## Creating a basic comp
As the central idea of this library is that you create your own Comps, it is designed to be very simple:
````java
var b = Comp.of(() -> new Button("Button"));
var l = Comp.of(() -> new Label("Label"));
// Create an HBox factory and apply some Augments to it
var layoutFactory = new HorizontalComp(List.of(b, l))
.apply(struc -> struc.get().setAlignment(Pos.CENTER))
.apply(GrowAugment.create(true, true))
.styleClass("layout");
// You can now create node instances of your layout
var region = layoutFactory.createRegion();
````
Most simple Comp definitions can be defined inline with the `Comp.of(...)` method.
## Creating more complex comps
For actual comp implementations, see for example
the [X-Pipe Extension API](https://github.com/xpipe-io/xpipe_java/tree/master/extension/src/main/java/io/xpipe/extension/comp)
.

39
fxcomps/build.gradle Normal file
View file

@ -0,0 +1,39 @@
plugins {
id 'java-library'
id 'maven-publish'
id 'signing'
}
version = file('../misc/version').text
group = 'io.xpipe'
archivesBaseName = 'xpipe-fxcomps'
repositories {
mavenCentral()
}
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
def currentOS = DefaultNativePlatform.currentOperatingSystem;
def platform
if (currentOS.isWindows()) {
platform = 'win'
} else if (currentOS.isLinux()) {
platform = 'linux'
} else if (currentOS.isMacOsX()) {
platform = 'mac'
}
dependencies {
compileOnly "org.openjfx:javafx-base:18:${platform}"
compileOnly "org.openjfx:javafx-controls:18:${platform}"
compileOnly "org.openjfx:javafx-graphics:18:${platform}"
compileOnly "org.openjfx:javafx-media:18:${platform}"
compileOnly "org.openjfx:javafx-web:18:${platform}"
compileOnly 'org.projectlombok:lombok:1.18.24'
annotationProcessor 'org.projectlombok:lombok:1.18.24'
}
apply from: 'publish.gradle'
apply from: "$projectDir/../deps/publish-base.gradle"

33
fxcomps/publish.gradle Normal file
View file

@ -0,0 +1,33 @@
publishing {
publications {
mavenJava(MavenPublication) {
artifactId = project.archivesBaseName
from components.java
pom {
name = 'FxComps'
description = 'The FxComps library provides a new approach to creating JavaFX interfaces and offers a quicker and more robust user interface development workflow.'
url = 'https://github.com/xpipe-io/xpipe_java/fxcomps'
licenses {
license {
name = 'The MIT License (MIT)'
url = 'https://github.com/xpipe-io/xpipe_java/LICENSE.md'
}
}
developers {
developer {
id = 'crschnick'
name = 'Christopher Schnick'
email = 'crschnick@xpipe.io'
}
}
scm {
connection = 'scm:git:git://github.com/xpipe-io/xpipe_java.git'
developerConnection = 'scm:git:ssh://github.com/xpipe-io/xpipe_java.git'
url = 'https://github.com/xpipe-io/xpipe_java'
}
}
}
}
}

View file

@ -0,0 +1,104 @@
package io.xpipe.fxcomps;
import io.xpipe.fxcomps.augment.Augment;
import io.xpipe.fxcomps.augment.GrowAugment;
import io.xpipe.fxcomps.comp.WrapperComp;
import io.xpipe.fxcomps.util.Shortcuts;
import io.xpipe.fxcomps.util.SimpleChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.ButtonBase;
import javafx.scene.control.Tooltip;
import javafx.scene.input.KeyCombination;
import javafx.scene.layout.Region;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
public abstract class Comp<S extends CompStructure<?>> {
private List<Augment<S>> augments;
public static <R extends Region> Comp<CompStructure<R>> of(Supplier<R> r) {
return new WrapperComp<>(() -> {
var region = r.get();
return () -> region;
});
}
public static <S extends CompStructure<?>> Comp<S> ofStructure(Supplier<S> r) {
return new WrapperComp<>(r);
}
@SuppressWarnings("unchecked")
public static <IR extends Region, SIN extends CompStructure<IR>, OR extends Region> Comp<CompStructure<OR>> derive(
Comp<SIN> comp, Function<IR, OR> r) {
return of(() -> r.apply((IR) comp.createRegion()));
}
@SuppressWarnings("unchecked")
public <T extends Comp<S>> T apply(Augment<S> augment) {
if (augments == null) {
augments = new ArrayList<>();
}
augments.add(augment);
return (T) this;
}
public Comp<S> disable(ObservableValue<Boolean> o) {
return apply(struc -> struc.get().disableProperty().bind(o));
}
public Comp<S> hide(ObservableValue<Boolean> o) {
return apply(struc -> {
var region = struc.get();
SimpleChangeListener.apply(o, n -> {
if (!n) {
region.setVisible(true);
region.setManaged(true);
} else {
region.setVisible(false);
region.setManaged(false);
}
});
});
}
public Comp<S> styleClass(String styleClass) {
return apply(struc -> struc.get().getStyleClass().add(styleClass));
}
public Comp<S> grow(boolean width, boolean height) {
return apply(GrowAugment.create(false, false));
}
public Comp<S> shortcut(KeyCombination shortcut, Consumer<S> con) {
return apply(struc -> Shortcuts.addShortcut(struc.get(), shortcut, r -> con.accept(struc)));
}
public Comp<S> shortcut(KeyCombination shortcut) {
return apply(struc -> Shortcuts.addShortcut((ButtonBase) struc.get(), shortcut));
}
public Comp<S> tooltip(Supplier<String> text) {
return apply(r -> Tooltip.install(r.get(), new Tooltip(text.get())));
}
public Region createRegion() {
return createStructure().get();
}
public S createStructure() {
S struc = createBase();
if (augments != null) {
for (var a : augments) {
a.augment(struc);
}
}
return struc;
}
public abstract S createBase();
}

View file

@ -0,0 +1,7 @@
package io.xpipe.fxcomps;
import javafx.scene.layout.Region;
public interface CompStructure<R extends Region> {
R get();
}

View file

@ -0,0 +1,13 @@
package io.xpipe.fxcomps;
import javafx.scene.layout.Region;
public abstract class SimpleComp extends Comp<CompStructure<Region>> {
@Override
public final CompStructure<Region> createBase() {
return new SimpleCompStructure<>(createSimple());
}
protected abstract Region createSimple();
}

View file

@ -0,0 +1,17 @@
package io.xpipe.fxcomps;
import javafx.scene.layout.Region;
import lombok.AllArgsConstructor;
import lombok.Value;
@Value
@AllArgsConstructor
public class SimpleCompStructure<R extends Region> implements CompStructure<R> {
R value;
@Override
public R get() {
return value;
}
}

View file

@ -0,0 +1,8 @@
package io.xpipe.fxcomps.augment;
import io.xpipe.fxcomps.CompStructure;
public interface Augment<S extends CompStructure<?>> {
void augment(S struc);
}

View file

@ -0,0 +1,65 @@
package io.xpipe.fxcomps.augment;
import io.xpipe.fxcomps.CompStructure;
import javafx.beans.binding.Bindings;
import javafx.scene.Node;
import javafx.scene.layout.Region;
public class GrowAugment<S extends CompStructure<?>> implements Augment<S> {
private final boolean width;
private final boolean height;
private GrowAugment(boolean width, boolean height) {
this.width = width;
this.height = height;
}
public static <S extends CompStructure<?>> GrowAugment<S> create(boolean width, boolean height) {
return new GrowAugment<>(width, height);
}
private void bind(Region r, Node parent) {
if (!(parent instanceof Region p)) {
return;
}
if (width) {
r.prefWidthProperty()
.bind(Bindings.createDoubleBinding(
() -> p.getWidth()
- p.getInsets().getLeft()
- p.getInsets().getRight(),
p.widthProperty(),
p.insetsProperty()));
}
if (height) {
r.prefHeightProperty()
.bind(Bindings.createDoubleBinding(
() -> {
var val = p.getHeight()
- p.getInsets().getTop()
- p.getInsets().getBottom();
if (val <= 0) {
return Region.USE_COMPUTED_SIZE;
}
return val;
},
p.heightProperty(),
p.insetsProperty()));
}
}
@Override
public void augment(S struc) {
struc.get().parentProperty().addListener((c, o, n) -> {
if (o instanceof Region) {
if (width) struc.get().prefWidthProperty().unbind();
if (height) struc.get().prefHeightProperty().unbind();
}
bind(struc.get(), n);
});
bind(struc.get(), struc.get().getParent());
}
}

View file

@ -0,0 +1,31 @@
package io.xpipe.fxcomps.augment;
import io.xpipe.fxcomps.CompStructure;
import javafx.scene.control.ContextMenu;
import javafx.scene.input.MouseButton;
public abstract class PopupMenuAugment<S extends CompStructure<?>> implements Augment<S> {
private final boolean showOnPrimaryButton;
protected PopupMenuAugment(boolean showOnPrimaryButton) {
this.showOnPrimaryButton = showOnPrimaryButton;
}
protected abstract ContextMenu createContextMenu();
@Override
public void augment(S struc) {
var cm = createContextMenu();
var r = struc.get();
r.setOnMousePressed(event -> {
if ((showOnPrimaryButton && event.getButton() == MouseButton.PRIMARY)
|| (!showOnPrimaryButton && event.getButton() == MouseButton.SECONDARY)) {
cm.show(r, event.getScreenX(), event.getScreenY());
event.consume();
} else {
cm.hide();
}
});
}
}

View file

@ -0,0 +1,29 @@
package io.xpipe.fxcomps.comp;
import io.xpipe.fxcomps.Comp;
import io.xpipe.fxcomps.CompStructure;
import io.xpipe.fxcomps.SimpleCompStructure;
import javafx.geometry.Pos;
import javafx.scene.layout.HBox;
import java.util.List;
public class HorizontalComp extends Comp<CompStructure<HBox>> {
private final List<Comp<?>> entries;
public HorizontalComp(List<Comp<?>> comps) {
entries = List.copyOf(comps);
}
@Override
public CompStructure<HBox> createBase() {
HBox b = new HBox();
b.getStyleClass().add("horizontal-comp");
for (var entry : entries) {
b.getChildren().add(entry.createRegion());
}
b.setAlignment(Pos.CENTER);
return new SimpleCompStructure<>(b);
}
}

View file

@ -0,0 +1,31 @@
package io.xpipe.fxcomps.comp;
import io.xpipe.fxcomps.Comp;
import io.xpipe.fxcomps.CompStructure;
import io.xpipe.fxcomps.SimpleCompStructure;
import io.xpipe.fxcomps.util.PlatformThread;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
public class LabelComp extends Comp<CompStructure<Label>> {
private final ObservableValue<String> text;
public LabelComp(String text) {
this.text = new SimpleStringProperty(text);
}
public LabelComp(ObservableValue<String> text) {
this.text = PlatformThread.sync(text);
}
@Override
public CompStructure<Label> createBase() {
var label = new Label();
label.textProperty().bind(text);
label.setAlignment(Pos.CENTER);
return new SimpleCompStructure<>(label);
}
}

View file

@ -0,0 +1,29 @@
package io.xpipe.fxcomps.comp;
import io.xpipe.fxcomps.Comp;
import io.xpipe.fxcomps.CompStructure;
import io.xpipe.fxcomps.SimpleCompStructure;
import javafx.geometry.Pos;
import javafx.scene.layout.StackPane;
import java.util.List;
public class StackComp extends Comp<CompStructure<StackPane>> {
private final List<Comp<?>> comps;
public StackComp(List<Comp<?>> comps) {
this.comps = List.copyOf(comps);
}
@Override
public CompStructure<StackPane> createBase() {
var pane = new StackPane();
for (var c : comps) {
pane.getChildren().add(c.createRegion());
}
pane.setAlignment(Pos.CENTER);
pane.setPickOnBounds(false);
return new SimpleCompStructure<>(pane);
}
}

View file

@ -0,0 +1,38 @@
package io.xpipe.fxcomps.comp;
import io.xpipe.fxcomps.Comp;
import io.xpipe.fxcomps.CompStructure;
import io.xpipe.fxcomps.SimpleCompStructure;
import io.xpipe.fxcomps.util.PlatformThread;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.layout.VBox;
import java.util.List;
public class VerticalComp extends Comp<CompStructure<VBox>> {
private final ObservableList<Comp<?>> entries;
public VerticalComp(List<Comp<?>> comps) {
entries = FXCollections.observableArrayList(List.copyOf(comps));
}
public VerticalComp(ObservableList<Comp<?>> entries) {
this.entries = PlatformThread.sync(entries);
}
@Override
public CompStructure<VBox> createBase() {
VBox b = new VBox();
b.getStyleClass().add("vertical-comp");
entries.addListener((ListChangeListener<? super Comp<?>>) c -> {
b.getChildren().setAll(c.getList().stream().map(Comp::createRegion).toList());
});
for (var entry : entries) {
b.getChildren().add(entry.createRegion());
}
return new SimpleCompStructure<>(b);
}
}

View file

@ -0,0 +1,20 @@
package io.xpipe.fxcomps.comp;
import io.xpipe.fxcomps.Comp;
import io.xpipe.fxcomps.CompStructure;
import java.util.function.Supplier;
public class WrapperComp<S extends CompStructure<?>> extends Comp<S> {
private final Supplier<S> structureSupplier;
public WrapperComp(Supplier<S> structureSupplier) {
this.structureSupplier = structureSupplier;
}
@Override
public S createBase() {
return structureSupplier.get();
}
}

View file

@ -0,0 +1,91 @@
package io.xpipe.fxcomps.util;
import javafx.beans.binding.Binding;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import java.lang.ref.WeakReference;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class BindingsHelper {
/*
TODO: Proper cleanup. Maybe with a separate thread?
*/
private static final Map<WeakReference<Object>, Set<ObservableValue<?>>> BINDINGS = new ConcurrentHashMap<>();
public static <T extends Binding<?>> T persist(T binding) {
var dependencies = new HashSet<ObservableValue<?>>();
while (dependencies.addAll(binding.getDependencies().stream()
.map(o -> (ObservableValue<?>) o)
.toList())) {}
dependencies.add(binding);
BINDINGS.put(new WeakReference<>(binding), dependencies);
return binding;
}
public static <T> void bindContent(ObservableList<T> l1, ObservableList<? extends T> l2) {
setContent(l1, l2);
l2.addListener((ListChangeListener<? super T>) c -> {
setContent(l1, l2);
});
}
public static <T> void setContent(ObservableList<T> toSet, List<? extends T> newList) {
if (toSet.equals(newList)) {
return;
}
if (toSet.size() == 0) {
toSet.setAll(newList);
return;
}
if (newList.containsAll(toSet)) {
var l = new ArrayList<>(newList);
l.removeIf(t -> !toSet.contains(t));
if (!l.equals(toSet)) {
toSet.setAll(newList);
return;
}
var start = 0;
for (int end = 0; end <= toSet.size(); end++) {
var index = end < toSet.size() ? newList.indexOf(toSet.get(end)) : newList.size();
for (; start < index; start++) {
toSet.add(start, newList.get(start));
}
start = index + 1;
}
return;
}
if (toSet.contains(newList)) {
var l = new ArrayList<>(newList);
l.removeAll(toSet);
newList.removeAll(l);
return;
}
toSet.removeIf(e -> !newList.contains(e));
if (toSet.size() + 1 == newList.size() && newList.containsAll(toSet)) {
var l = new ArrayList<>(newList);
l.removeAll(toSet);
var index = newList.indexOf(l.get(0));
toSet.add(index, l.get(0));
return;
}
if (toSet.size() - 1 == newList.size() && toSet.containsAll(newList)) {
var l = new ArrayList<>(toSet);
l.removeAll(newList);
toSet.remove(l.get(0));
return;
}
toSet.setAll(newList);
}
}

View file

@ -0,0 +1,274 @@
package io.xpipe.fxcomps.util;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@SuppressWarnings("unchecked")
public class PlatformThread {
public static Observable sync(Observable o) {
return new Observable() {
private final Map<InvalidationListener, InvalidationListener> invListenerMap = new ConcurrentHashMap<>();
@Override
public void addListener(InvalidationListener listener) {
InvalidationListener l = o -> {
PlatformThread.runLaterIfNeeded(() -> listener.invalidated(o));
};
invListenerMap.put(listener, l);
o.addListener(l);
}
@Override
public void removeListener(InvalidationListener listener) {
o.removeListener(invListenerMap.getOrDefault(listener, listener));
}
};
}
public static <T> ObservableValue<T> sync(ObservableValue<T> ov) {
return new ObservableValue<>() {
private final Map<ChangeListener<? super T>, ChangeListener<? super T>> changeListenerMap =
new ConcurrentHashMap<>();
private final Map<InvalidationListener, InvalidationListener> invListenerMap = new ConcurrentHashMap<>();
@Override
public void addListener(ChangeListener<? super T> listener) {
ChangeListener<? super T> l = (c, o, n) -> {
PlatformThread.runLaterIfNeeded(() -> listener.changed(c, o, n));
};
changeListenerMap.put(listener, l);
ov.addListener(l);
}
@Override
public void removeListener(ChangeListener<? super T> listener) {
ov.removeListener(changeListenerMap.getOrDefault(listener, listener));
}
@Override
public T getValue() {
return ov.getValue();
}
@Override
public void addListener(InvalidationListener listener) {
InvalidationListener l = o -> {
PlatformThread.runLaterIfNeeded(() -> listener.invalidated(o));
};
invListenerMap.put(listener, l);
ov.addListener(l);
}
@Override
public void removeListener(InvalidationListener listener) {
ov.removeListener(invListenerMap.getOrDefault(listener, listener));
}
};
}
public static <T> ObservableList<T> sync(ObservableList<T> ol) {
return new ObservableList<>() {
private final Map<ListChangeListener<? super T>, ListChangeListener<? super T>> listChangeListenerMap =
new ConcurrentHashMap<>();
private final Map<InvalidationListener, InvalidationListener> invListenerMap = new ConcurrentHashMap<>();
@Override
public void addListener(ListChangeListener<? super T> listener) {
ListChangeListener<? super T> l = (lc) -> {
PlatformThread.runLaterIfNeeded(() -> listener.onChanged(lc));
};
listChangeListenerMap.put(listener, l);
ol.addListener(l);
}
@Override
public void removeListener(ListChangeListener<? super T> listener) {
ol.removeListener(listChangeListenerMap.getOrDefault(listener, listener));
}
@Override
public boolean addAll(T... elements) {
return ol.addAll(elements);
}
@Override
public boolean setAll(T... elements) {
return ol.setAll(elements);
}
@Override
public boolean setAll(Collection<? extends T> col) {
return ol.setAll(col);
}
@Override
public boolean removeAll(T... elements) {
return ol.removeAll(elements);
}
@Override
public boolean retainAll(T... elements) {
return ol.retainAll(elements);
}
@Override
public void remove(int from, int to) {
ol.remove(from, to);
}
@Override
public int size() {
return ol.size();
}
@Override
public boolean isEmpty() {
return ol.isEmpty();
}
@Override
public boolean contains(Object o) {
return ol.contains(o);
}
@Override
public Iterator<T> iterator() {
return ol.iterator();
}
@Override
public Object[] toArray() {
return ol.toArray();
}
@Override
public <T1> T1[] toArray(T1[] a) {
return ol.toArray(a);
}
@Override
public boolean add(T t) {
return ol.add(t);
}
@Override
public boolean remove(Object o) {
return ol.remove(o);
}
@Override
public boolean containsAll(Collection<?> c) {
return ol.containsAll(c);
}
@Override
public boolean addAll(Collection<? extends T> c) {
return ol.addAll(c);
}
@Override
public boolean addAll(int index, Collection<? extends T> c) {
return ol.addAll(index, c);
}
@Override
public boolean removeAll(Collection<?> c) {
return ol.removeAll(c);
}
@Override
public boolean retainAll(Collection<?> c) {
return ol.retainAll(c);
}
@Override
public void clear() {
ol.clear();
}
@Override
public T get(int index) {
return ol.get(index);
}
@Override
public T set(int index, T element) {
return ol.set(index, element);
}
@Override
public void add(int index, T element) {
ol.add(index, element);
}
@Override
public T remove(int index) {
return ol.remove(index);
}
@Override
public int indexOf(Object o) {
return ol.indexOf(o);
}
@Override
public int lastIndexOf(Object o) {
return ol.lastIndexOf(o);
}
@Override
public ListIterator<T> listIterator() {
return ol.listIterator();
}
@Override
public ListIterator<T> listIterator(int index) {
return ol.listIterator(index);
}
@Override
public List<T> subList(int fromIndex, int toIndex) {
return ol.subList(fromIndex, toIndex);
}
@Override
public void addListener(InvalidationListener listener) {
InvalidationListener l = o -> {
PlatformThread.runLaterIfNeeded(() -> listener.invalidated(o));
};
invListenerMap.put(listener, l);
ol.addListener(l);
}
@Override
public void removeListener(InvalidationListener listener) {
ol.removeListener(invListenerMap.getOrDefault(listener, listener));
}
};
}
public static void runLaterIfNeeded(Runnable r) {
if (Platform.isFxApplicationThread()) {
r.run();
} else {
Platform.runLater(r);
}
}
}

View file

@ -0,0 +1,55 @@
package io.xpipe.fxcomps.util;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.ButtonBase;
import javafx.scene.input.KeyCombination;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.Region;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
public class Shortcuts {
private static final Map<Region, KeyCombination> SHORTCUTS = new HashMap<>();
public static <T extends ButtonBase> void addShortcut(T region, KeyCombination comb) {
addShortcut(region, comb, ButtonBase::fire);
}
public static <T extends Region> void addShortcut(T region, KeyCombination comb, Consumer<T> exec) {
AtomicReference<Scene> scene = new AtomicReference<>(region.getScene());
var filter = new EventHandler<KeyEvent>() {
public void handle(KeyEvent ke) {
if (comb.match(ke)) {
exec.accept(region);
ke.consume();
}
}
};
SHORTCUTS.put(region, comb);
SimpleChangeListener.apply(region.sceneProperty(), s -> {
if (s != null) {
scene.set(s);
s.addEventHandler(KeyEvent.KEY_PRESSED, filter);
SHORTCUTS.put(region, comb);
} else {
if (scene.get() == null) {
return;
}
scene.get().removeEventHandler(KeyEvent.KEY_PRESSED, filter);
SHORTCUTS.remove(region);
scene.set(null);
}
});
}
public static KeyCombination getShortcut(Region region) {
return SHORTCUTS.get(region);
}
}

View file

@ -0,0 +1,19 @@
package io.xpipe.fxcomps.util;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
@FunctionalInterface
public interface SimpleChangeListener<T> {
static <T> void apply(ObservableValue<T> obs, SimpleChangeListener<T> cl) {
obs.addListener(cl.wrapped());
cl.onChange(obs.getValue());
}
void onChange(T val);
default ChangeListener<T> wrapped() {
return (observable, oldValue, newValue) -> this.onChange(newValue);
}
}

View file

@ -0,0 +1,12 @@
open module io.xpipe.fxcomps {
exports io.xpipe.fxcomps;
exports io.xpipe.fxcomps.comp;
exports io.xpipe.fxcomps.augment;
exports io.xpipe.fxcomps.util;
requires static javafx.base;
requires static javafx.controls;
requires static java.desktop;
requires static javafx.web;
requires static lombok;
}

View file

@ -4,6 +4,7 @@ include 'api'
include 'core'
include 'beacon'
include 'extension'
include 'fxcomps'
buildscript {
repositories {