This commit is contained in:
crschnick
2025-09-04 20:02:53 +00:00
parent 70327c7e38
commit beaa0d01e3
34 changed files with 245 additions and 380 deletions

View File

@@ -11,7 +11,7 @@ public class DaemonFocusExchangeImpl extends DaemonFocusExchange {
@Override
public Object handle(HttpExchange exchange, Request msg) {
OperationMode.switchUp(OperationMode.GUI);
var w = AppMainWindow.getInstance();
var w = AppMainWindow.get();
if (w != null) {
w.focus();
}

View File

@@ -75,7 +75,7 @@ public class BrowserFullSessionComp extends SimpleComp {
loadingStack.apply(struc -> struc.get().setPickOnBounds(false));
var delayedStack = new DelayedInitComp(
left, () -> StoreViewState.get() != null && StoreViewState.get().isInitialized());
delayedStack.hide(AppMainWindow.getInstance().getStage().widthProperty().lessThan(1000));
delayedStack.hide(AppMainWindow.get().getStage().widthProperty().lessThan(1000));
var splitPane = new LeftSplitPaneComp(delayedStack, loadingStack)
.withInitialWidth(AppLayoutModel.get().getSavedState().getBrowserConnectionsWidth())
.withOnDividerChange(d -> {

View File

@@ -3,7 +3,6 @@ package io.xpipe.app.browser.file;
import io.xpipe.app.browser.icon.BrowserIcons;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.augment.GrowAugment;
import io.xpipe.app.comp.base.HorizontalComp;
import io.xpipe.app.comp.base.ListBoxViewComp;
import io.xpipe.app.ext.FileEntry;
@@ -42,7 +41,7 @@ public class BrowserFileOverviewComp extends SimpleComp {
event.consume();
});
l.setAlignment(Pos.CENTER_LEFT);
GrowAugment.create(true, false).augment(l);
l.setMaxWidth(10000);
return l;
});
};

View File

@@ -5,7 +5,7 @@ import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.base.ListBoxViewComp;
import io.xpipe.app.comp.base.PrettyImageHelper;
import io.xpipe.app.core.AppStyle;
import io.xpipe.app.core.window.AppWindowHelper;
import io.xpipe.app.core.window.AppWindowStyle;
import io.xpipe.app.platform.BindingsHelper;
import io.xpipe.app.platform.PlatformThread;
@@ -42,7 +42,7 @@ public class BrowserFileSelectionListComp extends SimpleComp {
public static Image snapshot(ObservableList<BrowserEntry> list) {
var r = new BrowserFileSelectionListComp(list).styleClass("drag").createRegion();
var scene = new Scene(r);
AppWindowHelper.setupStylesheets(scene);
AppWindowStyle.addStylesheets(scene);
AppStyle.addStylesheets(scene);
SnapshotParameters parameters = new SnapshotParameters();
parameters.setFill(Color.TRANSPARENT);

View File

@@ -301,7 +301,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
return false;
}
if (AppMainWindow.getInstance().getStage().getWidth() <= 1380) {
if (AppMainWindow.get().getStage().getWidth() <= 1380) {
return false;
}

View File

@@ -1,17 +1,18 @@
package io.xpipe.app.comp;
import io.xpipe.app.comp.augment.Augment;
import io.xpipe.app.comp.augment.GrowAugment;
import io.xpipe.app.comp.base.TooltipHelper;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.platform.BindingsHelper;
import io.xpipe.app.platform.PlatformThread;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.scene.Node;
import javafx.scene.control.Separator;
import javafx.scene.control.Tooltip;
import javafx.scene.input.KeyCombination;
@@ -211,7 +212,63 @@ public abstract class Comp<S extends CompStructure<?>> {
}
public Comp<S> grow(boolean width, boolean height) {
return apply(GrowAugment.create(width, height));
return apply(struc -> {
struc.get().parentProperty().addListener((c, o, n) -> {
if (o instanceof Region) {
if (width) {
struc.get().prefWidthProperty().unbind();
}
if (height) {
struc.get().prefHeightProperty().unbind();
}
}
bindGrow(struc.get(), n, width, height);
});
bindGrow(struc.get(), struc.get().getParent(), width, height);
});
}
private void bindGrow(Region r, Node parent, boolean width, boolean height) {
if (!(parent instanceof Region p)) {
return;
}
if (width) {
r.prefWidthProperty()
.bind(Bindings.createDoubleBinding(
() -> {
var val = p.getWidth()
- p.getInsets().getLeft()
- p.getInsets().getRight();
if (val <= 0) {
return Region.USE_COMPUTED_SIZE;
}
// Floor to prevent rounding issues which cause an infinite growing
return Math.floor(val);
},
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;
}
// Floor to prevent rounding issues which cause an infinite growing
return Math.floor(val);
},
p.heightProperty(),
p.insetsProperty()));
}
}
public Comp<S> tooltip(ObservableValue<String> text) {

View File

@@ -1,8 +1,6 @@
# FxComps - Compound Components for JavaFX
# Compound Components
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.
As a basis, JavaFX nodes are created and manage via comps (compound components).
## Principles
@@ -12,11 +10,6 @@ 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
@@ -86,44 +79,3 @@ If you for example bind your IDE Hot Reload to F4 and your Scene reload listener
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.

View File

@@ -1,81 +0,0 @@
package io.xpipe.app.comp.augment;
import io.xpipe.app.comp.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(
() -> {
var val = p.getWidth()
- p.getInsets().getLeft()
- p.getInsets().getRight();
if (val <= 0) {
return Region.USE_COMPUTED_SIZE;
}
// Floor to prevent rounding issues which cause an infinite growing
return Math.floor(val);
},
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;
}
// Floor to prevent rounding issues which cause an infinite growing
return Math.floor(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

@@ -126,7 +126,7 @@ public class AppMainWindowContentComp extends SimpleComp {
overlay.addListener((ListChangeListener<? super ModalOverlay>) c -> {
if (c.next() && c.wasAdded()) {
AppMainWindow.getInstance().focus();
AppMainWindow.get().focus();
// Close blocking modal windows
var childWindows = Window.getWindows().stream()

View File

@@ -3,8 +3,8 @@ package io.xpipe.app.comp.base;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.SimpleCompStructure;
import io.xpipe.app.core.AppActionLinkDetector;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppOpenArguments;
import io.xpipe.app.platform.PlatformThread;
import javafx.beans.binding.Bindings;
@@ -18,6 +18,7 @@ import javafx.scene.input.MouseButton;
import atlantafx.base.controls.CustomTextField;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
import java.util.Objects;
public class FilterComp extends Comp<CompStructure<CustomTextField>> {
@@ -76,7 +77,7 @@ public class FilterComp extends Comp<CompStructure<CustomTextField>> {
filter.textProperty().addListener((observable, oldValue, n) -> {
// Handle pasted xpipe URLs
if (n != null && n.startsWith("xpipe://")) {
AppActionLinkDetector.handle(n, false);
AppOpenArguments.handle(List.of(n));
filter.setText(null);
return;
}

View File

@@ -37,8 +37,8 @@ public class PrettyImageHelper {
private static ObservableValue<String> rasterizedImageIfExistsScaled(
String img, int height, int... availableSizes) {
ObservableDoubleValue obs = AppMainWindow.getInstance() != null
? AppMainWindow.getInstance().displayScale()
ObservableDoubleValue obs = AppMainWindow.get() != null
? AppMainWindow.get().displayScale()
: new SimpleDoubleProperty(1.0);
return Bindings.createStringBinding(
() -> {

View File

@@ -1,51 +0,0 @@
package io.xpipe.app.core;
import io.xpipe.app.core.window.AppDialog;
import javafx.scene.input.Clipboard;
import javafx.scene.input.DataFormat;
import lombok.Setter;
import java.util.List;
public class AppActionLinkDetector {
@Setter
private static String lastDetectedAction;
private static String getClipboardAction() {
var content = Clipboard.getSystemClipboard().getContent(DataFormat.URL);
if (content == null) {
content = Clipboard.getSystemClipboard().getContent(DataFormat.PLAIN_TEXT);
}
return content != null ? content.toString() : null;
}
public static void handle(String content, boolean showAlert) {
var detected = AppOpenArguments.parseActions(content);
if (detected.size() == 0) {
return;
}
if (showAlert && !showAlert()) {
return;
}
AppOpenArguments.handle(List.of(content));
}
public static void detectOnPaste() {
var content = getClipboardAction();
if (content == null) {
return;
}
lastDetectedAction = content;
handle(content, false);
}
private static boolean showAlert() {
return AppDialog.confirm("clipboardActionDetected");
}
}

View File

@@ -68,7 +68,7 @@ public class AppDesktopIntegration {
});
// Set dock icon explicitly on macOS
// This is necessary in case XPipe was started through a script as it will have no icon otherwise
// This is necessary in case the app was started through a script as it will have no icon otherwise
if (AppProperties.get().isDeveloperMode()
&& AppLogs.get().isWriteToSysout()
&& Taskbar.isTaskbarSupported()) {

View File

@@ -19,6 +19,8 @@ public class AppFont {
// Load ikonli fonts
TrackEvent.info("Loading ikonli fonts ...");
new FontIcon("mdi2s-stop");
new FontIcon("mdal-360");
new FontIcon("bi-alarm");
TrackEvent.info("Loading bundled fonts ...");
AppResources.with(

View File

@@ -108,7 +108,7 @@ public class AppFontSizes {
}
}
private static AppFontSizes interSize(AppFontSizes s) {
private static AppFontSizes fallbackFontSize(AppFontSizes s) {
if (s == BASE_10) {
return BASE_10;
} else if (s == BASE_10_5) {
@@ -128,7 +128,7 @@ public class AppFontSizes {
case OsType.MacOs ignored -> mac;
case OsType.Windows ignored -> windows;
};
return inter ? interSize(r) : r;
return inter ? fallbackFontSize(r) : r;
}
public static AppFontSizes getDefault() {

View File

@@ -215,7 +215,7 @@ public class AppTheme {
}
PlatformThread.runLaterIfNeeded(() -> {
var window = AppMainWindow.getInstance();
var window = AppMainWindow.get();
if (window == null) {
return;
}

View File

@@ -15,7 +15,7 @@ public class AppGnomeScaleDialog {
return;
}
if (AppMainWindow.getInstance() == null) {
if (AppMainWindow.get() == null) {
return;
}

View File

@@ -55,7 +55,7 @@ public class BaseMode extends OperationMode {
return;
}
// For debugging
// For debugging error handling
// if (true) throw new IllegalStateException();
TrackEvent.info("Initializing base mode components ...");

View File

@@ -40,7 +40,7 @@ public class GuiMode extends PlatformMode {
LicenseProvider.get().init();
PlatformThread.runLaterIfNeededBlocking(() -> {
AppMainWindow.getInstance().show();
AppMainWindow.get().show();
});
}
}

View File

@@ -1,7 +1,6 @@
package io.xpipe.app.core.mode;
import io.xpipe.app.beacon.AppBeaconServer;
import io.xpipe.app.browser.BrowserFullSessionModel;
import io.xpipe.app.core.*;
import io.xpipe.app.core.check.AppDebugModeCheck;
import io.xpipe.app.core.check.AppDirectoryPermissionsCheck;
@@ -17,7 +16,6 @@ import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.update.AppDistributionType;
import io.xpipe.app.util.*;
import io.xpipe.core.FailableRunnable;
import io.xpipe.core.OsType;
import io.xpipe.core.XPipeDaemonMode;
import javafx.application.Platform;
@@ -141,8 +139,8 @@ public abstract class OperationMode {
public static XPipeDaemonMode getStartupMode() {
var event = TrackEvent.withInfo("Startup mode determined");
if (AppMainWindow.getInstance() != null
&& AppMainWindow.getInstance().getStage().isShowing()) {
if (AppMainWindow.get() != null
&& AppMainWindow.get().getStage().isShowing()) {
event.tag("mode", "gui").tag("reason", "windowShowing").handle();
return XPipeDaemonMode.GUI;
}
@@ -374,8 +372,8 @@ public abstract class OperationMode {
BACKGROUND.onSwitchTo();
if (newMode != GUI
&& AppMainWindow.getInstance() != null
&& AppMainWindow.getInstance().getStage().isShowing()) {
&& AppMainWindow.get() != null
&& AppMainWindow.get().getStage().isShowing()) {
GUI.onSwitchTo();
newMode = GUI;
} else {

View File

@@ -11,7 +11,7 @@ import io.xpipe.app.prefs.CloseBehaviourDialog;
import io.xpipe.app.update.AppDistributionType;
import io.xpipe.app.platform.NativeWinWindowControl;
import io.xpipe.app.platform.PlatformThread;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.app.util.GlobalTimer;
import io.xpipe.core.OsType;
import javafx.beans.binding.Bindings;
@@ -35,7 +35,6 @@ import lombok.extern.jackson.Jacksonized;
import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import javax.imageio.ImageIO;
public class AppMainWindow {
@@ -52,7 +51,6 @@ public class AppMainWindow {
private final Stage stage;
private final BooleanProperty windowActive = new SimpleBooleanProperty(false);
private Thread thread;
private volatile Instant lastUpdate;
private boolean shown = false;
@@ -105,11 +103,12 @@ public class AppMainWindow {
if (AppPrefs.get() != null) {
stage.opacityProperty().bind(PlatformThread.sync(AppPrefs.get().windowOpacity()));
}
AppWindowHelper.addIcons(stage);
AppWindowHelper.setupStylesheets(stage.getScene());
AppWindowHelper.setupClickShield(stage);
AppWindowHelper.addMaximizedPseudoClass(stage);
AppWindowHelper.addFontSize(stage);
AppWindowStyle.addIcons(stage);
AppWindowStyle.addStylesheets(stage.getScene());
AppWindowStyle.addNavigationStyleClasses(stage.getScene());
AppWindowStyle.addClickShield(stage);
AppWindowStyle.addMaximizedPseudoClass(stage);
AppWindowStyle.addFontSize(stage);
AppTheme.initThemeHandlers(stage);
AppWindowTitle.getTitle().subscribe(s -> {
@@ -147,7 +146,7 @@ public class AppMainWindow {
});
}
public static AppMainWindow getInstance() {
public static AppMainWindow get() {
return INSTANCE;
}
@@ -186,29 +185,18 @@ public class AppMainWindow {
}
private synchronized void onChange() {
lastUpdate = Instant.now();
if (thread == null) {
thread = ThreadHelper.unstarted(() -> {
while (true) {
var toStop = lastUpdate.plus(Duration.of(1, ChronoUnit.SECONDS));
if (Instant.now().isBefore(toStop)) {
var toSleep = Duration.between(Instant.now(), toStop);
if (!toSleep.isNegative()) {
var ms = toSleep.toMillis();
ThreadHelper.sleep(ms);
}
} else {
break;
}
}
var timestamp = Instant.now();
lastUpdate = timestamp;
// Reduce printed window updates
GlobalTimer.delay(() -> {
if (!timestamp.equals(lastUpdate)) {
return;
}
synchronized (AppMainWindow.this) {
logChange();
thread = null;
}
});
thread.start();
}
synchronized (AppMainWindow.this) {
logChange();
}
}, Duration.ofSeconds(1));
}
private void logChange() {

View File

@@ -83,8 +83,8 @@ public class AppModifiedStage extends Stage {
case OsType.Linux ignored -> {}
case OsType.MacOs ignored -> {
var ctrl = new NativeMacOsWindowControl(stage);
var seamlessFrame = AppMainWindow.getInstance() != null
&& AppMainWindow.getInstance().getStage() == stage
var seamlessFrame = AppMainWindow.get() != null
&& AppMainWindow.get().getStage() == stage
&& !AppPrefs.get().performanceMode().get()
&& mergeFrame();
var seamlessFrameApplied = ctrl.setAppearance(
@@ -108,8 +108,8 @@ public class AppModifiedStage extends Stage {
boolean seamlessFrame;
if (AppPrefs.get().performanceMode().get()
|| !mergeFrame()
|| AppMainWindow.getInstance() == null
|| stage != AppMainWindow.getInstance().getStage()) {
|| AppMainWindow.get() == null
|| stage != AppMainWindow.get().getStage()) {
seamlessFrame = false;
} else {
// This is not available on Windows 10

View File

@@ -0,0 +1,97 @@
package io.xpipe.app.core.window;
import io.xpipe.app.platform.PlatformInit;
import javafx.application.Platform;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.input.KeyEvent;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import lombok.SneakyThrows;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Supplier;
public class AppSideWindow {
public static Optional<ButtonType> showBlockingAlert(Consumer<Alert> c) {
PlatformInit.init(true);
Supplier<Alert> supplier = () -> {
Alert a = createEmptyAlert();
var s = (Stage) a.getDialogPane().getScene().getWindow();
s.setOnShown(event -> {
Platform.runLater(() -> {
AppWindowBounds.clampWindow(s).ifPresent(rectangle2D -> {
s.setX(rectangle2D.getMinX());
s.setY(rectangle2D.getMinY());
// Somehow we have to set max size as setting the normal size does not work?
s.setMaxWidth(rectangle2D.getWidth());
s.setMaxHeight(rectangle2D.getHeight());
});
});
event.consume();
});
AppWindowBounds.fixInvalidStagePosition(s);
AppWindowStyle.addFontSize(s);
a.getDialogPane().getScene().addEventHandler(KeyEvent.KEY_PRESSED, event -> {
if (new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN).match(event)) {
s.close();
event.consume();
return;
}
if (event.getCode().equals(KeyCode.ESCAPE)) {
s.close();
event.consume();
}
});
return a;
};
AtomicReference<Optional<ButtonType>> result = new AtomicReference<>();
if (!Platform.isFxApplicationThread()) {
CountDownLatch latch = new CountDownLatch(1);
Platform.runLater(() -> {
try {
Alert a = supplier.get();
c.accept(a);
result.set(a.showAndWait());
} catch (Throwable t) {
result.set(Optional.empty());
} finally {
latch.countDown();
}
});
try {
latch.await();
} catch (InterruptedException ignored) {
}
} else {
Alert a = supplier.get();
c.accept(a);
result.set(a.showAndWait());
}
return result.get();
}
public static Alert createEmptyAlert() {
Alert alert = new Alert(Alert.AlertType.NONE);
if (AppMainWindow.get() != null) {
alert.initOwner(AppMainWindow.get().getStage());
}
alert.getDialogPane().getScene().setFill(Color.TRANSPARENT);
var stage = (Stage) alert.getDialogPane().getScene().getWindow();
AppModifiedStage.prepareStage(stage);
AppWindowStyle.addIcons(stage);
AppWindowStyle.addStylesheets(alert.getDialogPane().getScene());
AppWindowStyle.addNavigationStyleClasses(alert.getDialogPane().getScene());
return alert;
}
}

View File

@@ -2,8 +2,6 @@ package io.xpipe.app.core.window;
import io.xpipe.app.core.*;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.platform.InputHelper;
import io.xpipe.app.platform.PlatformInit;
import io.xpipe.core.OsType;
import javafx.application.Platform;
@@ -11,23 +9,15 @@ import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.css.PseudoClass;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.input.*;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import lombok.SneakyThrows;
import java.time.Duration;
import java.time.Instant;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Supplier;
public class AppWindowHelper {
public class AppWindowStyle {
public static void addMaximizedPseudoClass(Stage stage) {
stage.getScene().rootProperty().subscribe(root -> {
@@ -43,6 +33,29 @@ public class AppWindowHelper {
});
}
public static void addNavigationStyleClasses(Scene scene) {
Consumer<Boolean> onInput = kb -> {
var r = scene.getRoot();
if (r != null) {
// This property is broken on some systems
var acc = Platform.isAccessibilityActive();
r.pseudoClassStateChanged(PseudoClass.getPseudoClass("key-navigation"), kb);
r.pseudoClassStateChanged(PseudoClass.getPseudoClass("normal-navigation"), !kb);
r.pseudoClassStateChanged(PseudoClass.getPseudoClass("accessibility-navigation"), acc);
}
};
scene.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
var c = event.getCode();
var list = List.of(KeyCode.SPACE, KeyCode.ENTER, KeyCode.SHIFT, KeyCode.TAB);
onInput.accept(list.stream().anyMatch(keyCode -> keyCode == c)
|| event.getCode().isNavigationKey());
});
scene.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
onInput.accept(false);
});
}
public static void addIcons(Stage stage) {
stage.getIcons().clear();
@@ -59,82 +72,7 @@ public class AppWindowHelper {
});
}
@SneakyThrows
public static Optional<ButtonType> showBlockingAlert(Consumer<Alert> c) {
PlatformInit.init(true);
Supplier<Alert> supplier = () -> {
Alert a = AppWindowHelper.createEmptyAlert();
var s = (Stage) a.getDialogPane().getScene().getWindow();
s.setOnShown(event -> {
Platform.runLater(() -> {
AppWindowBounds.clampWindow(s).ifPresent(rectangle2D -> {
s.setX(rectangle2D.getMinX());
s.setY(rectangle2D.getMinY());
// Somehow we have to set max size as setting the normal size does not work?
s.setMaxWidth(rectangle2D.getWidth());
s.setMaxHeight(rectangle2D.getHeight());
});
});
event.consume();
});
AppWindowBounds.fixInvalidStagePosition(s);
AppWindowHelper.addFontSize(s);
a.getDialogPane().getScene().addEventHandler(KeyEvent.KEY_PRESSED, event -> {
if (new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN).match(event)) {
s.close();
event.consume();
return;
}
if (event.getCode().equals(KeyCode.ESCAPE)) {
s.close();
event.consume();
}
});
return a;
};
AtomicReference<Optional<ButtonType>> result = new AtomicReference<>();
if (!Platform.isFxApplicationThread()) {
CountDownLatch latch = new CountDownLatch(1);
Platform.runLater(() -> {
try {
Alert a = supplier.get();
c.accept(a);
result.set(a.showAndWait());
} catch (Throwable t) {
result.set(Optional.empty());
} finally {
latch.countDown();
}
});
try {
latch.await();
} catch (InterruptedException ignored) {
}
} else {
Alert a = supplier.get();
c.accept(a);
result.set(a.showAndWait());
}
return result.get();
}
public static Alert createEmptyAlert() {
Alert alert = new Alert(Alert.AlertType.NONE);
if (AppMainWindow.getInstance() != null) {
alert.initOwner(AppMainWindow.getInstance().getStage());
}
alert.getDialogPane().getScene().setFill(Color.TRANSPARENT);
var stage = (Stage) alert.getDialogPane().getScene().getWindow();
AppModifiedStage.prepareStage(stage);
addIcons(stage);
setupStylesheets(alert.getDialogPane().getScene());
return alert;
}
public static void setupStylesheets(Scene scene) {
public static void addStylesheets(Scene scene) {
AppStyle.addStylesheets(scene);
scene.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
@@ -146,20 +84,9 @@ public class AppWindowHelper {
}
});
TrackEvent.debug("Set stylesheet reload listener");
InputHelper.onNavigationInput(scene, (kb) -> {
var r = scene.getRoot();
if (r != null) {
// This property is broken on some systems
var acc = Platform.isAccessibilityActive();
r.pseudoClassStateChanged(PseudoClass.getPseudoClass("key-navigation"), kb);
r.pseudoClassStateChanged(PseudoClass.getPseudoClass("normal-navigation"), !kb);
r.pseudoClassStateChanged(PseudoClass.getPseudoClass("accessibility-navigation"), acc);
}
});
}
public static void setupClickShield(Stage stage) {
public static void addClickShield(Stage stage) {
if (OsType.getLocal() != OsType.MACOS) {
return;
}

View File

@@ -1,7 +1,6 @@
package io.xpipe.app.hub.comp;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.augment.GrowAugment;
import io.xpipe.app.core.AppFontSizes;
import javafx.beans.binding.Bindings;
@@ -139,8 +138,6 @@ public class DenseStoreEntryComp extends StoreEntryComp {
HBox.setHgrow(cr, Priority.ALWAYS);
grid.addRow(0, controls);
GrowAugment.create(true, false).augment(grid);
grid.getStyleClass().add("store-entry-grid");
grid.getStyleClass().add("dense");

View File

@@ -5,7 +5,6 @@ import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.SimpleCompStructure;
import io.xpipe.app.comp.augment.ContextMenuAugment;
import io.xpipe.app.comp.augment.GrowAugment;
import io.xpipe.app.comp.base.*;
import io.xpipe.app.core.*;
import io.xpipe.app.hub.action.HubBranchProvider;
@@ -126,7 +125,6 @@ public abstract class StoreEntryComp extends SimpleComp {
var button = new Button();
button.setGraphic(r);
GrowAugment.create(true, false).augment(new SimpleCompStructure<>(r));
button.getStyleClass().add("store-entry-comp");
button.setPadding(Insets.EMPTY);
button.setMaxWidth(5000);

View File

@@ -4,7 +4,6 @@ import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.base.DelayedInitComp;
import io.xpipe.app.comp.base.LeftSplitPaneComp;
import io.xpipe.app.core.AppActionLinkDetector;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.core.window.AppMainWindow;
import io.xpipe.app.platform.InputHelper;
@@ -26,7 +25,7 @@ public class StoreLayoutComp extends SimpleComp {
private Region createContent() {
var left = new StoreSidebarComp();
left.hide(AppMainWindow.getInstance().getStage().widthProperty().lessThan(1000));
left.hide(AppMainWindow.get().getStage().widthProperty().lessThan(1000));
left.minWidth(270);
left.maxWidth(500);
left.minHeight(0);
@@ -40,13 +39,6 @@ public class StoreLayoutComp extends SimpleComp {
AppLayoutModel.get().getSavedState().setSidebarWidth(aDouble);
});
comp.styleClass("store-layout");
comp.apply(struc -> {
InputHelper.onKeyCombination(
struc.get(), new KeyCodeCombination(KeyCode.V, KeyCombination.SHORTCUT_DOWN), true, keyEvent -> {
AppActionLinkDetector.detectOnPaste();
keyEvent.consume();
});
});
return comp.createRegion();
}
}

View File

@@ -1,7 +1,6 @@
package io.xpipe.app.issue;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.augment.GrowAugment;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.core.AppI18n;
@@ -66,7 +65,7 @@ public class ErrorHandlerComp extends SimpleComp {
});
});
b.disable(busy);
b.apply(GrowAugment.create(true, false));
b.maxWidth(2000);
return b.createRegion();
}

View File

@@ -62,16 +62,4 @@ public class InputHelper {
target.addEventHandler(KeyEvent.KEY_PRESSED, e);
}
}
public static void onNavigationInput(EventTarget target, Consumer<Boolean> r) {
target.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
var c = event.getCode();
var list = List.of(KeyCode.SPACE, KeyCode.ENTER, KeyCode.SHIFT, KeyCode.TAB);
r.accept(list.stream().anyMatch(keyCode -> keyCode == c)
|| event.getCode().isNavigationKey());
});
target.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
r.accept(false);
});
}
}

View File

@@ -78,7 +78,7 @@ public class TerminalDockComp extends SimpleComp {
}
private void setupListeners(StackPane stack) {
var s = AppMainWindow.getInstance().getStage();
var s = AppMainWindow.get().getStage();
var bounds = new ChangeListener<Bounds>() {
@Override

View File

@@ -2,7 +2,8 @@ package io.xpipe.app.util;
import io.xpipe.app.comp.base.SecretFieldComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.window.AppWindowHelper;
import io.xpipe.app.core.window.AppSideWindow;
import io.xpipe.app.core.window.AppWindowStyle;
import io.xpipe.app.secret.SecretManager;
import io.xpipe.app.secret.SecretQueryResult;
import io.xpipe.app.secret.SecretQueryState;
@@ -24,7 +25,7 @@ public class AskpassAlert {
public static SecretQueryResult queryRaw(String prompt, InPlaceSecretValue secretValue, boolean stealFocus) {
var prop = new SimpleObjectProperty<>(secretValue);
var r = AppWindowHelper.showBlockingAlert(alert -> {
var r = AppSideWindow.showBlockingAlert(alert -> {
alert.initModality(Modality.NONE);
alert.setTitle(AppI18n.get("askpassAlertTitle"));
alert.setHeaderText(prompt);

View File

@@ -2,7 +2,8 @@ package io.xpipe.app.util;
import io.xpipe.app.comp.base.TextFieldComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.window.AppWindowHelper;
import io.xpipe.app.core.window.AppSideWindow;
import io.xpipe.app.core.window.AppWindowStyle;
import javafx.animation.AnimationTimer;
import javafx.application.Platform;
@@ -17,7 +18,7 @@ public class AsktextAlert {
public static Optional<String> query(String prompt) {
var prop = new SimpleObjectProperty<String>();
var r = AppWindowHelper.showBlockingAlert(alert -> {
var r = AppSideWindow.showBlockingAlert(alert -> {
alert.setTitle(AppI18n.get("asktextAlertTitle"));
alert.setHeaderText(prompt);
alert.setAlertType(Alert.AlertType.CONFIRMATION);

View File

@@ -77,9 +77,9 @@ public class RemminaHelper {
.getValue(),
password != null ? password : "",
Math.round(
AppMainWindow.getInstance().getStage().getWidth()),
AppMainWindow.get().getStage().getWidth()),
Math.round(
AppMainWindow.getInstance().getStage().getHeight()));
AppMainWindow.get().getStage().getHeight()));
Files.createDirectories(file.getParent());
Files.writeString(file, string);
return file;

View File

@@ -78,7 +78,7 @@ public class LocalIdentityConvertHubLeafProvider implements HubLeafProvider<Loca
// Ugly solution to sync key file if needed
Platform.runLater(() -> {
var found = AppMainWindow.getInstance()
var found = AppMainWindow.get()
.getStage()
.getScene()
.getRoot()