mirror of
https://github.com/xpipe-io/xpipe.git
synced 2024-11-21 23:20:23 +00:00
Move fxcomps into this repository
This commit is contained in:
parent
74691c5a03
commit
ca5dd74086
22 changed files with 1076 additions and 0 deletions
21
fxcomps/LICENSE.md
Normal file
21
fxcomps/LICENSE.md
Normal 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
139
fxcomps/README.md
Normal 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
39
fxcomps/build.gradle
Normal 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
33
fxcomps/publish.gradle
Normal 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'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
104
fxcomps/src/main/java/io/xpipe/fxcomps/Comp.java
Normal file
104
fxcomps/src/main/java/io/xpipe/fxcomps/Comp.java
Normal 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();
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package io.xpipe.fxcomps;
|
||||
|
||||
import javafx.scene.layout.Region;
|
||||
|
||||
public interface CompStructure<R extends Region> {
|
||||
R get();
|
||||
}
|
13
fxcomps/src/main/java/io/xpipe/fxcomps/SimpleComp.java
Normal file
13
fxcomps/src/main/java/io/xpipe/fxcomps/SimpleComp.java
Normal 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();
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package io.xpipe.fxcomps.augment;
|
||||
|
||||
import io.xpipe.fxcomps.CompStructure;
|
||||
|
||||
public interface Augment<S extends CompStructure<?>> {
|
||||
|
||||
void augment(S struc);
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
31
fxcomps/src/main/java/io/xpipe/fxcomps/comp/LabelComp.java
Normal file
31
fxcomps/src/main/java/io/xpipe/fxcomps/comp/LabelComp.java
Normal 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);
|
||||
}
|
||||
}
|
29
fxcomps/src/main/java/io/xpipe/fxcomps/comp/StackComp.java
Normal file
29
fxcomps/src/main/java/io/xpipe/fxcomps/comp/StackComp.java
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
20
fxcomps/src/main/java/io/xpipe/fxcomps/comp/WrapperComp.java
Normal file
20
fxcomps/src/main/java/io/xpipe/fxcomps/comp/WrapperComp.java
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
274
fxcomps/src/main/java/io/xpipe/fxcomps/util/PlatformThread.java
Normal file
274
fxcomps/src/main/java/io/xpipe/fxcomps/util/PlatformThread.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
55
fxcomps/src/main/java/io/xpipe/fxcomps/util/Shortcuts.java
Normal file
55
fxcomps/src/main/java/io/xpipe/fxcomps/util/Shortcuts.java
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
12
fxcomps/src/main/java/module-info.java
Normal file
12
fxcomps/src/main/java/module-info.java
Normal 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;
|
||||
}
|
|
@ -4,6 +4,7 @@ include 'api'
|
|||
include 'core'
|
||||
include 'beacon'
|
||||
include 'extension'
|
||||
include 'fxcomps'
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
|
|
Loading…
Reference in a new issue