Squash merge branch 15-release into master

This commit is contained in:
crschnick
2025-02-11 11:17:48 +00:00
parent c7171ce204
commit b494c69ded
4295 changed files with 5438 additions and 64653 deletions
+1
View File
@@ -8,6 +8,7 @@ dev.properties
extensions.txt
dev_storage
local/
local*/
local_*/
.vs
.vscode
+2 -3
View File
@@ -20,9 +20,8 @@ It currently supports:
- [Kubernetes](https://kubernetes.io/) clusters, pods, and containers
- [Windows Subsystem for Linux](https://ubuntu.com/wsl), [Cygwin](https://www.cygwin.com/), and [MSYS2](https://www.msys2.org/) instances
- [Powershell Remote Sessions](https://learn.microsoft.com/en-us/powershell/scripting/learn/remoting/running-remote-commands?view=powershell-7.3)
- [Teleport tsh connections](https://goteleport.com/)
- VNC connections
- Any other custom remote connection methods that work through the command-line
- [Tailscale SSH](https://tailscale.com/kb/1193/tailscale-ssh) and [Teleport](https://goteleport.com/) connections
- RDP and VNC connections
## Connection hub
+7 -2
View File
@@ -48,9 +48,10 @@ dependencies {
api 'com.vladsch.flexmark:flexmark-ext-yaml-front-matter:0.64.8'
api 'com.vladsch.flexmark:flexmark-ext-toc:0.64.8'
api("com.github.weisj:jsvg:1.7.0")
api files("$rootDir/gradle/gradle_scripts/markdowngenerator-1.3.1.1.jar")
api files("$rootDir/gradle/gradle_scripts/vernacular-1.16.jar")
api 'org.bouncycastle:bcprov-jdk18on:1.79'
api 'org.bouncycastle:bcprov-jdk18on:1.80'
api 'info.picocli:picocli:4.7.6'
api ('org.kohsuke:github-api:1.326') {
exclude group: 'org.apache.commons', module: 'commons-lang3'
@@ -108,6 +109,8 @@ run {
def exts = files(project.allExtensions.stream().map(p -> p.getTasksByName('jar', true)[0].outputs.files.singleFile).toList());
classpath += exts
dependsOn(project.allExtensions.stream().map(p -> p.getTasksByName('jar', true)[0]).toList())
}
task runAttachedDebugger(type: JavaExec) {
@@ -118,7 +121,7 @@ task runAttachedDebugger(type: JavaExec) {
modularity.inferModulePath = true
jvmArgs += jvmRunArgs
jvmArgs += List.of(
"-javaagent:${System.getProperty("user.home")}/.attachme/attachme-agent-1.2.4.jar=port:7857,host:localhost".toString(),
"-javaagent:${System.getProperty("user.home")}/.attachme/attachme-agent-1.2.9.jar=port:7857,host:localhost".toString(),
"-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=127.0.0.1:0"
)
jvmArgs += ['-XX:+EnableDynamicAgentLoading']
@@ -126,6 +129,8 @@ task runAttachedDebugger(type: JavaExec) {
def exts = files(project.allExtensions.stream().map(p -> p.getTasksByName('jar', true)[0].outputs.files.singleFile).toList());
classpath += exts
dependsOn(project.allExtensions.stream().map(p -> p.getTasksByName('jar', true)[0]).toList())
}
processResources {
+1 -1
View File
@@ -19,7 +19,7 @@ public class Main {
"""
The daemon executable xpiped does not accept any command-line arguments.
For a reference on what you can do from the CLI, take a look at the xpipe CLI executable instead.
For a reference on how to use xpipe from the command-line, take a look at https://docs.xpipe.io/cli.
""");
return;
}
@@ -15,9 +15,10 @@ public class CategoryAddExchangeImpl extends CategoryAddExchange {
throw new BeaconClientException("Parent category with id " + msg.getParent() + " does not exist");
}
var found = DataStorage.get().getStoreCategories().stream().filter(
dataStoreCategory -> msg.getParent().equals(dataStoreCategory.getParentCategory()) &&
msg.getName().equals(dataStoreCategory.getName())).findAny();
var found = DataStorage.get().getStoreCategories().stream()
.filter(dataStoreCategory -> msg.getParent().equals(dataStoreCategory.getParentCategory())
&& msg.getName().equals(dataStoreCategory.getName()))
.findAny();
if (found.isPresent()) {
return Response.builder().category(found.get().getUuid()).build();
}
@@ -2,10 +2,10 @@ package io.xpipe.app.beacon.impl;
import io.xpipe.app.beacon.AppBeaconServer;
import io.xpipe.app.beacon.BlobManager;
import io.xpipe.app.ext.ConnectionFileSystem;
import io.xpipe.app.util.FixedSizeInputStream;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.api.FsReadExchange;
import io.xpipe.core.store.ConnectionFileSystem;
import com.sun.net.httpserver.HttpExchange;
import lombok.SneakyThrows;
@@ -2,8 +2,8 @@ package io.xpipe.app.beacon.impl;
import io.xpipe.app.beacon.AppBeaconServer;
import io.xpipe.app.beacon.BlobManager;
import io.xpipe.app.ext.ConnectionFileSystem;
import io.xpipe.beacon.api.FsWriteExchange;
import io.xpipe.core.store.ConnectionFileSystem;
import com.sun.net.httpserver.HttpExchange;
import lombok.SneakyThrows;
@@ -0,0 +1,32 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.terminal.TerminalLauncherManager;
import io.xpipe.app.terminal.TerminalView;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.api.TerminalPrepareExchange;
import com.sun.net.httpserver.HttpExchange;
public class TerminalPrepareExchangeImpl extends TerminalPrepareExchange {
@Override
public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException {
TerminalView.get().open(msg.getRequest(), msg.getPid());
TerminalLauncherManager.registerPid(msg.getRequest(), msg.getPid());
var term = AppPrefs.get().terminalType().getValue();
var unicode = term.supportsUnicode();
var escapes = term.supportsEscapes();
var finished = TerminalLauncherManager.isCompletedSuccessfully(msg.getRequest());
return Response.builder()
.supportsUnicode(unicode)
.supportsEscapeSequences(escapes)
.alreadyFinished(finished)
.build();
}
@Override
public boolean requiresEnabledApi() {
return false;
}
}
@@ -1,7 +1,6 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.terminal.TerminalLauncherManager;
import io.xpipe.app.terminal.TerminalView;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.BeaconServerException;
import io.xpipe.beacon.api.TerminalWaitExchange;
@@ -9,10 +8,10 @@ import io.xpipe.beacon.api.TerminalWaitExchange;
import com.sun.net.httpserver.HttpExchange;
public class TerminalWaitExchangeImpl extends TerminalWaitExchange {
@Override
public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException, BeaconServerException {
TerminalView.get().open(msg.getRequest(), msg.getPid());
TerminalLauncherManager.waitExchange(msg.getRequest(), msg.getPid());
TerminalLauncherManager.waitExchange(msg.getRequest());
return Response.builder().build();
}
@@ -11,7 +11,6 @@ import io.xpipe.app.comp.base.LeftSplitPaneComp;
import io.xpipe.app.comp.base.StackComp;
import io.xpipe.app.comp.base.VerticalComp;
import io.xpipe.app.comp.store.StoreEntryWrapper;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.storage.DataStoreEntryRef;
@@ -62,7 +61,6 @@ public class BrowserFileChooserSessionComp extends DialogComp {
});
var comp = new BrowserFileChooserSessionComp(stage, model);
comp.apply(struc -> struc.get().setPrefSize(1200, 700))
.apply(struc -> AppFont.normal(struc.get()))
.styleClass("browser")
.styleClass("chooser");
return comp;
@@ -93,13 +93,14 @@ public class BrowserFullSessionComp extends SimpleComp {
node.setClip(null);
node.setPickOnBounds(false);
});
struc.get().lookupAll(".split-pane-divider").forEach(node -> node.setViewOrder(1));
struc.get().lookupAll(".split-pane-divider").forEach(node -> node.setViewOrder(-1));
});
}
});
});
splitPane.styleClass("browser");
return splitPane.createRegion();
var r = splitPane.createRegion();
return r;
}
private Comp<CompStructure<VBox>> createLeftSide() {
@@ -194,7 +195,6 @@ public class BrowserFullSessionComp extends SimpleComp {
struc.get().setMinWidth(rightSplit.get());
struc.get().setPrefWidth(rightSplit.get());
struc.get().setMaxWidth(rightSplit.get());
struc.get().getParent().requestLayout();
});
});
@@ -202,7 +202,6 @@ public class BrowserFullSessionComp extends SimpleComp {
struc.get().setMinWidth(newValue.doubleValue());
struc.get().setPrefWidth(newValue.doubleValue());
struc.get().setMaxWidth(newValue.doubleValue());
struc.get().getParent().requestLayout();
});
AnchorPane.setBottomAnchor(struc.get(), 0.0);
@@ -2,7 +2,6 @@ package io.xpipe.app.browser;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.browser.file.BrowserHistorySavedState;
import io.xpipe.app.browser.file.BrowserHistorySavedStateImpl;
import io.xpipe.app.browser.file.BrowserHistoryTabModel;
import io.xpipe.app.browser.file.BrowserTransferModel;
import io.xpipe.app.prefs.AppPrefs;
@@ -189,7 +188,9 @@ public class BrowserFullSessionModel extends BrowserAbstractSessionModel<Browser
// Prevent blocking of shutdown
closeAsync(o);
}
BrowserHistorySavedStateImpl.get().save();
if (all.size() > 0) {
ThreadHelper.sleep(1000);
}
}
// Delete all files
@@ -57,7 +57,7 @@ public class BrowserSessionTabsComp extends SimpleComp {
var tabs = createTabPane();
var topBackground = Comp.hspacer().styleClass("top-spacer").createRegion();
leftPadding.subscribe(number -> {
StackPane.setMargin(topBackground, new Insets(0, 0, 0, -number.doubleValue() - 6));
StackPane.setMargin(topBackground, new Insets(0, 0, 0, -number.doubleValue() - 3));
});
var stack = new StackPane(topBackground, tabs);
stack.setAlignment(Pos.TOP_CENTER);
@@ -217,7 +217,8 @@ public class BrowserSessionTabsComp extends SimpleComp {
headerArea
.paddingProperty()
.bind(Bindings.createObjectBinding(
() -> new Insets(2, 0, 4, -leftPadding.get() + 2), leftPadding));
() -> new Insets(2, 0, 4, -leftPadding.get() + 3), leftPadding));
tabs.setPadding(new Insets(0, 0, 0, -5));
headerHeight.bind(headerArea.heightProperty());
});
}
@@ -431,7 +432,7 @@ public class BrowserSessionTabsComp extends SimpleComp {
},
tabModel.getName(),
global,
AppPrefs.get().language(),
AppI18n.activeLanguage(),
AppPrefs.get().censorMode()));
} else {
tab.textProperty().bind(tabModel.getName());
@@ -45,7 +45,12 @@ public class BrowserClipboard {
}
List<File> data = (List<File>) clipboard.getData(DataFlavor.javaFileListFlavor);
var files = data.stream().map(f -> f.toPath()).toList();
// Sometimes file data can contain invalid chars. Why?
var files = data.stream()
.filter(file ->
file.toString().chars().noneMatch(value -> Character.isISOControl(value)))
.map(f -> f.toPath())
.toList();
if (files.size() == 0) {
return;
}
@@ -5,7 +5,7 @@ import io.xpipe.app.comp.base.FilterComp;
import io.xpipe.app.comp.base.HorizontalComp;
import io.xpipe.app.comp.store.StoreCategoryWrapper;
import io.xpipe.app.comp.store.StoreViewState;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.util.DataStoreCategoryChoiceComp;
import javafx.beans.property.Property;
@@ -33,14 +33,14 @@ public final class BrowserConnectionListFilterComp extends SimpleComp {
this.category)
.styleClass(Styles.LEFT_PILL)
.apply(struc -> {
AppFont.medium(struc.get());
AppFontSizes.base(struc.get());
});
var filter = new FilterComp(this.filter)
.styleClass(Styles.RIGHT_PILL)
.minWidth(0)
.hgrow()
.apply(struc -> {
AppFont.medium(struc.get());
AppFontSizes.base(struc.get());
});
var top = new HorizontalComp(List.of(category, filter))
@@ -51,6 +51,7 @@ public final class BrowserConnectionListFilterComp extends SimpleComp {
first.prefHeightProperty().bind(second.heightProperty());
first.minHeightProperty().bind(second.heightProperty());
first.maxHeightProperty().bind(second.heightProperty());
AppFontSizes.xl(struc.get());
})
.styleClass("bookmarks-header")
.createRegion();
@@ -1,7 +1,7 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.util.InputHelper;
import javafx.scene.control.ContextMenu;
@@ -24,13 +24,13 @@ public final class BrowserContextMenu extends ContextMenu {
}
private void createMenu() {
AppFontSizes.lg(getStyleableNode());
InputHelper.onLeft(this, false, e -> {
hide();
e.consume();
});
AppFont.normal(this.getStyleableNode());
var empty = source == null;
var selected = new ArrayList<>(
empty
@@ -125,7 +125,7 @@ public final class BrowserFileListComp extends SimpleComp {
fileList.setComparator(table.getComparator());
return true;
});
table.setFixedCellSize(32.0);
table.setFixedCellSize(30.0);
prepareColumnVisibility(table, ownerCol, filenameCol);
prepareTableScrollFix(table);
@@ -134,7 +134,6 @@ public final class BrowserFileListComp extends SimpleComp {
prepareTableEntries(table);
prepareTableChanges(table, filenameCol, mtimeCol, modeCol, ownerCol);
prepareTypedSelectionModel(table);
return table;
}
@@ -291,7 +290,7 @@ public final class BrowserFileListComp extends SimpleComp {
});
fileList.getSelection().addListener((ListChangeListener<? super BrowserEntry>) c -> {
var existing = new HashSet<>(fileList.getSelection());
var existing = new HashSet<>(table.getSelectionModel().getSelectedItems());
var toApply = new HashSet<>(c.getList());
if (existing.equals(toApply)) {
return;
@@ -1,13 +1,13 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.core.window.AppWindowHelper;
import io.xpipe.app.ext.ConnectionFileSystem;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.FileBridge;
import io.xpipe.app.util.FileOpener;
import io.xpipe.core.process.ElevationFunction;
import io.xpipe.core.process.OsType;
import io.xpipe.core.store.ConnectionFileSystem;
import io.xpipe.core.store.FileEntry;
import io.xpipe.core.store.FileInfo;
import io.xpipe.core.store.FileNames;
@@ -45,8 +45,8 @@ public class BrowserFileOpener {
return fileSystem.openOutput(file.getPath(), totalBytes);
}
var rootSc = sc.identicalSubShell()
.elevated(ElevationFunction.elevated("sudo"))
var rootSc = sc.identicalDialectSubShell()
.elevated(ElevationFunction.elevated(null))
.start();
var rootFs = new ConnectionFileSystem(rootSc);
try {
@@ -6,7 +6,6 @@ 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.comp.base.VBoxViewComp;
import io.xpipe.core.store.FileEntry;
import javafx.collections.ObservableList;
@@ -48,12 +47,10 @@ public class BrowserFileOverviewComp extends SimpleComp {
});
};
if (grow) {
var c = new ListBoxViewComp<>(list, list, factory, true).styleClass("overview-file-list");
return c.createRegion();
} else {
var c = new VBoxViewComp<>(list, list, factory).styleClass("overview-file-list");
return c.createRegion();
var c = new ListBoxViewComp<>(list, list, factory, true).styleClass("overview-file-list");
if (!grow) {
c.apply(struc -> struc.get().setFitToHeight(true));
}
return c.createRegion();
}
}
@@ -60,6 +60,7 @@ public class BrowserFileSelectionListComp extends SimpleComp {
.createRegion();
var t = nameTransformation.apply(entry);
var l = new Label(t.getValue(), image);
l.setGraphicTextGap(6);
l.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);
t.addListener((observable, oldValue, newValue) -> {
PlatformThread.runLaterIfNeeded(() -> {
@@ -7,7 +7,7 @@ import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.SimpleCompStructure;
import io.xpipe.app.comp.augment.ContextMenuAugment;
import io.xpipe.app.comp.base.*;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.util.InputHelper;
import io.xpipe.app.util.PlatformThread;
@@ -80,9 +80,10 @@ public class BrowserFileSystemTabComp extends SimpleComp {
var topBar = new HBox();
topBar.setAlignment(Pos.CENTER);
topBar.getStyleClass().add("top-bar");
AppFontSizes.xl(topBar);
var navBar = new BrowserNavBarComp(model).createStructure();
filter.textField().prefHeightProperty().bind(navBar.get().heightProperty());
AppFont.medium(navBar.get());
AppFontSizes.base(navBar.get());
topBar.getChildren()
.setAll(
overview,
@@ -10,7 +10,6 @@ import io.xpipe.app.ext.ProcessControlProvider;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.terminal.*;
import io.xpipe.app.util.BooleanScope;
@@ -26,6 +25,7 @@ import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import lombok.Getter;
import lombok.NonNull;
import lombok.SneakyThrows;
import java.io.IOException;
@@ -121,13 +121,15 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
var current = getCurrentDirectory();
// We might close this after storage shutdown
if (DataStorage.get() != null
&& DataStorage.get().getStoreEntries().contains(getEntry().get())
&& savedState != null
&& current != null) {
// If this entry does not exist, it's not that bad if we save it anyway
if (
// DataStorage.get() != null
// && DataStorage.get().getStoreEntries().contains(getEntry().get())
savedState != null && current != null) {
savedState.cd(current.getPath(), false);
BrowserHistorySavedStateImpl.get()
.add(new BrowserHistorySavedState.Entry(getEntry().get().getUuid(), current.getPath()));
BrowserHistorySavedStateImpl.get().save();
}
try {
fileSystem.close();
@@ -301,11 +303,22 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
if (ShellDialects.getStartableDialects().stream().anyMatch(dialect -> adjustedPath
.toLowerCase()
.startsWith(dialect.getExecutableName().toLowerCase()))) {
var cc = fileSystem
.getShell()
.get()
.singularSubShell(ShellOpenFunction.of(CommandBuilder.ofString(adjustedPath), false));
openTerminalAsync(name, directory, cc, true);
var sub = fileSystem.getShell().get().subShell();
var open = new ShellOpenFunction() {
@Override
public CommandBuilder prepareWithoutInitCommand() {
return CommandBuilder.ofString(adjustedPath);
}
@Override
public CommandBuilder prepareWithInitCommand(@NonNull String command) {
return CommandBuilder.ofString(command);
}
};
sub.setDumbOpen(open);
sub.setTerminalOpen(open);
openTerminalAsync(name, directory, sub, true);
} else {
var cc = fileSystem.getShell().get().command(adjustedPath);
openTerminalAsync(name, directory, cc, true);
@@ -328,7 +341,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
}
try {
BrowserFileSystemHelper.validateDirectoryPath(this, resolvedPath, customInput);
BrowserFileSystemHelper.validateDirectoryPath(this, resolvedPath, true);
cdSyncWithoutCheck(path);
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
@@ -1,7 +1,7 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.util.PlatformThread;
@@ -24,7 +24,7 @@ public class BrowserGreetingComp extends SimpleComp {
r.setText(getText());
});
});
AppFont.setSize(r, 7);
AppFontSizes.title(r);
if (OsType.getLocal() != OsType.MACOS) {
r.getStyleClass().add(Styles.TEXT_BOLD);
}
@@ -3,35 +3,26 @@ package io.xpipe.app.browser.file;
import io.xpipe.app.browser.BrowserFullSessionModel;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.comp.base.HorizontalComp;
import io.xpipe.app.comp.base.LabelComp;
import io.xpipe.app.comp.base.ListBoxViewComp;
import io.xpipe.app.comp.base.PrettyImageHelper;
import io.xpipe.app.comp.base.TileButtonComp;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.comp.base.*;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.BindingsHelper;
import io.xpipe.app.util.DerivedObservableList;
import io.xpipe.app.util.LabelGraphic;
import io.xpipe.app.util.ThreadHelper;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.scene.layout.*;
import atlantafx.base.controls.Spacer;
import atlantafx.base.theme.Styles;
import java.util.LinkedHashMap;
import java.util.List;
public class BrowserHistoryTabComp extends SimpleComp {
@@ -45,30 +36,12 @@ public class BrowserHistoryTabComp extends SimpleComp {
@Override
protected Region createSimple() {
var state = BrowserHistorySavedStateImpl.get();
var welcome = new BrowserGreetingComp().createSimple();
var vbox = new VBox(welcome, new Spacer(4, Orientation.VERTICAL));
vbox.setAlignment(Pos.CENTER_LEFT);
var img = PrettyImageHelper.ofSpecificFixedSize("graphics/Hips.svg", 50, 61)
.padding(new Insets(5, 0, 0, 0))
.createRegion();
var hbox = new HBox(img, vbox);
hbox.setAlignment(Pos.CENTER_LEFT);
hbox.setSpacing(15);
if (state == null) {
var header = new Label();
header.textProperty().bind(AppI18n.observable("browserWelcomeEmpty"));
vbox.getChildren().add(header);
hbox.setPadding(new Insets(40, 40, 40, 50));
return new VBox(hbox);
}
var list = new DerivedObservableList<>(state.getEntries(), true)
.filtered(e -> {
if (DataStorage.get() == null) {
return false;
}
var entry = DataStorage.get().getStoreEntryIfPresent(e.getUuid());
if (entry.isEmpty()) {
return false;
@@ -82,20 +55,22 @@ public class BrowserHistoryTabComp extends SimpleComp {
})
.getList();
var empty = Bindings.createBooleanBinding(() -> list.isEmpty(), list);
var contentDisplay = createListDisplay(list);
var emptyDisplay = createEmptyDisplay();
var map = new LinkedHashMap<Comp<?>, ObservableValue<Boolean>>();
map.put(emptyDisplay, empty);
map.put(contentDisplay, empty.not());
var stack = new MultiContentComp(map);
return stack.createRegion();
}
var headerBinding = BindingsHelper.flatMap(empty, b -> {
if (b) {
return AppI18n.observable("browserWelcomeEmpty");
} else {
return AppI18n.observable("browserWelcomeSystems");
}
});
var header = new LabelComp(headerBinding).createRegion();
AppFont.setSize(header, 1);
vbox.getChildren().add(header);
private Comp<?> createListDisplay(ObservableList<BrowserHistorySavedState.Entry> list) {
var state = BrowserHistorySavedStateImpl.get();
var storeList = new VBox();
storeList.setSpacing(8);
var welcome = new BrowserGreetingComp();
var header = new LabelComp(AppI18n.observable("browserWelcomeSystems"));
var vbox = new VerticalComp(List.of(welcome, Comp.vspacer(4), header));
vbox.apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT));
var listBox = new ListBoxViewComp<>(
list,
@@ -117,32 +92,38 @@ public class BrowserHistoryTabComp extends SimpleComp {
.apply(struc -> {
VBox vBox = (VBox) struc.get().getContent();
vBox.setSpacing(10);
})
.hide(empty)
.createRegion();
var layout = new VBox();
layout.getStyleClass().add("welcome");
layout.setPadding(new Insets(25, 40, 40, 40));
layout.setSpacing(18);
layout.getChildren().add(hbox);
layout.getChildren().add(Comp.separator().hide(empty).createRegion());
layout.getChildren().add(listBox);
VBox.setVgrow(layout.getChildren().get(2), Priority.NEVER);
layout.getChildren().add(Comp.separator().hide(empty).createRegion());
});
var tile = new TileButtonComp("restore", "restoreAllSessions", "mdmz-restore", actionEvent -> {
model.restoreState(state);
actionEvent.consume();
})
.grow(true, false)
.hide(empty)
.accessibleTextKey("restoreAllSessions");
layout.getChildren().add(tile.createRegion());
AppFont.medium(layout);
var layout = new VerticalComp(List.of(vbox, Comp.vspacer(5), listBox, Comp.separator(), tile));
layout.styleClass("welcome");
layout.spacing(14);
layout.maxWidth(1000);
layout.padding(new Insets(45, 40, 40, 50));
layout.apply(struc -> {
struc.get().setMaxWidth(1000);
});
return layout;
}
private Comp<?> createEmptyDisplay() {
var intro = new IntroComp(
"browserWelcomeEmpty",
new LabelGraphic.CompGraphic(PrettyImageHelper.ofSpecificFixedSize("graphics/Hips.svg", 100, 122)));
intro.setButtonAction(() -> {
BrowserFullSessionModel.DEFAULT.openFileSystemAsync(
DataStorage.get().local().ref(), null, null);
});
intro.setButtonDefault(true);
return intro;
}
private Comp<?> entryButton(BrowserHistorySavedState.Entry e, BooleanProperty disable) {
var entry = DataStorage.get().getStoreEntryIfPresent(e.getUuid());
var graphic = entry.get().getEffectiveIconFile();
@@ -9,12 +9,15 @@ import io.xpipe.app.comp.base.PrettyImageHelper;
import io.xpipe.app.comp.base.TextFieldComp;
import io.xpipe.app.comp.base.TooltipAugment;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.ContextMenuHelper;
import io.xpipe.app.util.PlatformThread;
import io.xpipe.app.util.ThreadHelper;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
@@ -84,15 +87,25 @@ public class BrowserNavBarComp extends Comp<BrowserNavBarComp.Structure> {
pathRegion.focusedProperty(),
model.getInOverview()));
var stack = new StackPane(pathRegion, breadcrumbsRegion);
stack.setAlignment(Pos.CENTER_LEFT);
pathRegion.prefHeightProperty().bind(stack.heightProperty());
stack.widthProperty().addListener((observable, oldValue, newValue) -> {
setMargin(stack, breadcrumbsRegion);
});
model.getCurrentPath().addListener((observable, oldValue, newValue) -> {
PlatformThread.runLaterIfNeeded(() -> {
setMargin(stack, breadcrumbsRegion);
});
});
// Prevent overflow
var clip = new Rectangle();
clip.widthProperty().bind(stack.widthProperty());
clip.heightProperty().bind(stack.heightProperty());
breadcrumbsRegion.setClip(clip);
stack.setClip(clip);
stack.setAlignment(Pos.CENTER_LEFT);
HBox.setHgrow(stack, Priority.ALWAYS);
var topBox = new HBox(homeButton, stack, historyButton);
@@ -110,6 +123,15 @@ public class BrowserNavBarComp extends Comp<BrowserNavBarComp.Structure> {
return new Structure(topBox, pathRegion, historyButton);
}
private void setMargin(StackPane stackPane, Region region) {
var off = region.getWidth() - stackPane.getWidth();
if (off <= 0) {
StackPane.setMargin(region, new Insets(0, 0, 0, 0));
} else {
StackPane.setMargin(region, new Insets(0, 20, 0, -off - 20));
}
}
private Comp<CompStructure<TextField>> createPathBar() {
var path = new SimpleStringProperty(model.getCurrentPath().get());
model.getCurrentPath().subscribe((newValue) -> {
@@ -172,7 +194,7 @@ public class BrowserNavBarComp extends Comp<BrowserNavBarComp.Structure> {
}
private ContextMenu createContextMenu() {
var cm = new ContextMenu();
var cm = ContextMenuHelper.create();
var f = model.getHistory().getForwardHistory(8).stream().toList();
for (int i = f.size() - 1; i >= 0; i--) {
@@ -3,7 +3,6 @@ package io.xpipe.app.browser.file;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.base.SimpleTitledPaneComp;
import io.xpipe.app.comp.base.VerticalComp;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.DerivedObservableList;
@@ -77,7 +76,6 @@ public class BrowserOverviewComp extends SimpleComp {
var vbox = new VerticalComp(List.of(recentPane, commonPane, rootsPane)).styleClass("overview");
var r = vbox.createRegion();
AppFont.medium(r);
return r;
}
}
@@ -2,6 +2,7 @@ package io.xpipe.app.browser.file;
import io.xpipe.app.browser.icon.BrowserIconManager;
import io.xpipe.app.comp.base.PrettyImageHelper;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.util.BooleanAnimationTimer;
import io.xpipe.app.util.InputHelper;
import io.xpipe.app.util.ThreadHelper;
@@ -41,6 +42,7 @@ public class BrowserQuickAccessContextMenu extends ContextMenu {
this.base = base;
this.model = model;
AppFontSizes.lg(getStyleableNode());
addEventFilter(Menu.ON_SHOWING, e -> {
Node content = getSkin().getNode();
if (content instanceof Region r) {
@@ -115,7 +117,13 @@ public class BrowserQuickAccessContextMenu extends ContextMenu {
var dirs = browserEntries.stream()
.filter(e -> e.getRawFileEntry().getKind() == FileKind.DIRECTORY)
.toList();
if (dirs.size() == 1) {
// Expand subdir if only one
// Note that if we have a link to the directory itself, we shouldn't do it, otherwise we are stuck in a loop
if (dirs.size() == 1
&& !dirs.getFirst()
.getRawFileEntry()
.getPath()
.equals(entry.getRawFileEntry().getPath())) {
updateMenuItems((Menu) menus.get(dirs.getFirst()), dirs.getFirst(), true);
}
newItems.addAll(menus.values());
@@ -7,7 +7,7 @@ import io.xpipe.app.comp.augment.ContextMenuAugment;
import io.xpipe.app.comp.base.HorizontalComp;
import io.xpipe.app.comp.base.IconButtonComp;
import io.xpipe.app.comp.base.LabelComp;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.util.BindingsHelper;
import io.xpipe.app.util.HumanReadableFormat;
import io.xpipe.app.util.PlatformThread;
@@ -49,7 +49,7 @@ public class BrowserStatusBarComp extends SimpleComp {
event.consume();
r.startFullDrag();
});
AppFont.small(r);
AppFontSizes.xs(r);
simulateEmptyCell(r);
return r;
}
@@ -3,7 +3,6 @@ package io.xpipe.app.browser.file;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.base.*;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.util.DerivedObservableList;
import io.xpipe.app.util.ThreadHelper;
@@ -81,7 +80,6 @@ public class BrowserTransferComp extends SimpleComp {
.grow(false, true);
var dragNotice = new LabelComp(AppI18n.observable("dragLocalFiles"))
.apply(struc -> struc.get().setGraphic(new FontIcon("mdi2h-hand-left")))
.apply(struc -> AppFont.medium(struc.get()))
.apply(struc -> struc.get().setWrapText(true))
.hide(model.getEmpty());
@@ -193,6 +191,7 @@ public class BrowserTransferComp extends SimpleComp {
});
});
return stack.styleClass("transfer").createRegion();
var r = stack.styleClass("transfer").createRegion();
return r;
}
}
@@ -17,7 +17,7 @@ public class BrowserIconVariant {
}
protected final String getIcon() {
var t = AppPrefs.get() != null ? AppPrefs.get().theme.getValue() : null;
var t = AppPrefs.get() != null ? AppPrefs.get().theme().getValue() : null;
if (t == null) {
return lightIcon;
}
@@ -142,7 +142,15 @@ public abstract class Comp<S extends CompStructure<?>> {
}
public Comp<S> visible(ObservableValue<Boolean> o) {
return apply(struc -> struc.get().visibleProperty().bind(o));
return apply(struc -> {
var region = struc.get();
BindingsHelper.preserve(region, o);
o.subscribe(n -> {
PlatformThread.runLaterIfNeeded(() -> {
region.setVisible(n);
});
});
});
}
public Comp<S> disable(ObservableValue<Boolean> o) {
@@ -3,7 +3,6 @@ package io.xpipe.app.comp.base;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.store.StoreViewState;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
@@ -68,7 +67,6 @@ public class AppLayoutComp extends Comp<AppLayoutComp.Structure> {
}
});
});
AppFont.normal(pane);
pane.getStyleClass().add("layout");
return new Structure(pane, multiR, sidebarR, new ArrayList<>(multiR.getChildren()));
}
@@ -2,7 +2,7 @@ package io.xpipe.app.comp.base;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.core.AppProperties;
import io.xpipe.app.core.window.AppDialog;
import io.xpipe.app.core.window.AppMainWindow;
@@ -57,7 +57,7 @@ public class AppMainWindowContentComp extends SimpleComp {
var version = new LabelComp((AppProperties.get().isStaging() ? "XPipe PTB" : "XPipe") + " "
+ AppProperties.get().getVersion());
version.apply(struc -> {
AppFont.setSize(struc.get(), 1);
AppFontSizes.xxl(struc.get());
struc.get().setOpacity(0.6);
});
@@ -83,12 +83,13 @@ public class AppMainWindowContentComp extends SimpleComp {
loaded.subscribe(struc -> {
if (struc != null) {
PlatformThread.runNestedLoopIteration();
struc.prepareAddition();
anim.stop();
struc.prepareAddition();
pane.getChildren().add(struc.get());
struc.show();
pane.getChildren().remove(vbox);
PlatformThread.runNestedLoopIteration();
pane.getStyleClass().remove("background");
pane.getChildren().remove(vbox);
struc.show();
}
});
@@ -66,7 +66,9 @@ public class ButtonComp extends Comp<CompStructure<Button>> {
}
});
}
button.setOnAction(e -> getListener().run());
if (listener != null) {
button.setOnAction(e -> getListener().run());
}
button.getStyleClass().add("button-comp");
return new SimpleCompStructure<>(button);
}
@@ -68,10 +68,7 @@ public class ContextualFileReferenceChoiceComp extends Comp<CompStructure<HBox>>
BrowserFileChooserSessionComp.openSingleFile(
() -> fileSystem.getValue(),
fileStore -> {
if (fileStore == null) {
filePath.setValue(null);
fileSystem.setValue(null);
} else {
if (fileStore != null) {
filePath.setValue(fileStore.getPath());
fileSystem.setValue(fileStore.getFileSystem());
}
@@ -6,45 +6,38 @@ import io.xpipe.app.comp.SimpleCompStructure;
import io.xpipe.app.util.PlatformThread;
import javafx.beans.binding.Bindings;
import javafx.collections.ObservableList;
import javafx.beans.value.ObservableIntegerValue;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.OverrunStyle;
import lombok.AllArgsConstructor;
import java.util.function.Function;
public class CountComp<T> extends Comp<CompStructure<Label>> {
@AllArgsConstructor
public class CountComp extends Comp<CompStructure<Label>> {
private final ObservableList<T> sub;
private final ObservableList<T> all;
private final ObservableIntegerValue sub;
private final ObservableIntegerValue all;
private final Function<String, String> transformation;
public CountComp(ObservableList<T> sub, ObservableList<T> all) {
this(sub, all, Function.identity());
}
public CountComp(ObservableList<T> sub, ObservableList<T> all, Function<String, String> transformation) {
this.sub = PlatformThread.sync(sub);
this.all = PlatformThread.sync(all);
this.transformation = transformation;
}
@Override
public CompStructure<Label> createBase() {
var label = new Label();
label.setTextOverrun(OverrunStyle.CLIP);
label.setAlignment(Pos.CENTER);
label.textProperty()
.bind(Bindings.createStringBinding(
() -> {
if (sub.size() == all.size()) {
return transformation.apply(all.size() + "");
} else {
return transformation.apply(sub.size() + "/" + all.size());
}
},
sub,
all));
var binding = Bindings.createStringBinding(
() -> {
if (sub.get() == all.get()) {
return transformation.apply(all.get() + "");
} else {
return transformation.apply(sub.get() + "/" + all.get());
}
},
sub,
all);
label.textProperty().bind(PlatformThread.sync(binding));
label.getStyleClass().add("count-comp");
return new SimpleCompStructure<>(label);
}
@@ -4,13 +4,13 @@ import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.SimpleCompStructure;
import io.xpipe.app.comp.augment.ContextMenuAugment;
import io.xpipe.app.util.ContextMenuHelper;
import javafx.beans.binding.Bindings;
import javafx.beans.value.ObservableValue;
import javafx.css.Size;
import javafx.css.SizeUnits;
import javafx.scene.control.Button;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import org.kordamp.ikonli.javafx.FontIcon;
@@ -27,11 +27,13 @@ public class DropdownComp extends Comp<CompStructure<Button>> {
@Override
public CompStructure<Button> createBase() {
ContextMenu cm = new ContextMenu(items.stream()
.map(comp -> {
return new MenuItem(null, comp.createRegion());
})
.toArray(MenuItem[]::new));
var cm = ContextMenuHelper.create();
cm.getItems()
.setAll(items.stream()
.map(comp -> {
return new MenuItem(null, comp.createRegion());
})
.toList());
Button button = (Button) new ButtonComp(null, () -> {})
.apply(new ContextMenuAugment<>(e -> true, null, () -> {
@@ -2,10 +2,17 @@ package io.xpipe.app.comp.base;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.FileOpener;
import io.xpipe.core.process.ShellScript;
import io.xpipe.core.process.ShellStoreState;
import io.xpipe.core.store.StatefulDataStore;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.TextArea;
import javafx.scene.layout.AnchorPane;
@@ -20,6 +27,33 @@ import java.nio.file.Files;
public class IntegratedTextAreaComp extends Comp<IntegratedTextAreaComp.Structure> {
public static IntegratedTextAreaComp script(
ObservableValue<DataStoreEntryRef<ShellStore>> host, Property<ShellScript> value) {
var string = new SimpleStringProperty(
value.getValue() != null ? value.getValue().getValue() : null);
string.addListener((observable, oldValue, newValue) -> {
value.setValue(newValue != null ? new ShellScript(newValue) : null);
});
var i = new IntegratedTextAreaComp(
string,
false,
"script",
Bindings.createStringBinding(
() -> {
return host.getValue() != null
&& host.getValue().getStore() instanceof StatefulDataStore<?> sd
&& sd.getState() instanceof ShellStoreState sss
&& sss.getShellDialect() != null
? sss.getShellDialect().getScriptFileEnding()
: "sh";
},
host));
i.minHeight(60);
i.prefHeight(60);
i.maxHeight(60);
return i;
}
private final Property<String> value;
private final boolean lazy;
private final String identifier;
@@ -1,7 +1,7 @@
package io.xpipe.app.comp.base;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.util.LabelGraphic;
import io.xpipe.core.process.OsType;
@@ -43,7 +43,7 @@ public class IntroComp extends SimpleComp {
if (OsType.getLocal() != OsType.MACOS) {
title.getStyleClass().add(Styles.TEXT_BOLD);
}
AppFont.setSize(title, 7);
AppFontSizes.title(title);
var introDesc = new Label();
introDesc.textProperty().bind(AppI18n.observable(translationsKey + "Content"));
@@ -50,8 +50,10 @@ public class LazyTextFieldComp extends Comp<CompStructure<TextField>> {
});
// Handles external updates
PlatformThread.sync(appliedValue).addListener((observable, oldValue, n) -> {
currentValue.setValue(n);
appliedValue.addListener((observable, oldValue, n) -> {
PlatformThread.runLaterIfNeeded(() -> {
currentValue.setValue(n);
});
});
r.setMinWidth(0);
@@ -3,6 +3,7 @@ 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.AppLayoutModel;
import io.xpipe.app.util.DerivedObservableList;
import io.xpipe.app.util.PlatformThread;
@@ -11,6 +12,7 @@ import javafx.beans.binding.Bindings;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.scene.Node;
import javafx.scene.control.ScrollBar;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.Region;
@@ -18,10 +20,7 @@ import javafx.scene.layout.VBox;
import lombok.Setter;
import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.function.Function;
public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
@@ -34,7 +33,6 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
private final ObservableList<T> shown;
private final ObservableList<T> all;
private final Function<T, Comp<?>> compFunction;
private final int limit = Integer.MAX_VALUE;
private final boolean scrollBar;
@Setter
@@ -55,12 +53,12 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
VBox vbox = new VBox();
vbox.getStyleClass().add("list-box-content");
vbox.setFocusTraversable(false);
var scroll = new ScrollPane(vbox);
refresh(vbox, shown, all, cache, false);
vbox.requestLayout();
refresh(scroll, vbox, shown, all, cache, false, false);
shown.addListener((ListChangeListener<? super T>) (c) -> {
refresh(vbox, c.getList(), all, cache, true);
refresh(scroll, vbox, c.getList(), all, cache, true, true);
});
all.addListener((ListChangeListener<? super T>) c -> {
@@ -69,7 +67,6 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
}
});
var scroll = new ScrollPane(vbox);
if (scrollBar) {
scroll.setVbarPolicy(ScrollPane.ScrollBarPolicy.ALWAYS);
scroll.skinProperty().subscribe(newValue -> {
@@ -93,15 +90,110 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scroll.setFitToWidth(true);
scroll.getStyleClass().add("list-box-view-comp");
scroll.vvalueProperty().addListener((observable, oldValue, newValue) -> {
updateVisibilities(scroll, vbox);
});
scroll.heightProperty().addListener((observable, oldValue, newValue) -> {
updateVisibilities(scroll, vbox);
});
vbox.heightProperty().addListener((observable, oldValue, newValue) -> {
Platform.runLater(() -> {
updateVisibilities(scroll, vbox);
});
});
// We can't directly listen to any parent element changing visibility, so this is a compromise
if (AppLayoutModel.get() != null) {
AppLayoutModel.get().getSelected().addListener((observable, oldValue, newValue) -> {
PlatformThread.runLaterIfNeeded(() -> {
updateVisibilities(scroll, vbox);
});
});
}
vbox.sceneProperty().addListener((observable, oldValue, newValue) -> {
Node c = vbox;
while ((c = c.getParent()) != null) {
c.boundsInParentProperty().addListener((observable1, oldValue1, newValue1) -> {
updateVisibilities(scroll, vbox);
});
}
Platform.runLater(() -> {
updateVisibilities(scroll, vbox);
});
if (newValue != null) {
newValue.heightProperty().addListener((observable1, oldValue1, newValue1) -> {
updateVisibilities(scroll, vbox);
});
}
});
return new SimpleCompStructure<>(scroll);
}
private boolean isVisible(ScrollPane pane, VBox box, Node node) {
if (pane.getScene() == null || box.getScene() == null || node.getScene() == null) {
return false;
}
var paneHeight = pane.getHeight();
var scrollCenter = box.getBoundsInLocal().getHeight() * pane.getVvalue();
var minBoundsHeight = scrollCenter - paneHeight;
var maxBoundsHeight = scrollCenter + paneHeight;
var nodeMinHeight = node.getBoundsInParent().getMinY();
var nodeMaxHeight = node.getBoundsInParent().getMaxY();
if (paneHeight == 0.0
|| box.getHeight() == 0.0
|| ((Region) node).getHeight() == 0.0
|| nodeMinHeight == nodeMaxHeight) {
return false;
}
if (nodeMaxHeight < minBoundsHeight) {
return false;
}
if (nodeMinHeight > maxBoundsHeight) {
return false;
}
if (pane.getScene().getHeight() > 200) {
var sceneNodeBounds = node.localToScene(node.getBoundsInLocal());
if (sceneNodeBounds.getMaxY() < 0
|| sceneNodeBounds.getMinY() > pane.getScene().getHeight()) {
return false;
}
}
return true;
}
private void updateVisibilities(ScrollPane scroll, VBox vbox) {
for (Node child : vbox.getChildren()) {
var v = isVisible(scroll, vbox, child);
child.setVisible(v);
}
}
private void refresh(
VBox listView, List<? extends T> shown, List<? extends T> all, Map<T, Region> cache, boolean asynchronous) {
ScrollPane scroll,
VBox listView,
List<? extends T> shown,
List<? extends T> all,
Map<T, Region> cache,
boolean asynchronous,
boolean refreshVisibilities) {
Runnable update = () -> {
synchronized (cache) {
var set = new HashSet<T>();
// These lists might diverge on updates
set.addAll(shown);
set.addAll(all);
// Clear cache of unused values
cache.keySet().removeIf(t -> !all.contains(t));
cache.keySet().removeIf(t -> !set.contains(t));
}
final long[] lastPause = {System.currentTimeMillis()};
@@ -117,13 +209,18 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
if (!cache.containsKey(v)) {
var comp = compFunction.apply(v);
cache.put(v, comp != null ? comp.createRegion() : null);
if (comp != null) {
var r = comp.createRegion();
r.setVisible(false);
cache.put(v, r);
} else {
cache.put(v, null);
}
}
return cache.get(v);
})
.filter(region -> region != null)
.limit(limit)
.toList();
if (listView.getChildren().equals(newShown)) {
@@ -140,6 +237,9 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
var d = new DerivedObservableList<>(listView.getChildren(), true);
d.setContent(newShown);
if (refreshVisibilities) {
updateVisibilities(scroll, listView);
}
};
if (asynchronous) {
@@ -91,7 +91,8 @@ public class ListSelectorComp<T> extends SimpleComp {
if (showAllSelector.get()) {
var allSelector = new CheckBox(null);
allSelector.setSelected(currentVals.stream().filter(t -> !disable.test(t)).count() == selected.size());
allSelector.setSelected(
currentVals.stream().filter(t -> !disable.test(t)).count() == selected.size());
allSelector.selectedProperty().addListener((observable, oldValue, newValue) -> {
cbs.forEach(checkBox -> {
if (checkBox.isDisabled()) {
@@ -0,0 +1,138 @@
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.util.DerivedObservableList;
import io.xpipe.app.util.PlatformThread;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.scene.control.ScrollBar;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.skin.VirtualFlow;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import lombok.Setter;
import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
public class ListVirtualViewComp<T> extends Comp<CompStructure<ScrollPane>> {
private static final PseudoClass ODD = PseudoClass.getPseudoClass("odd");
private static final PseudoClass EVEN = PseudoClass.getPseudoClass("even");
private static final PseudoClass FIRST = PseudoClass.getPseudoClass("first");
private static final PseudoClass LAST = PseudoClass.getPseudoClass("last");
private final ObservableList<T> shown;
private final ObservableList<T> all;
private final Function<T, Comp<?>> compFunction;
private final int limit = Integer.MAX_VALUE;
private final boolean scrollBar;
@Setter
private int platformPauseInterval = -1;
public ListVirtualViewComp(
ObservableList<T> shown, ObservableList<T> all, Function<T, Comp<?>> compFunction, boolean scrollBar) {
this.shown = shown;
this.all = all;
this.compFunction = compFunction;
this.scrollBar = scrollBar;
}
@Override
public CompStructure<ScrollPane> createBase() {
Map<T, Region> cache = new IdentityHashMap<>();
var vbox = new VirtualFlow<>();
vbox.getStyleClass().add("list-box-content");
vbox.setFocusTraversable(false);
var scroll = new ScrollPane(vbox);
if (scrollBar) {
scroll.setVbarPolicy(ScrollPane.ScrollBarPolicy.ALWAYS);
scroll.skinProperty().subscribe(newValue -> {
if (newValue != null) {
ScrollBar bar = (ScrollBar) scroll.lookup(".scroll-bar:vertical");
bar.opacityProperty()
.bind(Bindings.createDoubleBinding(
() -> {
var v = bar.getVisibleAmount();
// Check for rounding and accuracy issues
// It might not be exactly equal to 1.0
return v < 0.99 ? 1.0 : 0.0;
},
bar.visibleAmountProperty()));
}
});
} else {
scroll.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scroll.setFitToHeight(true);
}
scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scroll.setFitToWidth(true);
scroll.getStyleClass().add("list-box-view-comp");
return new SimpleCompStructure<>(scroll);
}
private void refresh(
VBox listView, List<? extends T> shown, List<? extends T> all, Map<T, Region> cache, boolean asynchronous) {
Runnable update = () -> {
synchronized (cache) {
// Clear cache of unused values
cache.keySet().removeIf(t -> !all.contains(t));
}
final long[] lastPause = {System.currentTimeMillis()};
// Create copy to reduce chances of concurrent modification
var shownCopy = new ArrayList<>(shown);
var newShown = shownCopy.stream()
.map(v -> {
var elapsed = System.currentTimeMillis() - lastPause[0];
if (platformPauseInterval != -1 && elapsed > platformPauseInterval) {
PlatformThread.runNestedLoopIteration();
lastPause[0] = System.currentTimeMillis();
}
if (!cache.containsKey(v)) {
var comp = compFunction.apply(v);
cache.put(v, comp != null ? comp.createRegion() : null);
}
return cache.get(v);
})
.filter(region -> region != null)
.limit(limit)
.toList();
if (listView.getChildren().equals(newShown)) {
return;
}
for (int i = 0; i < newShown.size(); i++) {
var r = newShown.get(i);
r.pseudoClassStateChanged(ODD, i % 2 != 0);
r.pseudoClassStateChanged(EVEN, i % 2 == 0);
r.pseudoClassStateChanged(FIRST, i == 0);
r.pseudoClassStateChanged(LAST, i == newShown.size() - 1);
}
var d = new DerivedObservableList<>(listView.getChildren(), true);
d.setContent(newShown);
};
if (asynchronous) {
Platform.runLater(update);
} else {
PlatformThread.runLaterIfNeeded(update);
}
}
}
@@ -57,7 +57,10 @@ public class LoadingOverlayComp extends Comp<CompStructure<StackPane>> {
}
if (!showLoading.getValue()) {
Platform.runLater(() -> loadingOverlay.setVisible(false));
Platform.runLater(() -> {
loadingOverlay.setVisible(false);
loadingOverlay.setManaged(false);
});
}
});
} else {
@@ -68,7 +71,10 @@ public class LoadingOverlayComp extends Comp<CompStructure<StackPane>> {
}
if (showLoading.getValue()) {
Platform.runLater(() -> loadingOverlay.setVisible(true));
Platform.runLater(() -> {
loadingOverlay.setVisible(true);
loadingOverlay.setManaged(true);
});
}
});
}
@@ -95,8 +95,8 @@ public class MarkdownComp extends Comp<CompStructure<StackPane>> {
.setUserDataDirectory(
AppProperties.get().getDataDir().resolve("webview").toFile());
var theme = AppPrefs.get() != null
&& AppPrefs.get().theme.getValue() != null
&& AppPrefs.get().theme.getValue().isDark()
&& AppPrefs.get().theme().getValue() != null
&& AppPrefs.get().theme().getValue().isDark()
? "misc/github-markdown-dark.css"
: "misc/github-markdown-light.css";
var url = AppResources.getResourceURL(AppResources.XPIPE_MODULE, theme).orElseThrow();
@@ -40,7 +40,11 @@ public class ModalButton {
}
public static ModalButton cancel() {
return new ModalButton("cancel", null, true, false);
return cancel(null);
}
public static ModalButton cancel(Runnable action) {
return new ModalButton("cancel", action, true, false);
}
public static ModalButton skip() {
@@ -64,6 +64,10 @@ public class ModalOverlay {
AppDialog.show(this, false);
}
public boolean isShowing() {
return AppDialog.getModalOverlay().contains(this);
}
public void showAndWait() {
AppDialog.showAndWait(this);
}
@@ -2,7 +2,7 @@ package io.xpipe.app.comp.base;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppLogs;
import io.xpipe.app.util.PlatformThread;
@@ -45,9 +45,9 @@ public class ModalOverlayComp extends SimpleComp {
protected Region createSimple() {
var bgRegion = background.createRegion();
var modal = new ModalPane();
modal.setInTransitionFactory(OsType.getLocal() == OsType.LINUX ? null : node -> fadeInDelyed(node));
modal.setInTransitionFactory(null);
modal.setOutTransitionFactory(
OsType.getLocal() == OsType.LINUX ? null : node -> Animations.fadeOut(node, Duration.millis(200)));
OsType.getLocal() == OsType.LINUX ? null : node -> Animations.fadeOut(node, Duration.millis(50)));
modal.focusedProperty().addListener((observable, oldValue, newValue) -> {
var c = modal.getContent();
if (newValue && c != null) {
@@ -177,7 +177,7 @@ public class ModalOverlayComp extends SimpleComp {
AppI18n.get(newValue.getTitleKey()),
newValue.getGraphic() != null ? newValue.getGraphic().createGraphicNode() : null);
l.setGraphicTextGap(8);
AppFont.normal(l);
AppFontSizes.xl(l);
content.getChildren().addFirst(l);
} else {
content.getChildren().addFirst(Comp.vspacer(0).createRegion());
@@ -194,7 +194,7 @@ public class ModalOverlayComp extends SimpleComp {
}
}
content.getChildren().add(buttonBar);
AppFont.small(buttonBar);
AppFontSizes.xs(buttonBar);
}
var modalBox = new ModalBox(content) {
@@ -228,6 +228,14 @@ public class ModalOverlayComp extends SimpleComp {
var busy = mocc.busy();
if (busy != null) {
var loading = LoadingOverlayComp.noProgress(Comp.of(() -> modalBox), busy);
// loading.apply(struc -> {
// var bg = struc.get().getChildren().getFirst();
// struc.get().getChildren().get(1).addEventFilter(MouseEvent.MOUSE_PRESSED, event ->
// {
// bg.fireEvent(event);
// event.consume();
// });
// });
return loading.createRegion();
}
}
@@ -289,7 +297,7 @@ public class ModalOverlayComp extends SimpleComp {
var t = new Timeline(
new KeyFrame(Duration.ZERO, new KeyValue(node.opacityProperty(), 0.01)),
new KeyFrame(Duration.millis(50), new KeyValue(node.opacityProperty(), 0.01, Animations.EASE)),
new KeyFrame(Duration.millis(150), new KeyValue(node.opacityProperty(), 1, Animations.EASE)));
new KeyFrame(Duration.millis(1250), new KeyValue(node.opacityProperty(), 1, Animations.EASE)));
t.statusProperty().addListener((obs, old, val) -> {
if (val == Animation.Status.STOPPED) {
@@ -6,9 +6,9 @@ import javafx.beans.value.ObservableValue;
import lombok.Getter;
@Getter
public abstract class ModalOverlayContentComp extends SimpleComp {
@Getter
protected ModalOverlay modalOverlay;
void setModalOverlay(ModalOverlay modalOverlay) {
@@ -3,7 +3,7 @@ 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.AppFont;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.util.PlatformThread;
import javafx.beans.Observable;
@@ -58,7 +58,7 @@ public class OptionsComp extends Comp<CompStructure<Pane>> {
if (showVertical) {
var line = new VBox();
line.prefWidthProperty().bind(pane.widthProperty());
line.setSpacing(5);
line.setSpacing(2);
var name = new Label();
name.getStyleClass().add("name");
@@ -92,7 +92,7 @@ public class OptionsComp extends Comp<CompStructure<Pane>> {
popover.setCloseButtonEnabled(false);
popover.setHeaderAlwaysVisible(false);
popover.setDetachable(true);
AppFont.small(popover.getContentNode());
AppFontSizes.xs(popover.getContentNode());
var extendedDescription = new Button("... ?");
extendedDescription.setMinWidth(Region.USE_PREF_SIZE);
@@ -100,7 +100,7 @@ public class OptionsComp extends Comp<CompStructure<Pane>> {
extendedDescription.getStyleClass().add(Styles.ACCENT);
extendedDescription.getStyleClass().add("long-description");
extendedDescription.setAccessibleText("Help");
AppFont.normal(extendedDescription);
AppFontSizes.xl(extendedDescription);
extendedDescription.setOnAction(e -> {
popover.show(extendedDescription);
e.consume();
@@ -119,6 +119,7 @@ public class OptionsComp extends Comp<CompStructure<Pane>> {
}
} else {
line.getChildren().add(description);
line.getChildren().add(new Spacer(2, Orientation.VERTICAL));
}
}
@@ -128,6 +129,7 @@ public class OptionsComp extends Comp<CompStructure<Pane>> {
compRegion.accessibleHelpProperty().bind(PlatformThread.sync(entry.description()));
}
line.getChildren().add(compRegion);
compRegion.getStyleClass().add("options-content");
}
pane.getChildren().add(line);
@@ -2,7 +2,7 @@ package io.xpipe.app.comp.base;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppFontSizes;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.Button;
@@ -32,7 +32,7 @@ public class PopupMenuButtonComp extends SimpleComp {
popover.setCloseButtonEnabled(false);
popover.setHeaderAlwaysVisible(false);
popover.setDetachable(true);
AppFont.small(popover.getContentNode());
AppFontSizes.xs(popover.getContentNode());
var extendedDescription = new Button();
extendedDescription.textProperty().bind(name);
@@ -94,8 +94,8 @@ public class PrettyImageComp extends SimpleComp {
Consumer<String> update = val -> {
var useDark = AppPrefs.get() != null
&& AppPrefs.get().theme.get() != null
&& AppPrefs.get().theme.get().isDark();
&& AppPrefs.get().theme().getValue() != null
&& AppPrefs.get().theme().getValue().isDark();
var fixed = val != null
? FileNames.getBaseName(val) + (useDark ? "-dark" : "") + "." + FileNames.getExtension(val)
: null;
@@ -110,7 +110,7 @@ public class PrettyImageComp extends SimpleComp {
PlatformThread.sync(value).subscribe(update);
if (AppPrefs.get() != null) {
AppPrefs.get().theme.addListener((observable, oldValue, newValue) -> {
AppPrefs.get().theme().addListener((observable, oldValue, newValue) -> {
update.accept(value.getValue());
});
}
@@ -1,116 +0,0 @@
package io.xpipe.app.comp.base;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.resources.AppImages;
import io.xpipe.app.util.PlatformThread;
import io.xpipe.core.store.FileNames;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Pos;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import java.util.function.Consumer;
public class PrettySvgComp extends SimpleComp {
private final ObservableValue<String> value;
private final double width;
private final double height;
public PrettySvgComp(ObservableValue<String> value, double width, double height) {
this.value = value;
this.width = width;
this.height = height;
}
@Override
protected Region createSimple() {
var image = new SimpleStringProperty();
var syncValue = PlatformThread.sync(value);
var storeIcon = SvgView.create(Bindings.createObjectBinding(
() -> {
if (image.get() == null) {
return null;
}
if (AppImages.hasSvgImage(image.getValue())) {
return AppImages.svgImage(image.getValue());
} else if (AppImages.hasSvgImage(image.getValue().replace("-dark", ""))) {
return AppImages.svgImage(image.getValue().replace("-dark", ""));
} else {
return null;
}
},
image));
var ar = Bindings.createDoubleBinding(
() -> {
return storeIcon.getWidth().getValue().doubleValue()
/ storeIcon.getHeight().getValue().doubleValue();
},
storeIcon.getWidth(),
storeIcon.getHeight());
var widthProperty = Bindings.createDoubleBinding(
() -> {
boolean widthLimited = width / height < ar.doubleValue();
if (widthLimited) {
return width;
} else {
return height * ar.doubleValue();
}
},
ar);
var heightProperty = Bindings.createDoubleBinding(
() -> {
boolean widthLimited = width / height < ar.doubleValue();
if (widthLimited) {
return width / ar.doubleValue();
} else {
return height;
}
},
ar);
var stack = new StackPane();
var wv = storeIcon.createWebview();
if (wv.isPresent()) {
var node = wv.get();
node.prefWidthProperty().bind(widthProperty);
node.maxWidthProperty().bind(widthProperty);
node.minWidthProperty().bind(widthProperty);
node.prefHeightProperty().bind(heightProperty);
node.maxHeightProperty().bind(heightProperty);
node.minHeightProperty().bind(heightProperty);
stack.getChildren().add(node);
}
Consumer<String> update = val -> {
var useDark = AppPrefs.get() != null
&& AppPrefs.get().theme.get() != null
&& AppPrefs.get().theme.get().isDark();
var fixed = val != null
? FileNames.getBaseName(val) + (useDark ? "-dark" : "") + "." + FileNames.getExtension(val)
: null;
image.set(fixed);
};
syncValue.subscribe(update);
if (AppPrefs.get() != null) {
AppPrefs.get().theme.addListener((observable, oldValue, newValue) -> {
update.accept(syncValue.getValue());
});
}
stack.setFocusTraversable(false);
stack.setPrefWidth(width);
stack.setMinWidth(width);
stack.setPrefHeight(height);
stack.setMinHeight(height);
stack.setAlignment(Pos.CENTER);
stack.getStyleClass().add("stack");
return stack;
}
}
@@ -3,10 +3,10 @@ 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.AppFont;
import io.xpipe.app.core.AppDistributionType;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.update.UpdateAvailableDialog;
import io.xpipe.app.update.XPipeDistributionType;
import io.xpipe.app.util.PlatformThread;
import javafx.application.Platform;
@@ -76,7 +76,7 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
var shortcut = e.combination();
b.apply(new TooltipAugment<>(e.name(), shortcut));
b.apply(struc -> {
AppFont.setSize(struc.get(), 1);
AppFontSizes.xl(struc.get());
struc.get().pseudoClassStateChanged(selected, value.getValue().equals(e));
value.addListener((c, o, n) -> {
PlatformThread.runLaterIfNeeded(() -> {
@@ -123,17 +123,17 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
.tooltipKey("updateAvailableTooltip")
.accessibleTextKey("updateAvailableTooltip");
b.apply(struc -> {
AppFont.setSize(struc.get(), 1);
AppFontSizes.xl(struc.get());
});
b.hide(PlatformThread.sync(Bindings.createBooleanBinding(
() -> {
return XPipeDistributionType.get()
return AppDistributionType.get()
.getUpdateHandler()
.getPreparedUpdate()
.getValue()
== null;
},
XPipeDistributionType.get().getUpdateHandler().getPreparedUpdate())));
AppDistributionType.get().getUpdateHandler().getPreparedUpdate())));
vbox.getChildren().add(b.createRegion());
}
@@ -142,7 +142,6 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
filler.setMaxHeight(3000);
vbox.getChildren().add(filler);
VBox.setVgrow(filler, Priority.ALWAYS);
filler.prefWidthProperty().bind(((Region) vbox.getChildren().getFirst()).widthProperty());
vbox.getStyleClass().add("sidebar-comp");
return new SimpleCompStructure<>(vbox);
}
@@ -1,45 +0,0 @@
package io.xpipe.app.comp.base;
import javafx.css.Size;
import javafx.css.SizeUnits;
import javafx.geometry.Point2D;
import java.util.regex.Pattern;
public class SvgHelper {
public static Size parseSize(String string) {
for (SizeUnits unit : SizeUnits.values()) {
if (string.endsWith(unit.toString())) {
return new Size(
Double.parseDouble(string.substring(
0, string.length() - unit.toString().length())),
unit);
}
}
return new Size(Double.parseDouble(string), SizeUnits.PX);
}
public static Point2D getDimensions(String val) {
var regularExpression = Pattern.compile("<svg[^>]+?width=\"([^ ]+)\"", Pattern.DOTALL);
var matcher = regularExpression.matcher(val);
if (!matcher.find()) {
var viewBox = Pattern.compile(
"<svg.+?viewBox=\"([\\d.]+)\\s+([\\d.]+)\\s+([\\d.]+)\\s+([\\d.]+)\"", Pattern.DOTALL);
matcher = viewBox.matcher(val);
if (matcher.find()) {
return new Point2D(
parseSize(matcher.group(3)).pixels(),
parseSize(matcher.group(4)).pixels());
}
}
var width = matcher.group(1);
regularExpression = Pattern.compile("<svg.+?height=\"([^ ]+)\"", Pattern.DOTALL);
matcher = regularExpression.matcher(val);
matcher.find();
var height = matcher.group(1);
return new Point2D(parseSize(width).pixels(), parseSize(height).pixels());
}
}
@@ -1,146 +0,0 @@
package io.xpipe.app.comp.base;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.core.AppProperties;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.PlatformThread;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.scene.AccessibleRole;
import javafx.scene.Node;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.web.WebView;
import lombok.Builder;
import lombok.Getter;
import lombok.SneakyThrows;
import lombok.Value;
import java.util.Optional;
import java.util.Set;
@Getter
public class SvgView {
private final ObservableValue<Number> width;
private final ObservableValue<Number> height;
private final ObservableValue<String> svgContent;
private static boolean canCreateWebview = true;
private SvgView(ObservableValue<Number> width, ObservableValue<Number> height, ObservableValue<String> svgContent) {
this.width = PlatformThread.sync(width);
this.height = PlatformThread.sync(height);
this.svgContent = PlatformThread.sync(svgContent);
}
@SneakyThrows
public static SvgView create(ObservableValue<String> content) {
var widthProperty = new SimpleIntegerProperty();
var heightProperty = new SimpleIntegerProperty();
content.subscribe(val -> {
if (val == null || val.isBlank()) {
return;
}
var dim = SvgHelper.getDimensions(val);
widthProperty.set((int) Math.ceil(dim.getX()));
heightProperty.set((int) Math.ceil(dim.getY()));
});
return new SvgView(widthProperty, heightProperty, content);
}
private String getHtml(String content) {
return "<html><body style='margin: 0; padding: 0; border: none;' >" + content + "</body></html>";
}
private WebView createWebView() {
if (!canCreateWebview) {
return null;
}
WebView wv;
try {
// This can happen if we are using a custom JavaFX build without webkit
wv = new WebView();
} catch (Throwable t) {
ErrorEvent.fromThrowable(t).omit().expected().handle();
canCreateWebview = false;
return null;
}
wv.getStyleClass().add("svg-comp");
wv.getEngine()
.setUserDataDirectory(
AppProperties.get().getDataDir().resolve("webview").toFile());
// Sometimes a web view might not render when the background is set to transparent, at least according to stack
// overflow
wv.setPageFill(Color.valueOf("#00000001"));
// wv.setPageFill(Color.BLACK);
wv.getEngine().setJavaScriptEnabled(false);
wv.setContextMenuEnabled(false);
wv.setFocusTraversable(false);
wv.setAccessibleRole(AccessibleRole.IMAGE_VIEW);
wv.setDisable(true);
wv.getEngine().loadContent(svgContent.getValue() != null ? getHtml(svgContent.getValue()) : null);
svgContent.subscribe(n -> {
if (n == null) {
wv.setOpacity(0.0);
return;
}
wv.setOpacity(1.0);
var html = getHtml(n);
wv.getEngine().loadContent(html);
});
// Hide scrollbars that popup on every content change. Bug in WebView?
wv.getChildrenUnmodifiable().addListener((ListChangeListener<Node>) change -> {
Set<Node> scrolls = wv.lookupAll(".scroll-bar");
for (Node scroll : scrolls) {
scroll.setFocusTraversable(false);
scroll.setVisible(false);
scroll.setManaged(false);
}
});
// As the aspect ratio of the WebView is kept constant, we can compute the zoom only using the width
wv.zoomProperty()
.bind(Bindings.createDoubleBinding(
() -> {
return wv.getWidth() / width.getValue().doubleValue();
},
wv.widthProperty(),
width));
wv.maxWidthProperty().bind(wv.prefWidthProperty());
wv.maxHeightProperty().bind(wv.prefHeightProperty());
wv.minWidthProperty().bind(wv.prefWidthProperty());
wv.minHeightProperty().bind(wv.prefHeightProperty());
return wv;
}
public Optional<WebView> createWebview() {
var wv = createWebView();
return Optional.ofNullable(wv);
}
@Value
@Builder
public static class Structure implements CompStructure<StackPane> {
StackPane pane;
WebView webView;
@Override
public StackPane get() {
return pane;
}
}
}
@@ -2,7 +2,7 @@ package io.xpipe.app.comp.base;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.util.PlatformThread;
@@ -68,7 +68,7 @@ public class TileButtonComp extends Comp<TileButtonComp.Structure> {
header.textProperty().bind(PlatformThread.sync(name));
var desc = new Label();
desc.textProperty().bind(PlatformThread.sync(description));
AppFont.small(desc);
AppFontSizes.xs(desc);
desc.setOpacity(0.8);
var text = new VBox(header, desc);
text.setSpacing(2);
@@ -1,44 +0,0 @@
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 javafx.beans.value.ObservableValue;
import javafx.scene.control.TitledPane;
import java.util.concurrent.atomic.AtomicInteger;
public class TitledPaneComp extends Comp<CompStructure<TitledPane>> {
private final ObservableValue<String> name;
private final Comp<?> content;
private final int height;
public TitledPaneComp(ObservableValue<String> name, Comp<?> content, int height) {
this.name = name;
this.content = content;
this.height = height;
}
@Override
public CompStructure<TitledPane> createBase() {
var tp = new TitledPane(null, content.createRegion());
tp.textProperty().bind(name);
tp.getStyleClass().add("titled-pane-comp");
tp.setExpanded(false);
tp.setAnimated(false);
AtomicInteger minimizedSize = new AtomicInteger();
tp.expandedProperty().addListener((c, o, n) -> {
if (n) {
if (minimizedSize.get() == 0) {
minimizedSize.set((int) tp.getHeight());
}
tp.setPrefHeight(height);
} else {
tp.setPrefHeight(minimizedSize.get());
}
});
return new SimpleCompStructure<>(tp);
}
}
@@ -2,6 +2,7 @@ package io.xpipe.app.comp.base;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.augment.Augment;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.util.PlatformThread;
@@ -41,7 +42,7 @@ public class TooltipAugment<S extends CompStructure<?>> implements Augment<S> {
} else {
tt.textProperty().bind(PlatformThread.sync(text));
}
tt.setStyle("-fx-font-size: 11pt;");
AppFontSizes.base(tt.getStyleableNode());
tt.setWrapText(true);
tt.setMaxWidth(400);
tt.getStyleClass().add("fancy-tooltip");
@@ -1,76 +0,0 @@
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.util.PlatformThread;
import javafx.application.Platform;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
public class VBoxViewComp<T> extends Comp<CompStructure<VBox>> {
private final ObservableList<T> shown;
private final ObservableList<T> all;
private final Function<T, Comp<?>> compFunction;
public VBoxViewComp(ObservableList<T> shown, ObservableList<T> all, Function<T, Comp<?>> compFunction) {
this.shown = PlatformThread.sync(shown);
this.all = PlatformThread.sync(all);
this.compFunction = compFunction;
}
@Override
public CompStructure<VBox> createBase() {
Map<T, Region> cache = new HashMap<>();
VBox vbox = new VBox();
vbox.setFocusTraversable(false);
refresh(vbox, shown, cache, false);
vbox.requestLayout();
shown.addListener((ListChangeListener<? super T>) (c) -> {
refresh(vbox, c.getList(), cache, true);
});
all.addListener((ListChangeListener<? super T>) c -> {
cache.keySet().retainAll(c.getList());
});
return new SimpleCompStructure<>(vbox);
}
private void refresh(VBox listView, List<? extends T> c, Map<T, Region> cache, boolean asynchronous) {
Runnable update = () -> {
var newShown = c.stream()
.map(v -> {
if (!cache.containsKey(v)) {
cache.put(v, compFunction.apply(v).createRegion());
}
return cache.get(v);
})
.toList();
if (!listView.getChildren().equals(newShown)) {
listView.getChildren().setAll(newShown);
listView.layout();
}
};
if (asynchronous) {
Platform.runLater(update);
} else {
PlatformThread.runLaterIfNeeded(update);
}
}
}
@@ -13,11 +13,8 @@ import javafx.scene.layout.*;
public class DenseStoreEntryComp extends StoreEntryComp {
private final boolean showIcon;
public DenseStoreEntryComp(StoreSection section, boolean showIcon, Comp<?> content) {
public DenseStoreEntryComp(StoreSection section, Comp<?> content) {
super(section, content);
this.showIcon = showIcon;
}
private Label createInformation(GridPane grid) {
@@ -74,12 +71,10 @@ public class DenseStoreEntryComp extends StoreEntryComp {
var notes = new StoreNotesComp(getWrapper()).createRegion();
var userIcon = createUserIcon().createRegion();
if (showIcon) {
var storeIcon = createIcon(28, 24);
grid.getColumnConstraints().add(new ColumnConstraints(38));
grid.add(storeIcon, 0, 0);
GridPane.setHalignment(storeIcon, HPos.CENTER);
}
var storeIcon = createIcon(28, 24);
GridPane.setHalignment(storeIcon, HPos.CENTER);
grid.add(storeIcon, 0, 0);
grid.getColumnConstraints().add(new ColumnConstraints(34));
var customSize = content != null ? 100 : 0;
var custom = new ColumnConstraints(0, customSize, customSize);
@@ -4,7 +4,6 @@ import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.base.PrettyImageHelper;
import io.xpipe.app.comp.base.StackComp;
import io.xpipe.app.resources.AppResources;
import io.xpipe.app.util.BindingsHelper;
import io.xpipe.core.process.SystemState;
import io.xpipe.core.store.FileNames;
@@ -51,7 +50,11 @@ public class OsLogoComp extends SimpleComp {
},
wrapper.getPersistentState(),
state);
var hide = BindingsHelper.map(img, s -> s != null);
var hide = Bindings.createBooleanBinding(
() -> {
return img.get() != null;
},
img);
return new StackComp(List.of(
new SystemStateComp(state).hide(hide),
PrettyImageHelper.ofFixedSize(img, 24, 24).visible(hide)))
@@ -1,7 +1,7 @@
package io.xpipe.app.comp.store;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.core.process.OsType;
import javafx.geometry.HPos;
@@ -25,7 +25,7 @@ public class StandardStoreEntryComp extends StoreEntryComp {
var summary = new Label();
summary.textProperty().bind(getWrapper().getShownSummary());
summary.getStyleClass().add("summary");
AppFont.small(summary);
AppFontSizes.xs(summary);
return summary;
}
@@ -40,7 +40,7 @@ public class StandardStoreEntryComp extends StoreEntryComp {
var storeIcon = createIcon(46, 40);
grid.add(storeIcon, 0, 0, 1, 2);
grid.getColumnConstraints().add(new ColumnConstraints(56));
grid.getColumnConstraints().add(new ColumnConstraints(52));
var active = new StoreActiveComp(getWrapper()).createRegion();
var nameBox = new HBox(name, userIcon, notes);
@@ -4,7 +4,7 @@ import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.augment.ContextMenuAugment;
import io.xpipe.app.comp.base.*;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataColor;
@@ -12,11 +12,11 @@ import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreCategory;
import io.xpipe.app.util.ClipboardHelper;
import io.xpipe.app.util.ContextMenuHelper;
import io.xpipe.app.util.DerivedObservableList;
import io.xpipe.app.util.LabelGraphic;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
@@ -29,6 +29,7 @@ import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import lombok.EqualsAndHashCode;
@@ -50,7 +51,8 @@ public class StoreCategoryComp extends SimpleComp {
@Override
protected Region createSimple() {
var prop = new SimpleStringProperty(category.getName().getValue());
var prop = new SimpleStringProperty();
category.getName().subscribe(prop::setValue);
AppPrefs.get().censorMode().subscribe(aBoolean -> {
var n = category.getName().getValue();
prop.setValue(aBoolean ? "*".repeat(n.length()) : n);
@@ -67,18 +69,17 @@ public class StoreCategoryComp extends SimpleComp {
() -> {
var exp = category.getExpanded().get()
&& category.getChildren().getList().size() > 0;
return new LabelGraphic.IconGraphic(exp ? "mdal-keyboard_arrow_down" : "mdal-keyboard_arrow_right");
return new LabelGraphic.IconGraphic(exp ? "mdi2m-menu-down-outline" : "mdi2m-menu-right-outline");
},
category.getExpanded(),
category.getChildren().getList());
var expandButton = new IconButtonComp(expandIcon, () -> {
category.toggleExpanded();
})
.apply(struc -> AppFont.medium(struc.get()))
.apply(struc -> {
struc.get().setAlignment(Pos.CENTER);
struc.get().setPadding(new Insets(-2, 0, 0, 0));
struc.get().setFocusTraversable(false);
HBox.setMargin(struc.get(), new Insets(0, 0, 2.6, 0));
})
.disable(Bindings.isEmpty(category.getChildren().getList()))
.styleClass("expand-button")
@@ -92,10 +93,7 @@ public class StoreCategoryComp extends SimpleComp {
}
if (!DataStorage.get().supportsSharing()
|| (!category.getCategory().canShare()
&& !category.getCategory()
.getUuid()
.equals(DataStorage.LOCAL_IDENTITIES_CATEGORY_UUID))) {
|| (!category.getCategory().canShare())) {
return new LabelGraphic.IconGraphic("mdi2g-git");
}
@@ -104,7 +102,7 @@ public class StoreCategoryComp extends SimpleComp {
category.getSync(),
hover);
var statusButton = new IconButtonComp(statusIcon)
.apply(struc -> AppFont.small(struc.get()))
.apply(struc -> AppFontSizes.xs(struc.get()))
.apply(struc -> {
struc.get().setAlignment(Pos.CENTER);
struc.get().setPadding(new Insets(0, 0, 0, 0));
@@ -119,25 +117,18 @@ public class StoreCategoryComp extends SimpleComp {
}))
.styleClass("status-button");
var shownList = new DerivedObservableList<>(
category.getAllContainedEntries().getList(), true)
.filtered(
storeEntryWrapper -> {
return storeEntryWrapper.matchesFilter(
StoreViewState.get().getFilterString().getValue());
},
StoreViewState.get().getFilterString())
.getList();
var count =
new CountComp<>(shownList, category.getAllContainedEntries().getList(), string -> "(" + string + ")");
count.visible(Bindings.isNotEmpty(shownList));
var count = new CountComp(
category.getShownContainedEntriesCount(),
category.getAllContainedEntriesCount(),
string -> "(" + string + ")");
count.visible(Bindings.notEqual(0, category.getShownContainedEntriesCount()));
var showStatus = hover.or(new SimpleBooleanProperty(DataStorage.get().supportsSharing()))
.or(showing);
var focus = new SimpleBooleanProperty();
var h = new HorizontalComp(List.of(
expandButton,
Comp.hspacer(1),
Comp.hspacer(category.getCategory().getParentCategory() == null ? 3 : 0),
Comp.of(() -> name).hgrow(),
Comp.hspacer(2),
count,
@@ -145,7 +136,8 @@ public class StoreCategoryComp extends SimpleComp {
statusButton.hide(showStatus.not())));
h.padding(new Insets(0, 10, 0, (category.getDepth() * 10)));
var categoryButton = new ButtonComp(null, h.createRegion(), category::select)
var categoryButton = new ButtonComp(
null, new SimpleObjectProperty<>(new LabelGraphic.CompGraphic(h)), category::select)
.focusTraversable()
.styleClass("category-button")
.apply(struc -> hover.bind(struc.get().hoverProperty()))
@@ -172,6 +164,7 @@ public class StoreCategoryComp extends SimpleComp {
var children =
new ListBoxViewComp<>(l, l, storeCategoryWrapper -> new StoreCategoryComp(storeCategoryWrapper), false);
children.styleClass("children");
children.minHeight(0);
var hide = Bindings.createBooleanBinding(
() -> {
@@ -197,7 +190,6 @@ public class StoreCategoryComp extends SimpleComp {
private ContextMenu createContextMenu(Region text) {
var contextMenu = ContextMenuHelper.create();
AppFont.normal(contextMenu.getStyleableNode());
if (AppPrefs.get().enableHttpApi().get()) {
var copyId = new MenuItem(AppI18n.get("copyId"), new FontIcon("mdi2c-content-copy"));
@@ -2,6 +2,8 @@ package io.xpipe.app.comp.store;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.base.ScrollComp;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.core.process.OsType;
import javafx.beans.binding.Bindings;
import javafx.scene.layout.Region;
@@ -20,12 +22,16 @@ public class StoreCategoryListComp extends SimpleComp {
sp.styleClass("store-category-bar");
sp.apply(struc -> {
Region content = (Region) struc.get().getContent();
struc.get().setFitToWidth(true);
if (OsType.getLocal() == OsType.MACOS) {
AppFontSizes.lg(struc.get());
}
struc.get()
.minHeightProperty()
.bind(Bindings.createDoubleBinding(
() -> {
var h = content.getHeight();
return Math.min(200, h + 2);
return Math.min(150, h + 2);
},
content.heightProperty()));
});
@@ -32,9 +32,11 @@ public class StoreCategoryWrapper {
private final Property<Boolean> sync;
private final DerivedObservableList<StoreCategoryWrapper> children;
private final DerivedObservableList<StoreEntryWrapper> directContainedEntries;
private final DerivedObservableList<StoreEntryWrapper> allContainedEntries;
private final IntegerProperty shownContainedEntriesCount = new SimpleIntegerProperty();
private final IntegerProperty allContainedEntriesCount = new SimpleIntegerProperty();
private final BooleanProperty expanded = new SimpleBooleanProperty();
private final Property<DataColor> color = new SimpleObjectProperty<>();
private final BooleanProperty largeCategoryOptimizations = new SimpleBooleanProperty();
private StoreCategoryWrapper cachedParent;
public StoreCategoryWrapper(DataStoreCategory category) {
@@ -57,7 +59,6 @@ public class StoreCategoryWrapper {
this.sortMode = new SimpleObjectProperty<>(category.getSortMode());
this.sync = new SimpleObjectProperty<>(category.isSync());
this.children = new DerivedObservableList<>(FXCollections.observableArrayList(), true);
this.allContainedEntries = new DerivedObservableList<>(FXCollections.observableArrayList(), true);
this.directContainedEntries = new DerivedObservableList<>(FXCollections.observableArrayList(), true);
this.color.setValue(category.getColor());
setupListeners();
@@ -136,7 +137,7 @@ public class StoreCategoryWrapper {
update();
});
AppPrefs.get().language().addListener((observable, oldValue, newValue) -> {
AppI18n.activeLanguage().addListener((observable, oldValue, newValue) -> {
update();
});
@@ -177,22 +178,32 @@ public class StoreCategoryWrapper {
return entry.getEntry().getCategoryUuid().equals(category.getUuid());
})
.toList());
allContainedEntries.setContent(allEntries.stream()
.filter(entry -> {
return entry.getEntry().getCategoryUuid().equals(category.getUuid())
|| (AppPrefs.get()
.showChildCategoriesInParentCategory()
.get()
&& children.getList().stream()
.anyMatch(storeCategoryWrapper -> storeCategoryWrapper.contains(entry)));
})
.toList());
children.setContent(StoreViewState.get().getCategories().getList().stream()
.filter(storeCategoryWrapper -> getCategory()
.getUuid()
.equals(storeCategoryWrapper.getCategory().getParentCategory()))
.toList());
var direct = directContainedEntries.getList().size();
var sub = children.getList().stream()
.mapToInt(value -> value.allContainedEntriesCount.get())
.sum();
allContainedEntriesCount.setValue(direct + sub);
var performanceCount =
AppPrefs.get().showChildCategoriesInParentCategory().get() ? allContainedEntriesCount.get() : direct;
if (performanceCount > 500) {
largeCategoryOptimizations.setValue(true);
}
var directFiltered = directContainedEntries.getList().stream()
.filter(storeEntryWrapper -> storeEntryWrapper.matchesFilter(
StoreViewState.get().getFilterString().getValue()))
.count();
var subFiltered = children.getList().stream()
.mapToInt(value -> value.shownContainedEntriesCount.get())
.sum();
shownContainedEntriesCount.setValue(directFiltered + subFiltered);
Optional.ofNullable(getParent()).ifPresent(storeCategoryWrapper -> {
storeCategoryWrapper.update();
});
@@ -6,7 +6,7 @@ import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.comp.base.FilterComp;
import io.xpipe.app.comp.base.HorizontalComp;
import io.xpipe.app.comp.base.PrettyImageHelper;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.LocalStore;
import io.xpipe.app.ext.ShellStore;
@@ -170,7 +170,7 @@ public class StoreChoiceComp<T extends DataStore> extends SimpleComp {
popover.setHeaderAlwaysVisible(true);
popover.setDetachable(true);
popover.setTitle(AppI18n.get("selectConnection"));
AppFont.small(popover.getContentNode());
AppFontSizes.xs(popover.getContentNode());
}
return popover;
@@ -231,7 +231,7 @@ public class StoreChoiceComp<T extends DataStore> extends SimpleComp {
var icon = new FontIcon("mdal-keyboard_arrow_down");
icon.setDisable(true);
icon.setPickOnBounds(false);
AppFont.header(icon);
AppFontSizes.xl(icon);
var pane = new StackPane(r, icon);
pane.focusedProperty().addListener((observable, oldValue, newValue) -> {
if (newValue) {
@@ -23,7 +23,6 @@ import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.scene.control.*;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Region;
@@ -32,7 +31,6 @@ import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import atlantafx.base.controls.Spacer;
import lombok.AccessLevel;
import lombok.experimental.FieldDefaults;
import net.synedra.validatorfx.GraphicDecorationStackPane;
@@ -448,10 +446,18 @@ public class StoreCreationComp extends DialogComp {
}
private Region createStoreProperties(Comp<?> comp, Validator propVal) {
var p = provider.getValue();
var nameKey = p == null
|| p.getCreationCategory() == null
|| p.getCreationCategory().getCategory().equals(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID)
? "connection"
: p.getCreationCategory().getCategory().equals(DataStorage.ALL_SCRIPTS_CATEGORY_UUID)
? "script"
: "identity";
return new OptionsBuilder()
.addComp(comp, store)
.name("connectionName")
.description("connectionNameDescription")
.name(nameKey + "Name")
.description(nameKey + "NameDescription")
.addString(name, false)
.nonNull(propVal)
.buildComp()
@@ -514,7 +520,7 @@ public class StoreCreationComp extends DialogComp {
var sep = new Separator();
sep.getStyleClass().add("spacer");
var top = new VBox(providerChoice.createRegion(), new Spacer(5, Orientation.VERTICAL), sep);
var top = new VBox(providerChoice.createRegion(), sep);
top.getStyleClass().add("top");
if (showProviders) {
layout.setTop(top);
@@ -69,7 +69,7 @@ public class StoreCreationMenu {
item.setOnAction(event -> {
StoreCreationComp.showCreation(
defaultProvider != null
? DataStoreProviders.byName(defaultProvider).orElseThrow()
? DataStoreProviders.byId(defaultProvider).orElseThrow()
: null,
category);
event.consume();
@@ -87,7 +87,7 @@ public class StoreCreationMenu {
StoreCreationComp.showCreation(
defaultProvider != null
? DataStoreProviders.byName(defaultProvider).orElseThrow()
? DataStoreProviders.byId(defaultProvider).orElseThrow()
: null,
category);
event.consume();
@@ -9,18 +9,15 @@ import io.xpipe.app.comp.base.IconButtonComp;
import io.xpipe.app.comp.base.LabelComp;
import io.xpipe.app.comp.base.LoadingOverlayComp;
import io.xpipe.app.comp.base.TooltipAugment;
import io.xpipe.app.core.App;
import io.xpipe.app.core.AppActionLinkDetector;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.*;
import io.xpipe.app.ext.ActionProvider;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.resources.AppResources;
import io.xpipe.app.storage.DataColor;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.update.XPipeDistributionType;
import io.xpipe.app.util.*;
import io.xpipe.core.process.OsType;
import javafx.beans.binding.Bindings;
import javafx.beans.value.ObservableDoubleValue;
@@ -65,7 +62,7 @@ public abstract class StoreEntryComp extends SimpleComp {
var forceCondensed = AppPrefs.get() != null
&& AppPrefs.get().condenseConnectionDisplay().get();
if (!preferLarge || forceCondensed) {
return new DenseStoreEntryComp(section, true, content);
return new DenseStoreEntryComp(section, content);
} else {
return new StandardStoreEntryComp(section, content);
}
@@ -78,7 +75,7 @@ public abstract class StoreEntryComp extends SimpleComp {
} else {
var forceCondensed = AppPrefs.get() != null
&& AppPrefs.get().condenseConnectionDisplay().get();
return forceCondensed ? new DenseStoreEntryComp(e, true, null) : new StandardStoreEntryComp(e, null);
return forceCondensed ? new DenseStoreEntryComp(e, null) : new StandardStoreEntryComp(e, null);
}
}
@@ -138,7 +135,19 @@ public abstract class StoreEntryComp extends SimpleComp {
var loading = LoadingOverlayComp.noProgress(
Comp.of(() -> button), getWrapper().getEffectiveBusy());
AppFont.normal(button);
if (OsType.getLocal() == OsType.MACOS) {
AppFontSizes.base(button);
} else if (OsType.getLocal() == OsType.LINUX) {
AppFontSizes.xl(button);
} else {
AppFontSizes.apply(button, sizes -> {
if (sizes.getBase().equals("10.5")) {
return sizes.getXl();
} else {
return sizes.getLg();
}
});
}
return loading.createRegion();
}
@@ -175,7 +184,7 @@ public abstract class StoreEntryComp extends SimpleComp {
button.styleClass("user-icon");
button.tooltipKey("personalConnection");
button.apply(struc -> {
AppFont.medium(struc.get());
AppFontSizes.base(struc.get());
struc.get().setOpacity(1.0);
});
button.hide(Bindings.not(getWrapper().getPerUser()));
@@ -207,7 +216,7 @@ public abstract class StoreEntryComp extends SimpleComp {
update.run();
ig.setAlignment(Pos.CENTER_RIGHT);
ig.getStyleClass().add("button-bar");
AppFont.medium(ig);
AppFontSizes.base(ig);
return ig;
}
@@ -266,8 +275,7 @@ public abstract class StoreEntryComp extends SimpleComp {
}
protected ContextMenu createContextMenu() {
var contextMenu = new ContextMenu();
AppFont.normal(contextMenu.getStyleableNode());
var contextMenu = ContextMenuHelper.create();
var hasSep = false;
for (var p : getWrapper().getActionProviders()) {
@@ -473,7 +481,7 @@ public abstract class StoreEntryComp extends SimpleComp {
});
menu.getItems().add(sc);
if (XPipeDistributionType.get().isSupportsUrls()) {
if (AppDistributionType.get().isSupportsUrls()) {
var l = new MenuItem(null, new FontIcon("mdi2c-clipboard-list-outline"));
l.textProperty().bind(AppI18n.observable("base.copyShareLink"));
l.setOnAction(event -> {
@@ -5,7 +5,7 @@ import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.base.CountComp;
import io.xpipe.app.comp.base.FilterComp;
import io.xpipe.app.comp.base.IconButtonComp;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.util.BindingsHelper;
import io.xpipe.app.util.LabelGraphic;
@@ -28,6 +28,8 @@ import javafx.scene.text.TextAlignment;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.function.Function;
public class StoreEntryListOverviewComp extends SimpleComp {
private final Property<StoreSortMode> sortMode;
@@ -76,7 +78,8 @@ public class StoreEntryListOverviewComp extends SimpleComp {
return inRootCategory;
},
StoreViewState.get().getActiveCategory());
var count = new CountComp<>(all.getList(), all.getList());
var allCount = Bindings.size(all.getList());
var count = new CountComp(allCount, allCount, Function.identity());
var c = count.createRegion();
var topBar = new HBox(
@@ -86,8 +89,13 @@ public class StoreEntryListOverviewComp extends SimpleComp {
createDateSortButton().createRegion(),
Comp.hspacer(2).createRegion(),
createAlphabeticalSortButton().createRegion());
AppFont.setSize(label, 2);
AppFont.setSize(c, 2);
if (OsType.getLocal() == OsType.MACOS) {
AppFontSizes.xxxl(label);
AppFontSizes.xxxl(c);
} else {
AppFontSizes.xxl(label);
AppFontSizes.xxl(c);
}
topBar.setAlignment(Pos.CENTER);
topBar.getStyleClass().add("top");
return topBar;
@@ -112,7 +120,6 @@ public class StoreEntryListOverviewComp extends SimpleComp {
HBox.setHgrow(f, Priority.ALWAYS);
f.getStyleClass().add("filter-bar");
AppFont.medium(hbox);
return hbox;
}
@@ -121,7 +128,6 @@ public class StoreEntryListOverviewComp extends SimpleComp {
menu.textProperty().bind(AppI18n.observable("addConnections"));
menu.setAlignment(Pos.CENTER);
menu.setTextAlignment(TextAlignment.CENTER);
AppFont.medium(menu);
StoreCreationMenu.addButtons(menu);
menu.setOpacity(0.85);
menu.setMinWidth(Region.USE_PREF_SIZE);
@@ -129,7 +135,7 @@ public class StoreEntryListOverviewComp extends SimpleComp {
if (OsType.getLocal().equals(OsType.MACOS)) {
menu.setPadding(new Insets(-2, 0, -2, 0));
} else {
menu.setPadding(new Insets(-4, 0, -4, 0));
menu.setPadding(new Insets(-5, -2, -5, -2));
}
return menu;
@@ -157,7 +163,6 @@ public class StoreEntryListOverviewComp extends SimpleComp {
}
});
alphabetical.apply(alphabeticalR -> {
AppFont.medium(alphabeticalR.get());
alphabeticalR
.get()
.opacityProperty()
@@ -198,7 +203,6 @@ public class StoreEntryListOverviewComp extends SimpleComp {
}
});
date.apply(dateR -> {
AppFont.medium(dateR.get());
dateR.get()
.opacityProperty()
.bind(Bindings.createDoubleBinding(
@@ -15,7 +15,7 @@ import io.xpipe.core.store.SingletonSessionStore;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.value.ObservableStringValue;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import lombok.Getter;
@@ -53,6 +53,10 @@ public class StoreEntryWrapper {
private final Property<DataStore> store = new SimpleObjectProperty<>();
private final Property<String> information = new SimpleStringProperty();
private final BooleanProperty perUser = new SimpleBooleanProperty();
private final ObservableValue<String> shownName;
private final ObservableValue<String> shownSummary;
private final ObservableValue<String> shownInformation;
private final BooleanProperty largeCategoryOptimizations = new SimpleBooleanProperty();
private boolean effectiveBusyProviderBound = false;
private final BooleanProperty effectiveBusy = new SimpleBooleanProperty();
@@ -61,6 +65,27 @@ public class StoreEntryWrapper {
this.entry = entry;
this.name = new SimpleStringProperty(entry.getName());
this.lastAccess = new SimpleObjectProperty<>(entry.getLastAccess().minus(Duration.ofMillis(500)));
this.shownName = Bindings.createStringBinding(
() -> {
var n = name.getValue();
return n != null && AppPrefs.get().censorMode().get() ? "*".repeat(n.length()) : n;
},
AppPrefs.get().censorMode(),
name);
this.shownSummary = Bindings.createStringBinding(
() -> {
var n = summary.getValue();
return n != null && AppPrefs.get().censorMode().get() ? "*".repeat(n.length()) : n;
},
AppPrefs.get().censorMode(),
summary);
this.shownInformation = Bindings.createStringBinding(
() -> {
var n = information.getValue();
return n != null && AppPrefs.get().censorMode().get() ? "*".repeat(n.length()) : n;
},
AppPrefs.get().censorMode(),
information);
ActionProvider.ALL_STANDALONE.stream()
.filter(dataStoreActionProvider -> {
return !entry.isDisabled()
@@ -155,34 +180,27 @@ public class StoreEntryWrapper {
this.effectiveBusy.bind(busy);
}
var storeChanged = store.getValue() != entry.getStore();
store.setValue(entry.getStore());
if (storeChanged || !information.isBound()) {
if (entry.getProvider() != null) {
var section = StoreViewState.get().getSectionForWrapper(this);
if (section.isPresent()) {
information.unbind();
try {
var binding = PlatformThread.sync(entry.getProvider().informationString(section.get()));
information.bind(binding);
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
information.bind(new SimpleStringProperty());
}
}
}
}
lastAccess.setValue(entry.getLastAccess());
disabled.setValue(entry.isDisabled());
validity.setValue(entry.getValidity());
expanded.setValue(entry.isExpanded());
persistentState.setValue(entry.getStorePersistentState());
// The property values are only registered as changed once they are queried
// If we use information bindings that depend on some of these properties
// but use the store methods to retrieve data instead of the wrapper properties,
// the bindings do not get updated as the change events are not fired.
// We can also fire them manually with this
persistentState.getValue();
// Use map copy to recognize update
// This is a synchronized map, so we synchronize the access
synchronized (entry.getStoreCache()) {
cache.setValue(new HashMap<>(entry.getStoreCache()));
if (!entry.getStoreCache().equals(cache.getValue())) {
cache.setValue(new HashMap<>(entry.getStoreCache()));
// Same here
cache.getValue();
}
}
color.setValue(entry.getColor());
notes.setValue(new StoreNotes(entry.getNotes(), entry.getNotes()));
@@ -198,10 +216,30 @@ public class StoreEntryWrapper {
storeCategoryWrapper.getCategory().getUuid().equals(entry.getCategoryUuid()))
.findFirst()
.orElse(StoreViewState.get().getAllConnectionsCategory()));
largeCategoryOptimizations.setValue(
category.getValue().getLargeCategoryOptimizations().getValue());
perUser.setValue(
!category.getValue().getRoot().equals(StoreViewState.get().getAllIdentitiesCategory())
&& entry.isPerUserStore());
var storeChanged = store.getValue() != entry.getStore();
store.setValue(entry.getStore());
if (storeChanged || !information.isBound()) {
if (entry.getProvider() != null) {
var section = StoreViewState.get().getSectionForWrapper(this);
if (section.isPresent()) {
information.unbind();
try {
var is = entry.getProvider().informationString(section.get());
information.bind(is);
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
information.bind(new SimpleStringProperty());
}
}
}
}
if (!entry.getValidity().isUsable()) {
summary.setValue(null);
} else {
@@ -323,7 +361,7 @@ public class StoreEntryWrapper {
}
public boolean matchesFilter(String filter) {
if (filter == null || nameProperty().getValue().toLowerCase().contains(filter.toLowerCase())) {
if (filter == null || name.getValue().toLowerCase().contains(filter.toLowerCase())) {
return true;
}
@@ -339,38 +377,4 @@ public class StoreEntryWrapper {
public Property<String> nameProperty() {
return name;
}
public BooleanProperty disabledProperty() {
return disabled;
}
public ObservableStringValue getShownName() {
return Bindings.createStringBinding(
() -> {
var n = nameProperty().getValue();
return AppPrefs.get().censorMode().get() ? "*".repeat(n.length()) : n;
},
AppPrefs.get().censorMode(),
nameProperty());
}
public ObservableStringValue getShownSummary() {
return Bindings.createStringBinding(
() -> {
var n = summary.getValue();
return AppPrefs.get().censorMode().get() ? "*".repeat(n.length()) : n;
},
AppPrefs.get().censorMode(),
summary);
}
public ObservableStringValue getShownInformation() {
return Bindings.createStringBinding(
() -> {
var n = information.getValue();
return AppPrefs.get().censorMode().get() ? "*".repeat(n.length()) : n;
},
AppPrefs.get().censorMode(),
information);
}
}
@@ -2,39 +2,46 @@ package io.xpipe.app.comp.store;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.base.*;
import io.xpipe.app.resources.SystemIcon;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.icon.SystemIcon;
import io.xpipe.app.icon.SystemIconCache;
import io.xpipe.app.icon.SystemIconManager;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.LabelGraphic;
import io.xpipe.app.util.ThreadHelper;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.property.*;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.text.TextAlignment;
import atlantafx.base.theme.Styles;
import atlantafx.base.theme.Tweaks;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.*;
import static atlantafx.base.theme.Styles.TEXT_SMALL;
public class StoreIconChoiceComp extends SimpleComp {
private final Runnable reshow;
private final Property<SystemIcon> selected;
private final List<SystemIcon> icons;
private final Set<SystemIcon> icons;
private final int columns;
private final SimpleStringProperty filter;
private final Runnable doubleClick;
public StoreIconChoiceComp(
Runnable reshow,
Property<SystemIcon> selected,
List<SystemIcon> icons,
Set<SystemIcon> icons,
int columns,
SimpleStringProperty filter,
Runnable doubleClick) {
this.reshow = reshow;
this.selected = selected;
this.icons = icons;
this.columns = columns;
@@ -52,30 +59,59 @@ public class StoreIconChoiceComp extends SimpleComp {
}
private void initTable(TableView<List<SystemIcon>> table) {
for (int i = 0; i < columns; i++) {
var col = new TableColumn<List<SystemIcon>, SystemIcon>("col" + i);
final int colIndex = i;
col.setCellValueFactory(cb -> {
var row = cb.getValue();
var item = row.size() > colIndex ? row.get(colIndex) : null;
return new SimpleObjectProperty<>(item);
});
col.setCellFactory(cb -> new IconCell());
col.getStyleClass().add(Tweaks.ALIGN_CENTER);
table.getColumns().add(col);
if (SystemIconCache.isBuilt()) {
for (int i = 0; i < columns; i++) {
var col = new TableColumn<List<SystemIcon>, SystemIcon>("col" + i);
final int colIndex = i;
col.setCellValueFactory(cb -> {
var row = cb.getValue();
var item = row.size() > colIndex ? row.get(colIndex) : null;
return new SimpleObjectProperty<>(item);
});
col.setCellFactory(cb -> new IconCell());
col.getStyleClass().add(Tweaks.ALIGN_CENTER);
table.getColumns().add(col);
}
}
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_ALL_COLUMNS);
table.getSelectionModel().setCellSelectionEnabled(true);
table.getStyleClass().add("icon-browser");
table.setPlaceholder(new Region());
var busy = new SimpleBooleanProperty(false);
var refreshButton = new ButtonComp(
AppI18n.observable("refreshIcons"),
new SimpleObjectProperty<>(new LabelGraphic.IconGraphic("mdi2r-refresh")),
() -> {
ThreadHelper.runFailableAsync(() -> {
try (var ignored = new BooleanScope(busy).start()) {
SystemIconManager.reload();
}
reshow.run();
});
});
refreshButton.disable(busy);
var text = new LabelComp(AppI18n.observable("refreshIconsDescription"));
text.apply(struc -> {
struc.get().setWrapText(true);
struc.get().setTextAlignment(TextAlignment.CENTER);
struc.get().setPrefWidth(300);
});
text.styleClass(Styles.TEXT_SUBTLE);
text.visible(busy);
var vbox = new VerticalComp(List.of(refreshButton, text)).spacing(25);
vbox.apply(struc -> struc.get().setAlignment(Pos.CENTER));
var placeholder = new StackPane(vbox.createRegion());
table.setPlaceholder(placeholder);
}
private void updateData(TableView<List<SystemIcon>> table, String filterString) {
var displayedIcons = filterString == null || filterString.isBlank() || filterString.length() < 2
? icons
? icons.stream()
.sorted(Comparator.comparing(systemIcon -> systemIcon.getId()))
.toList()
: icons.stream()
.filter(icon -> containsString(icon.getDisplayName(), filterString))
.filter(icon -> containsString(icon.getId(), filterString))
.toList();
var data = partitionList(displayedIcons, columns);
@@ -137,8 +173,8 @@ public class StoreIconChoiceComp extends SimpleComp {
return;
}
root.setText(icon.getDisplayName());
image.set("app:system/" + icon.getIconName() + ".svg");
root.setText(icon.getId());
image.set(SystemIconManager.getIconFile(icon));
setGraphic(root);
}
}
@@ -1,11 +1,12 @@
package io.xpipe.app.comp.store;
import io.xpipe.app.comp.base.*;
import io.xpipe.app.resources.SystemIcon;
import io.xpipe.app.resources.SystemIcons;
import io.xpipe.app.icon.SystemIcon;
import io.xpipe.app.icon.SystemIconManager;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.util.Hyperlinks;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
@@ -35,15 +36,28 @@ public class StoreIconChoiceDialog {
var filterText = new SimpleStringProperty();
var filter = new FilterComp(filterText).grow(true, false);
filter.focusOnShow();
var github = new ButtonComp(null, new FontIcon("mdi2g-github"), () -> {
Hyperlinks.open(Hyperlinks.SELFHST_ICONS);
var github = new ButtonComp(null, new FontIcon("mdomz-settings"), () -> {
overlay.close();
AppPrefs.get().selectCategory("icons");
})
.grow(false, true);
var modal = ModalOverlay.of(
"chooseCustomIcon",
new StoreIconChoiceComp(selected, SystemIcons.getSystemIcons(), 5, filterText, () -> {
finish();
})
new StoreIconChoiceComp(
() -> {
var showing = overlay.isShowing();
overlay.close();
if (showing) {
Platform.runLater(() -> overlay.show());
}
},
selected,
SystemIconManager.getIcons(),
5,
filterText,
() -> {
finish();
})
.prefWidth(600));
modal.addButtonBarComp(github);
modal.addButtonBarComp(filter);
@@ -63,7 +77,12 @@ public class StoreIconChoiceDialog {
}
private void finish() {
entry.setIcon(selected.get() != null ? selected.getValue().getIconName() : null, true);
entry.setIcon(
selected.get() != null
? selected.getValue().getSource().getId() + "/"
+ selected.getValue().getId()
: null,
true);
overlay.close();
}
}
@@ -36,10 +36,10 @@ public class StoreIconComp extends SimpleComp {
dots.setIconSize((int) (h * 1.3));
var stack = new StackPane(background, storeIcon, dots);
stack.setMinHeight(w + 7);
stack.setMinWidth(w + 7);
stack.setMaxHeight(w + 7);
stack.setMaxWidth(w + 7);
stack.setMinHeight(w + 5);
stack.setMinWidth(w + 5);
stack.setMaxHeight(w + 5);
stack.setMaxWidth(w + 5);
stack.getStyleClass().add("icon");
stack.setAlignment(Pos.CENTER);
@@ -1,7 +1,7 @@
package io.xpipe.app.comp.store;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.DataStoreCreationCategory;
import io.xpipe.app.ext.DataStoreProviders;
@@ -29,7 +29,7 @@ public class StoreIdentitiesIntroComp extends SimpleComp {
if (OsType.getLocal() != OsType.MACOS) {
title.getStyleClass().add(Styles.TEXT_BOLD);
}
AppFont.setSize(title, 7);
AppFontSizes.title(title);
var introDesc = new Label();
introDesc.textProperty().bind(AppI18n.observable("identitiesIntroText"));
@@ -50,8 +50,8 @@ public class StoreIdentitiesIntroComp extends SimpleComp {
addButton.setOnAction(event -> {
var canSync = DataStorage.get().supportsSharing();
var prov = canSync
? DataStoreProviders.byName("syncedIdentity").orElseThrow()
: DataStoreProviders.byName("localIdentity").orElseThrow();
? DataStoreProviders.byId("syncedIdentity").orElseThrow()
: DataStoreProviders.byId("localIdentity").orElseThrow();
StoreCreationComp.showCreation(prov, DataStoreCreationCategory.IDENTITY);
event.consume();
});
@@ -76,7 +76,7 @@ public class StoreIdentitiesIntroComp extends SimpleComp {
if (OsType.getLocal() != OsType.MACOS) {
title.getStyleClass().add(Styles.TEXT_BOLD);
}
AppFont.setSize(title, 7);
AppFontSizes.title(title);
var importDesc = new Label();
importDesc.textProperty().bind(AppI18n.observable("identitiesIntroBottomText"));
@@ -2,7 +2,7 @@ package io.xpipe.app.comp.store;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.base.PrettyImageHelper;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
@@ -29,7 +29,7 @@ public class StoreIntroComp extends SimpleComp {
if (OsType.getLocal() != OsType.MACOS) {
title.getStyleClass().add(Styles.TEXT_BOLD);
}
AppFont.setSize(title, 7);
AppFontSizes.title(title);
var introDesc = new Label();
introDesc.textProperty().bind(AppI18n.observable("storeIntroDescription"));
@@ -69,7 +69,7 @@ public class StoreIntroComp extends SimpleComp {
if (OsType.getLocal() != OsType.MACOS) {
title.getStyleClass().add(Styles.TEXT_BOLD);
}
AppFont.setSize(title, 7);
AppFontSizes.title(title);
var importDesc = new Label();
importDesc.textProperty().bind(AppI18n.observable("storeIntroImportDescription"));
@@ -6,7 +6,7 @@ import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.comp.base.DialogComp;
import io.xpipe.app.comp.base.IconButtonComp;
import io.xpipe.app.comp.base.MarkdownEditorComp;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.BindingsHelper;
@@ -35,7 +35,7 @@ public class StoreNotesComp extends Comp<StoreNotesComp.Structure> {
public Structure createBase() {
var n = wrapper.getNotes();
var button = new IconButtonComp("mdi2n-note-text")
.apply(struc -> AppFont.small(struc.get()))
.apply(struc -> AppFontSizes.xs(struc.get()))
.focusTraversableForAccessibility()
.tooltipKey("notes")
.styleClass("notes-button")
@@ -139,7 +139,7 @@ public class StoreNotesComp extends Comp<StoreNotesComp.Structure> {
ref.set(null);
}
});
AppFont.small(popover.getContentNode());
AppFontSizes.xs(popover.getContentNode());
md.getEditButton().addEventFilter(ActionEvent.ANY, event -> {
if (!popover.isDetached()) {
@@ -2,7 +2,7 @@ package io.xpipe.app.comp.store;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.core.AppCache;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.core.AppI18n;
import io.xpipe.core.process.OsType;
@@ -33,7 +33,7 @@ public class StoreScriptsIntroComp extends SimpleComp {
if (OsType.getLocal() != OsType.MACOS) {
title.getStyleClass().add(Styles.TEXT_BOLD);
}
AppFont.setSize(title, 7);
AppFontSizes.title(title);
var introDesc = new Label();
introDesc.textProperty().bind(AppI18n.observable("scriptsIntroText"));
@@ -66,7 +66,7 @@ public class StoreScriptsIntroComp extends SimpleComp {
if (OsType.getLocal() != OsType.MACOS) {
title.getStyleClass().add(Styles.TEXT_BOLD);
}
AppFont.setSize(title, 7);
AppFontSizes.title(title);
var importDesc = new Label();
importDesc.textProperty().bind(AppI18n.observable("scriptsIntroBottomText"));
@@ -14,7 +14,7 @@ import javafx.beans.value.ObservableIntegerValue;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import lombok.Value;
import lombok.Getter;
import java.util.ArrayList;
import java.util.Comparator;
@@ -22,14 +22,14 @@ import java.util.List;
import java.util.function.Predicate;
import java.util.function.ToIntFunction;
@Value
@Getter
public class StoreSection {
StoreEntryWrapper wrapper;
DerivedObservableList<StoreSection> allChildren;
DerivedObservableList<StoreSection> shownChildren;
int depth;
ObservableBooleanValue showDetails;
private final StoreEntryWrapper wrapper;
private final DerivedObservableList<StoreSection> allChildren;
private final DerivedObservableList<StoreSection> shownChildren;
private final int depth;
private final ObservableBooleanValue showDetails;
public StoreSection(
StoreEntryWrapper wrapper,
@@ -85,9 +85,8 @@ public class StoreSection {
}
});
var comp = explicitOrderComp;
var mappedSortMode = BindingsHelper.flatMap(
category,
storeCategoryWrapper -> storeCategoryWrapper.getSortMode());
var mappedSortMode =
BindingsHelper.flatMap(category, storeCategoryWrapper -> storeCategoryWrapper.getSortMode());
return list.sorted(
(o1, o2) -> {
var r = comp.compare(o1, o2);
@@ -114,7 +113,8 @@ public class StoreSection {
ObservableIntegerValue updateObservable) {
var topLevel = all.filtered(
section -> {
return DataStorage.get().isRootEntry(section.getEntry(), category.getValue().getCategory());
return DataStorage.get()
.isRootEntry(section.getEntry(), category.getValue().getCategory());
},
category,
updateObservable);
@@ -163,7 +163,8 @@ public class StoreSection {
// .orElse(false);
// is children. This check is fast as the children are cached in the storage
if (!DataStorage.get().getStoreChildren(e.getEntry()).contains(other.getEntry())) {
if (DataStorage.get() == null
|| !DataStorage.get().getStoreChildren(e.getEntry()).contains(other.getEntry())) {
return false;
}
@@ -200,7 +201,10 @@ public class StoreSection {
// If this entry is already shown as root due to a different category than parent, don't
// show it
// again here
!DataStorage.get().isRootEntry(section.getWrapper().getEntry(), category.getValue().getCategory());
!DataStorage.get()
.isRootEntry(
section.getWrapper().getEntry(),
category.getValue().getCategory());
},
category,
filterString,
@@ -56,7 +56,7 @@ public class StoreViewState {
INSTANCE.updateContent();
INSTANCE.initSections();
INSTANCE.updateContent();
INSTANCE.initFilterJump();
INSTANCE.initFilterListener();
}
public static void reset() {
@@ -96,9 +96,10 @@ public class StoreViewState {
}
}
private void initFilterJump() {
private void initFilterListener() {
var all = getAllConnectionsCategory();
filter.addListener((observable, oldValue, newValue) -> {
categories.getList().forEach(e -> e.update());
var matchingCats = categories.getList().stream()
.filter(storeCategoryWrapper ->
storeCategoryWrapper.getRoot().equals(all))
@@ -175,12 +176,13 @@ public class StoreViewState {
@Override
public void onStoreAdd(DataStoreEntry... entry) {
var l = Arrays.stream(entry)
.map(StoreEntryWrapper::new)
.peek(storeEntryWrapper -> storeEntryWrapper.update())
.peek(wrapper -> wrapper.applyLastAccess())
.toList();
Platform.runLater(() -> {
var l = Arrays.stream(entry)
.map(StoreEntryWrapper::new)
.peek(storeEntryWrapper -> storeEntryWrapper.update())
.peek(wrapper -> wrapper.applyLastAccess())
.toList();
// Don't update anything if we have already reset
if (INSTANCE == null) {
return;
@@ -0,0 +1,198 @@
package io.xpipe.app.core;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.update.GitHubUpdater;
import io.xpipe.app.update.PortableUpdater;
import io.xpipe.app.update.UpdateHandler;
import io.xpipe.app.util.LocalExec;
import io.xpipe.app.util.Translatable;
import io.xpipe.core.process.OsType;
import io.xpipe.core.util.ModuleHelper;
import io.xpipe.core.util.XPipeInstallation;
import javafx.beans.value.ObservableValue;
import lombok.Getter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.function.Supplier;
public enum AppDistributionType implements Translatable {
UNKNOWN("unknown", false, () -> new GitHubUpdater(false)),
DEVELOPMENT("development", true, () -> new GitHubUpdater(false)),
PORTABLE("portable", false, () -> new PortableUpdater(true)),
NATIVE_INSTALLATION("install", true, () -> new GitHubUpdater(true)),
HOMEBREW("homebrew", true, () -> new PortableUpdater(true)),
APT_REPO("apt", true, () -> new PortableUpdater(true)),
RPM_REPO("rpm", true, () -> new PortableUpdater(true)),
WEBTOP("webtop", true, () -> new PortableUpdater(false)),
CHOCO("choco", true, () -> new PortableUpdater(true));
private static AppDistributionType type;
@Getter
private final String id;
@Getter
private final boolean supportsUrls;
private final Supplier<UpdateHandler> updateHandlerSupplier;
private UpdateHandler updateHandler;
AppDistributionType(String id, boolean supportsUrls, Supplier<UpdateHandler> updateHandlerSupplier) {
this.id = id;
this.supportsUrls = supportsUrls;
this.updateHandlerSupplier = updateHandlerSupplier;
}
public static void init() {
if (type != null) {
return;
}
if (!ModuleHelper.isImage()) {
type = DEVELOPMENT;
return;
}
if (!AppProperties.get().isNewBuildSession() && !isDifferentDaemonExecutable()) {
var cached = AppCache.getNonNull("dist", String.class, () -> null);
var cachedType = Arrays.stream(values())
.filter(xPipeDistributionType ->
xPipeDistributionType.getId().equals(cached))
.findAny()
.orElse(null);
if (cachedType != null) {
type = cachedType;
return;
}
}
var det = determine();
// Don't cache unknown type
if (det == UNKNOWN) {
return;
}
type = det;
AppCache.update("dist", type.getId());
TrackEvent.withInfo("Determined distribution type")
.tag("type", type.getId())
.handle();
}
private static boolean isDifferentDaemonExecutable() {
var cached = AppCache.getNonNull("daemonExecutable", String.class, () -> null);
var current = XPipeInstallation.getCurrentInstallationBasePath()
.resolve(XPipeInstallation.getDaemonExecutablePath(OsType.getLocal()))
.toString();
if (current.equals(cached)) {
return false;
}
AppCache.update("daemonExecutable", current);
return true;
}
public static AppDistributionType get() {
if (type == null) {
TrackEvent.withWarn("Distribution type requested before init").handle();
return UNKNOWN;
}
return type;
}
public static AppDistributionType determine() {
var base = XPipeInstallation.getCurrentInstallationBasePath();
if (OsType.getLocal().equals(OsType.MACOS)) {
if (!base.toString().equals(XPipeInstallation.getLocalDefaultInstallationBasePath())) {
return PORTABLE;
}
try {
var r = LocalExec.readStdoutIfPossible(
"pkgutil",
"--pkg-info",
AppProperties.get().isStaging() ? "io.xpipe.xpipe-ptb" : "io.xpipe.xpipe");
if (r.isEmpty()) {
return PORTABLE;
}
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).omit().handle();
return PORTABLE;
}
} else {
var file = base.resolve("installation");
if (!Files.exists(file)) {
return PORTABLE;
}
}
if (OsType.getLocal() == OsType.LINUX && Files.isDirectory(Path.of("/kclient"))) {
return WEBTOP;
}
if (OsType.getLocal().equals(OsType.WINDOWS)) {
var out = LocalExec.readStdoutIfPossible("choco", "list", "xpipe");
if (out.isPresent()) {
if (out.get().contains("xpipe")) {
return CHOCO;
}
}
}
// In theory, we can also add && !AppProperties.get().isStaging() here, but we want to replicate the
// production behavior
if (OsType.getLocal().equals(OsType.MACOS)) {
var out = LocalExec.readStdoutIfPossible("/opt/homebrew/bin/brew", "list", "--casks", "--versions");
if (out.isPresent()) {
if (out.get().lines().anyMatch(s -> {
var split = s.split(" ");
return split.length == 2
&& split[0].equals("xpipe")
&& split[1].equals(AppProperties.get().getVersion());
})) {
return HOMEBREW;
}
}
}
if (OsType.getLocal() == OsType.LINUX) {
if (base.startsWith("/opt")) {
var aptOut = LocalExec.readStdoutIfPossible("apt", "show", "xpipe");
if (aptOut.isPresent()) {
var fromRepo = aptOut.get().lines().anyMatch(s -> {
return s.contains("APT-Sources") && s.contains("apt.xpipe.io");
});
if (fromRepo) {
return APT_REPO;
}
}
var yumRepo = LocalExec.readStdoutIfPossible("test", "-f", "/etc/yum.repos.d/xpipe.repo");
if (yumRepo.isPresent()) {
return RPM_REPO;
}
}
}
return AppDistributionType.NATIVE_INSTALLATION;
}
public UpdateHandler getUpdateHandler() {
if (updateHandler == null) {
updateHandler = updateHandlerSupplier.get();
}
return updateHandler;
}
@Override
public ObservableValue<String> toTranslatedString() {
return AppI18n.observable(getId() + "Dist");
}
}
@@ -26,7 +26,6 @@ import java.util.stream.Stream;
public class AppExtensionManager {
private static AppExtensionManager INSTANCE;
private final boolean loadedProviders;
private final List<Extension> loadedExtensions = new ArrayList<>();
private final List<ModuleLayer> leafModuleLayers = new ArrayList<>();
private final List<Path> extensionBaseDirectories = new ArrayList<>();
@@ -35,29 +34,22 @@ public class AppExtensionManager {
@Getter
private ModuleLayer extendedLayer;
public AppExtensionManager(boolean loadedProviders) {
this.loadedProviders = loadedProviders;
}
public static void init(boolean loadProviders) throws Exception {
var load = INSTANCE == null || !INSTANCE.loadedProviders && loadProviders;
if (INSTANCE == null) {
INSTANCE = new AppExtensionManager(loadProviders);
INSTANCE.determineExtensionDirectories();
INSTANCE.loadBaseExtension();
INSTANCE.loadAllExtensions();
public static synchronized void init() throws Exception {
if (INSTANCE != null) {
return;
}
if (load) {
try {
ProcessControlProvider.init(INSTANCE.extendedLayer);
ModuleLayerLoader.loadAll(INSTANCE.extendedLayer, t -> {
ErrorEvent.fromThrowable(t).handle();
});
} catch (Throwable t) {
throw ExtensionException.corrupt("Service provider initialization failed", t);
}
INSTANCE = new AppExtensionManager();
INSTANCE.determineExtensionDirectories();
INSTANCE.loadBaseExtension();
INSTANCE.loadAllExtensions();
try {
ProcessControlProvider.init(INSTANCE.extendedLayer);
ModuleLayerLoader.loadAll(INSTANCE.extendedLayer, t -> {
ErrorEvent.fromThrowable(t).handle();
});
} catch (Throwable t) {
throw ExtensionException.corrupt("Service provider initialization failed", t);
}
}
@@ -4,7 +4,6 @@ import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.resources.AppResources;
import io.xpipe.core.process.OsType;
import javafx.scene.Node;
import javafx.scene.text.Font;
import org.kordamp.ikonli.javafx.FontIcon;
@@ -17,41 +16,14 @@ import java.nio.file.attribute.BasicFileAttributes;
public class AppFont {
public static void header(Node node) {
setSize(node, +1);
}
public static void normal(Node node) {
setSize(node, 0);
}
public static void medium(Node node) {
setSize(node, -1);
}
public static void small(Node node) {
setSize(node, -2);
}
public static void verySmall(Node node) {
setSize(node, -3);
}
public static void setSize(Node node, int off) {
if (node.getStyle().contains("-fx-font-size: ")) {
return;
}
// Somehow the font is bigger on Linux
var baseSize = OsType.getLocal() == OsType.LINUX ? 11 : 12;
node.setStyle(node.getStyle() + "-fx-font-size: " + (baseSize + off) + "pt;");
}
public static void init() {
// Load ikonli fonts
TrackEvent.info("Loading ikonli fonts ...");
new FontIcon("mdi2s-stop");
new FontIcon("mdi2m-magnify");
new FontIcon("mdi2d-database-plus");
new FontIcon("mdi2p-professional-hexagon");
new FontIcon("mdi2c-chevron-double-right");
TrackEvent.info("Loading bundled fonts ...");
AppResources.with(
@@ -0,0 +1,124 @@
package io.xpipe.app.core;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.core.process.OsType;
import javafx.scene.Node;
import lombok.AllArgsConstructor;
import lombok.Value;
import java.util.function.Function;
import java.util.regex.Pattern;
@Value
@AllArgsConstructor
public class AppFontSizes {
private static final Pattern FONT_SIZE_PATTERN = Pattern.compile("-fx-font-size: \\d+(\\.\\d+)?pt;");
public static void xs(Node node) {
apply(node, AppFontSizes::getXs);
}
public static void sm(Node node) {
apply(node, AppFontSizes::getSm);
}
public static void base(Node node) {
apply(node, AppFontSizes::getBase);
}
public static void lg(Node node) {
apply(node, AppFontSizes::getLg);
}
public static void xl(Node node) {
apply(node, AppFontSizes::getXl);
}
public static void xxl(Node node) {
apply(node, AppFontSizes::getXxl);
}
public static void xxxl(Node node) {
apply(node, AppFontSizes::getXxl);
}
public static void title(Node node) {
apply(node, AppFontSizes::getTitle);
}
public static void apply(Node node, Function<AppFontSizes, String> function) {
if (AppPrefs.get() == null) {
setFont(node, function.apply(getDefault()));
return;
}
AppPrefs.get().theme().subscribe((newValue) -> {
var effective = newValue != null ? newValue.getFontSizes() : getDefault();
setFont(node, function.apply(effective));
});
}
private static void setFont(Node node, String fontSize) {
var s = node.getStyle();
var matcher = FONT_SIZE_PATTERN.matcher(s);
s = matcher.replaceAll("");
node.setStyle("-fx-font-size: " + fontSize + "pt;" + s);
}
public static final AppFontSizes DEFAULT = getDefault();
public static final AppFontSizes BASE_10 = ofBase("10");
public static final AppFontSizes BASE_10_5 = ofBase("10.5");
public static final AppFontSizes BASE_11 = ofBase("11");
public static AppFontSizes ofBase(String base) {
if (base.contains(".")) {
var l = Integer.parseInt(base.split("\\.")[0]);
var r = "." + base.split("\\.")[1];
return new AppFontSizes(
(l - 1) + r, (l - 1) + "", base, (l + 1) + "", (l + 1) + r, (l + 2) + r, (l + 3) + r, (l + 7) + r);
} else {
var l = Integer.parseInt(base);
return new AppFontSizes(
(l - 1) + "", (l - 1) + ".5", l + "", l + ".5", l + 1 + "", l + 2 + "", l + 3 + "", l + 7 + "");
}
}
public static AppFontSizes forOs(AppFontSizes windows, AppFontSizes linux, AppFontSizes mac) {
return switch (OsType.getLocal()) {
case OsType.Linux linux1 -> linux;
case OsType.MacOs macOs -> mac;
case OsType.Windows windows1 -> windows;
};
}
public static AppFontSizes getDefault() {
return forOs(AppFontSizes.BASE_10_5, AppFontSizes.BASE_10, AppFontSizes.BASE_11);
}
// -1.0pt
String xs;
// -0.5pt
String sm;
// 0pt
String base;
// +0.5pt
String lg;
// +1.0pt
String xl;
// +2.0pt
String xxl;
// +3.0pt
String xxxl;
// +7.0pt
String title;
}
@@ -25,7 +25,6 @@ public class AppGreetingsDialog {
tp.setExpanded(true);
tp.setText(AppI18n.get("introduction"));
tp.setAlignment(Pos.CENTER_LEFT);
AppFont.normal(tp);
AppResources.with(AppResources.XPIPE_MODULE, "misc/welcome.md", file -> {
var md = Files.readString(file);
@@ -41,7 +40,6 @@ public class AppGreetingsDialog {
tp.setExpanded(false);
tp.setText(AppI18n.get("eula"));
tp.setAlignment(Pos.CENTER_LEFT);
AppFont.normal(tp);
AppResources.with(AppResources.XPIPE_MODULE, "misc/eula.md", file -> {
var md = Files.readString(file);
@@ -84,7 +82,6 @@ public class AppGreetingsDialog {
var label = new Label(AppI18n.get("legalAccept"));
label.setGraphic(cb);
AppFont.medium(label);
label.setPadding(new Insets(20, 0, 10, 0));
label.setOnMouseClicked(event -> accepted.set(!accepted.get()));
label.setGraphicTextGap(10);
+68 -142
View File
@@ -4,36 +4,34 @@ import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.prefs.SupportedLocale;
import io.xpipe.app.util.BindingsHelper;
import io.xpipe.app.util.PlatformState;
import io.xpipe.app.util.PlatformThread;
import io.xpipe.core.util.XPipeInstallation;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import lombok.Value;
import org.apache.commons.io.FilenameUtils;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
public class AppI18n {
private static final Pattern VAR_PATTERN = Pattern.compile("\\$\\w+?\\$");
private static AppI18n INSTANCE;
private final Property<LoadedTranslations> currentLanguage = new SimpleObjectProperty<>();
private LoadedTranslations english;
private final Property<AppI18nData> currentLanguage = new SimpleObjectProperty<>();
private final ObservableValue<SupportedLocale> currentLocale = BindingsHelper.map(
currentLanguage,
appI18nData -> appI18nData != null ? appI18nData.getLocale() : SupportedLocale.getEnglish());
private final Map<String, ObservableValue<String>> observableCache = new HashMap<>();
private AppI18nData english;
public static ObservableValue<SupportedLocale> activeLanguage() {
if (INSTANCE == null) {
return new SimpleObjectProperty<>(SupportedLocale.getEnglish());
}
return INSTANCE.currentLocale;
}
public static void init() throws Exception {
if (INSTANCE == null) {
@@ -47,48 +45,55 @@ public class AppI18n {
}
public static ObservableValue<String> observable(String s, Object... vars) {
return INSTANCE.observableImpl(s, vars);
}
private ObservableValue<String> observableImpl(String s, Object... vars) {
if (s == null) {
return null;
}
var key = INSTANCE.getKey(s);
return Bindings.createStringBinding(
() -> {
return get(key, vars);
},
INSTANCE.currentLanguage);
synchronized (this) {
var key = getKey(s);
// Don't cache vars
if (vars.length > 0) {
var binding = Bindings.createStringBinding(
() -> {
return getLocalised(key, vars);
},
currentLanguage);
return binding;
}
var found = observableCache.get(key);
if (found != null) {
return found;
}
var binding = Bindings.createStringBinding(
() -> {
return getLocalised(key, vars);
},
currentLanguage);
observableCache.put(key, binding);
return binding;
}
}
public static String get(String s, Object... vars) {
return INSTANCE.getLocalised(s, vars);
}
private static String getValue(String s, Object... vars) {
Objects.requireNonNull(s);
s = s.replace("\\n", "\n");
for (var v : vars) {
v = v != null ? v : "null";
var matcher = VAR_PATTERN.matcher(s);
if (matcher.find()) {
var group = matcher.group();
s = s.replace(group, v.toString());
} else {
TrackEvent.warn("No match found for value " + v + " in string " + s);
}
}
return s;
}
private void load() throws Exception {
if (english == null) {
english = load(Locale.ENGLISH);
english = AppI18nData.load(SupportedLocale.getEnglish());
Locale.setDefault(Locale.ENGLISH);
// Load bundled JDK locale resources
SupportedLocale.ALL.forEach(supportedLocale -> {
supportedLocale.getLocale().getDisplayName();
});
// Load bundled JDK locale resources by initializing the classes
for (var value : SupportedLocale.values()) {
value.getLocale().getDisplayName();
}
}
if (currentLanguage.getValue() == null && PlatformState.getCurrent() == PlatformState.RUNNING) {
@@ -97,8 +102,11 @@ public class AppI18n {
PlatformThread.runLaterIfNeededBlocking(() -> {
AppPrefs.get().language().subscribe(n -> {
try {
currentLanguage.setValue(n != null ? load(n.getLocale()) : null);
Locale.setDefault(n != null ? n.getLocale() : Locale.ENGLISH);
var newValue = n != null ? AppI18nData.load(n) : null;
PlatformThread.runLaterIfNeeded(() -> {
currentLanguage.setValue(newValue);
Locale.setDefault(n != null ? n.getLocale() : Locale.ENGLISH);
});
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
}
@@ -108,11 +116,7 @@ public class AppI18n {
}
}
public LoadedTranslations getLoaded() {
return currentLanguage.getValue() != null ? currentLanguage.getValue() : english;
}
public String getKey(String s) {
private String getKey(String s) {
var key = s;
if (s.startsWith("app.")
|| s.startsWith("base.")
@@ -124,35 +128,30 @@ public class AppI18n {
return key;
}
public String getLocalised(String s, Object... vars) {
private String getLocalised(String s, Object... vars) {
var key = getKey(s);
if (english == null) {
TrackEvent.warn("Translations not initialized for " + key);
return s;
return key;
}
if (currentLanguage.getValue() != null
&& currentLanguage.getValue().getTranslations().containsKey(key)) {
var localisedString = currentLanguage.getValue().getTranslations().get(key);
return getValue(localisedString, vars);
if (currentLanguage.getValue() != null) {
var localisedString = currentLanguage.getValue().getLocalised(key, vars);
if (localisedString.isPresent()) {
return localisedString.get();
}
}
if (english.getTranslations().containsKey(key)) {
var localisedString = english.getTranslations().get(key);
return getValue(localisedString, vars);
if (english != null) {
var localisedString = english.getLocalised(key, vars);
if (localisedString.isPresent()) {
return localisedString.get();
}
}
TrackEvent.warn("Translation key not found for " + key);
return key;
}
private boolean matchesLocale(Path f, Locale l) {
var name = FilenameUtils.getBaseName(f.getFileName().toString());
var ending = "_" + l.toLanguageTag();
return name.endsWith(ending);
}
public String getMarkdownDocumentation(String name) {
if (name.contains(":")) {
name = name.substring(name.indexOf(":") + 1);
@@ -174,77 +173,4 @@ public class AppI18n {
.handle();
return "";
}
private LoadedTranslations load(Locale l) throws Exception {
TrackEvent.info("Loading translations ...");
var translations = new HashMap<String, String>();
{
var basePath = XPipeInstallation.getLangPath().resolve("strings");
AtomicInteger fileCounter = new AtomicInteger();
AtomicInteger lineCounter = new AtomicInteger();
Files.walkFileTree(basePath, new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (!matchesLocale(file, l)) {
return FileVisitResult.CONTINUE;
}
if (!file.getFileName().toString().endsWith(".properties")) {
return FileVisitResult.CONTINUE;
}
fileCounter.incrementAndGet();
try (var in = Files.newInputStream(file)) {
var props = new Properties();
props.load(new InputStreamReader(in, StandardCharsets.UTF_8));
props.forEach((key, value) -> {
translations.put(key.toString(), value.toString());
lineCounter.incrementAndGet();
});
} catch (IOException ex) {
ErrorEvent.fromThrowable(ex).omitted(true).build().handle();
}
return FileVisitResult.CONTINUE;
}
});
}
var markdownDocumentations = new HashMap<String, String>();
{
var basePath = XPipeInstallation.getLangPath().resolve("texts");
Files.walkFileTree(basePath, new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (!matchesLocale(file, l)) {
return FileVisitResult.CONTINUE;
}
if (!file.getFileName().toString().endsWith(".md")) {
return FileVisitResult.CONTINUE;
}
var name = file.getFileName()
.toString()
.substring(0, file.getFileName().toString().lastIndexOf("_"));
try (var in = Files.newInputStream(file)) {
markdownDocumentations.put(name, new String(in.readAllBytes(), StandardCharsets.UTF_8));
} catch (IOException ex) {
ErrorEvent.fromThrowable(ex).omitted(true).build().handle();
}
return FileVisitResult.CONTINUE;
}
});
}
return new LoadedTranslations(l, translations, markdownDocumentations);
}
@Value
public static class LoadedTranslations {
Locale locale;
Map<String, String> translations;
Map<String, String> markdownDocumentations;
}
}
@@ -0,0 +1,127 @@
package io.xpipe.app.core;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.SupportedLocale;
import io.xpipe.core.util.XPipeInstallation;
import lombok.Value;
import org.apache.commons.io.FilenameUtils;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
@Value
public class AppI18nData {
private static final Pattern VAR_PATTERN = Pattern.compile("\\$\\w+?\\$");
SupportedLocale locale;
Map<String, String> translations;
Map<String, String> markdownDocumentations;
Optional<String> getLocalised(String s, Object... vars) {
if (getTranslations().containsKey(s)) {
var localisedString = getTranslations().get(s);
return Optional.ofNullable(getValue(localisedString, vars));
}
return Optional.empty();
}
private String getValue(String s, Object... vars) {
s = s.replace("\\n", "\n");
if (vars.length > 0) {
var matcher = VAR_PATTERN.matcher(s);
for (var v : vars) {
v = v != null ? v : "null";
if (matcher.find()) {
var group = matcher.group();
s = s.replace(group, v.toString());
} else {
TrackEvent.warn("No match found for value " + v + " in string " + s);
}
}
}
return s;
}
static AppI18nData load(SupportedLocale l) throws Exception {
TrackEvent.info("Loading translations ...");
var translations = new HashMap<String, String>();
{
var basePath = XPipeInstallation.getLangPath().resolve("strings");
AtomicInteger fileCounter = new AtomicInteger();
AtomicInteger lineCounter = new AtomicInteger();
Files.walkFileTree(basePath, new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (!matchesLocale(file, l.getLocale())) {
return FileVisitResult.CONTINUE;
}
if (!file.getFileName().toString().endsWith(".properties")) {
return FileVisitResult.CONTINUE;
}
fileCounter.incrementAndGet();
try (var in = Files.newInputStream(file)) {
var props = new Properties();
props.load(new InputStreamReader(in, StandardCharsets.UTF_8));
props.forEach((key, value) -> {
translations.put(key.toString(), value.toString());
lineCounter.incrementAndGet();
});
} catch (IOException ex) {
ErrorEvent.fromThrowable(ex).omitted(true).build().handle();
}
return FileVisitResult.CONTINUE;
}
});
}
var markdownDocumentations = new HashMap<String, String>();
{
var basePath = XPipeInstallation.getLangPath().resolve("texts");
Files.walkFileTree(basePath, new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (!matchesLocale(file, l.getLocale())) {
return FileVisitResult.CONTINUE;
}
if (!file.getFileName().toString().endsWith(".md")) {
return FileVisitResult.CONTINUE;
}
var name = file.getFileName()
.toString()
.substring(0, file.getFileName().toString().lastIndexOf("_"));
try (var in = Files.newInputStream(file)) {
markdownDocumentations.put(name, new String(in.readAllBytes(), StandardCharsets.UTF_8));
} catch (IOException ex) {
ErrorEvent.fromThrowable(ex).omitted(true).build().handle();
}
return FileVisitResult.CONTINUE;
}
});
}
return new AppI18nData(l, translations, markdownDocumentations);
}
private static boolean matchesLocale(Path f, Locale l) {
var name = FilenameUtils.getBaseName(f.getFileName().toString());
var ending = "_" + l.toLanguageTag();
return name.endsWith(ending);
}
}
@@ -4,7 +4,6 @@ import io.xpipe.app.browser.BrowserFullSessionComp;
import io.xpipe.app.browser.BrowserFullSessionModel;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.store.StoreLayoutComp;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.prefs.AppPrefsComp;
import io.xpipe.app.util.Hyperlinks;
import io.xpipe.app.util.LabelGraphic;
@@ -102,12 +101,6 @@ public class AppLayoutModel {
LicenseProvider.get().overviewPage(),
null,
null),
new Entry(
AppI18n.observable("team"),
new LabelGraphic.IconGraphic("mdi2a-account-group"),
null,
() -> AppPrefs.get().selectCategory("vault"),
null),
new Entry(
AppI18n.observable("visitGithubRepository"),
new LabelGraphic.IconGraphic("mdi2g-github"),
@@ -127,17 +120,17 @@ public class AppLayoutModel {
// () -> Hyperlinks.open(
// "http://localhost:" + AppBeaconServer.get().getPort()),
// null),
new Entry(
AppI18n.observable("documentation"),
new LabelGraphic.IconGraphic("mdi2b-book-open-variant"),
null,
() -> Hyperlinks.open(Hyperlinks.DOCS),
null),
new Entry(
AppI18n.observable("webtop"),
new LabelGraphic.IconGraphic("mdi2d-desktop-mac"),
null,
() -> Hyperlinks.open(Hyperlinks.GITHUB_WEBTOP),
null),
new Entry(
AppI18n.observable("pythonApi"),
new LabelGraphic.IconGraphic("mdi2l-language-python"),
null,
() -> Hyperlinks.open(Hyperlinks.GITHUB_PYTHON_API),
null)));
return l;
@@ -21,6 +21,7 @@ public class AppOpenArguments {
private static final List<String> bufferedArguments = new ArrayList<>();
public static synchronized void init() {
handleImpl(AppProperties.get().getArguments().getOpenArgs());
handleImpl(bufferedArguments);
bufferedArguments.clear();
}
@@ -30,7 +30,6 @@ public class AppProperties {
UUID buildUuid;
String sentryUrl;
String arch;
List<String> languages;
@Getter
boolean image;
@@ -95,14 +94,10 @@ public class AppProperties {
.orElse(UUID.randomUUID());
sentryUrl = System.getProperty("io.xpipe.app.sentryUrl");
arch = System.getProperty("io.xpipe.app.arch");
languages = Arrays.stream(System.getProperty("io.xpipe.app.languages").split(","))
.sorted()
.toList();
staging = Optional.ofNullable(System.getProperty("io.xpipe.app.staging"))
.map(Boolean::parseBoolean)
.orElse(false);
devLoginPassword = Optional.ofNullable(System.getProperty("io.xpipe.app.loginPassword"))
.orElse(null);
devLoginPassword = System.getProperty("io.xpipe.app.loginPassword");
useVirtualThreads = Optional.ofNullable(System.getProperty("io.xpipe.app.useVirtualThreads"))
.map(Boolean::parseBoolean)
.orElse(true);
@@ -5,8 +5,11 @@ import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.resources.AppResources;
import javafx.application.Platform;
import javafx.scene.Scene;
import atlantafx.base.theme.Styles;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
@@ -19,6 +22,7 @@ public class AppStyle {
private static final Map<Path, String> STYLESHEET_CONTENTS = new LinkedHashMap<>();
private static final Map<AppTheme.Theme, String> THEME_SPECIFIC_STYLESHEET_CONTENTS = new LinkedHashMap<>();
private static final Map<AppTheme.Theme, String> THEME_PREFERENCES_STYLESHEET_CONTENTS = new LinkedHashMap<>();
private static final List<Scene> scenes = new ArrayList<>();
private static String FONT_CONTENTS = "";
@@ -34,10 +38,15 @@ public class AppStyle {
AppPrefs.get().useSystemFont().addListener((c, o, n) -> {
changeFontUsage(n);
});
AppPrefs.get().theme.addListener((c, o, n) -> {
AppPrefs.get().theme().addListener((c, o, n) -> {
changeTheme(n);
});
}
var fxPrefs = Platform.getPreferences();
fxPrefs.accentColorProperty().addListener((c, o, n) -> {
changePlatformPreferences();
});
}
private static void loadStylesheets() {
@@ -88,6 +97,8 @@ public class AppStyle {
var bytes = Files.readAllBytes(file);
var s = "data:text/css;base64," + Base64.getEncoder().encodeToString(bytes);
THEME_SPECIFIC_STYLESHEET_CONTENTS.put(theme, s);
THEME_PREFERENCES_STYLESHEET_CONTENTS.put(
theme, Styles.toDataURI(theme.getPlatformPreferencesStylesheet()));
}
});
}
@@ -104,16 +115,42 @@ public class AppStyle {
}
}
private static void changePlatformPreferences() {
if (AppPrefs.get() == null) {
return;
}
var t = AppPrefs.get().theme().getValue();
if (t == null) {
return;
}
scenes.forEach(scene -> {
scene.getStylesheets().remove(THEME_PREFERENCES_STYLESHEET_CONTENTS.get(t));
});
THEME_PREFERENCES_STYLESHEET_CONTENTS.clear();
for (AppTheme.Theme theme : AppTheme.Theme.ALL) {
THEME_PREFERENCES_STYLESHEET_CONTENTS.put(
theme, Styles.toDataURI(theme.getPlatformPreferencesStylesheet()));
}
scenes.forEach(scene -> {
scene.getStylesheets().add(THEME_PREFERENCES_STYLESHEET_CONTENTS.get(t));
});
}
private static void changeTheme(AppTheme.Theme theme) {
scenes.forEach(scene -> {
scene.getStylesheets().removeAll(THEME_SPECIFIC_STYLESHEET_CONTENTS.values());
scene.getStylesheets().removeAll(THEME_PREFERENCES_STYLESHEET_CONTENTS.values());
scene.getStylesheets().add(THEME_SPECIFIC_STYLESHEET_CONTENTS.get(theme));
scene.getStylesheets().add(THEME_PREFERENCES_STYLESHEET_CONTENTS.get(theme));
});
}
public static void reloadStylesheets(Scene scene) {
STYLESHEET_CONTENTS.clear();
THEME_SPECIFIC_STYLESHEET_CONTENTS.clear();
THEME_PREFERENCES_STYLESHEET_CONTENTS.clear();
FONT_CONTENTS = "";
init();
@@ -130,9 +167,10 @@ public class AppStyle {
scene.getStylesheets().add(s);
});
if (AppPrefs.get() != null) {
var t = AppPrefs.get().theme.get();
var t = AppPrefs.get().theme().getValue();
if (t != null) {
scene.getStylesheets().add(THEME_SPECIFIC_STYLESHEET_CONTENTS.get(t));
scene.getStylesheets().add(THEME_PREFERENCES_STYLESHEET_CONTENTS.get(t));
}
}
TrackEvent.debug("Added stylesheets for scene");
@@ -6,6 +6,7 @@ import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.resources.AppResources;
import io.xpipe.app.util.ColorHelper;
import io.xpipe.app.util.PlatformThread;
import io.xpipe.core.process.OsType;
@@ -22,6 +23,7 @@ import javafx.css.PseudoClass;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.util.Duration;
@@ -32,6 +34,7 @@ import lombok.SneakyThrows;
import java.nio.file.Files;
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Collectors;
public class AppTheme {
@@ -60,20 +63,16 @@ public class AppTheme {
return;
}
AppPrefs.get().theme.subscribe(t -> {
AppPrefs.get().theme().subscribe(t -> {
Theme.ALL.forEach(theme -> root.getStyleClass().remove(theme.getCssId()));
if (t == null) {
return;
}
root.getStyleClass().add(t.getCssId());
stage.getScene().getStylesheets().addAll(t.getAdditionalStylesheets());
root.pseudoClassStateChanged(LIGHT, !t.isDark());
root.pseudoClassStateChanged(DARK, t.isDark());
});
AppPrefs.get().theme.addListener((observable, oldValue, newValue) -> {
stage.getScene().getStylesheets().removeAll(oldValue.getAdditionalStylesheets());
});
AppPrefs.get().performanceMode().subscribe(val -> {
root.pseudoClassStateChanged(PRETTY, !val);
@@ -96,19 +95,19 @@ public class AppTheme {
var lastSystemDark = AppCache.getBoolean("lastDarkTheme", false);
var nowDark = Platform.getPreferences().getColorScheme() == ColorScheme.DARK;
AppCache.update("lastDarkTheme", nowDark);
if (AppPrefs.get().theme.getValue() == null || lastSystemDark != nowDark) {
if (AppPrefs.get().theme().getValue() == null || lastSystemDark != nowDark) {
setDefault();
}
Platform.getPreferences().colorSchemeProperty().addListener((observableValue, colorScheme, t1) -> {
Platform.runLater(() -> {
if (t1 == ColorScheme.DARK
&& !AppPrefs.get().theme.getValue().isDark()) {
&& !AppPrefs.get().theme().getValue().isDark()) {
AppPrefs.get().theme.setValue(Theme.getDefaultDarkTheme());
}
if (t1 != ColorScheme.DARK
&& AppPrefs.get().theme.getValue().isDark()) {
&& AppPrefs.get().theme().getValue().isDark()) {
AppPrefs.get().theme.setValue(Theme.getDefaultLightTheme());
}
});
@@ -120,11 +119,11 @@ public class AppTheme {
ErrorEvent.fromThrowable(t).omit().handle();
}
var t = AppPrefs.get().theme.getValue();
var t = AppPrefs.get().theme().getValue();
t.apply();
TrackEvent.debug("Set theme " + t.getId() + " for scene");
AppPrefs.get().theme.addListener((c, o, n) -> {
AppPrefs.get().theme().addListener((c, o, n) -> {
changeTheme(n);
});
@@ -199,8 +198,15 @@ public class AppTheme {
private final String name;
private final int skipLines;
public DerivedTheme(String id, String cssId, String name, atlantafx.base.theme.Theme theme, int skipLines) {
super(id, cssId, theme);
public DerivedTheme(
String id,
String cssId,
String name,
atlantafx.base.theme.Theme theme,
AppFontSizes sizes,
Supplier<Color> contextMenuColor,
int skipLines) {
super(id, cssId, theme, sizes, contextMenuColor);
this.name = name;
this.skipLines = skipLines;
}
@@ -237,17 +243,75 @@ public class AppTheme {
@AllArgsConstructor
public static class Theme implements PrefsChoiceValue {
public static final Theme PRIMER_LIGHT = new Theme("light", "primer", new PrimerLight());
public static final Theme PRIMER_DARK = new Theme("dark", "primer", new PrimerDark());
public static final Theme NORD_LIGHT = new Theme("nordLight", "nord", new NordLight());
public static final Theme NORD_DARK = new Theme("nordDark", "nord", new NordDark());
public static final Theme CUPERTINO_LIGHT = new Theme("cupertinoLight", "cupertino", new CupertinoLight());
public static final Theme CUPERTINO_DARK = new Theme("cupertinoDark", "cupertino", new CupertinoDark());
public static final Theme DRACULA = new Theme("dracula", "dracula", new Dracula());
public static final Theme MOCHA = new DerivedTheme("mocha", "mocha", "Mocha", new PrimerDark(), 115);
public static final Theme PRIMER_LIGHT = new Theme(
"light",
"primer",
new PrimerLight(),
AppFontSizes.forOs(AppFontSizes.BASE_10_5, AppFontSizes.BASE_10, AppFontSizes.BASE_11),
() -> ColorHelper.withOpacity(
Platform.getPreferences().getAccentColor().darker().desaturate(), 0.3));
public static final Theme PRIMER_DARK = new Theme(
"dark",
"primer",
new PrimerDark(),
AppFontSizes.forOs(AppFontSizes.BASE_11, AppFontSizes.BASE_10, AppFontSizes.BASE_11),
() -> ColorHelper.withOpacity(
Platform.getPreferences().getAccentColor().desaturate().desaturate(), 0.2));
public static final Theme NORD_LIGHT = new Theme(
"nordLight",
"nord",
new NordLight(),
AppFontSizes.forOs(AppFontSizes.BASE_10_5, AppFontSizes.BASE_10, AppFontSizes.BASE_11),
() -> ColorHelper.withOpacity(
Platform.getPreferences().getAccentColor().darker().desaturate(), 0.3));
public static final Theme NORD_DARK = new Theme(
"nordDark",
"nord",
new NordDark(),
AppFontSizes.forOs(AppFontSizes.BASE_11, AppFontSizes.BASE_10, AppFontSizes.BASE_11),
() -> ColorHelper.withOpacity(
Platform.getPreferences().getAccentColor().desaturate().desaturate(), 0.2));
public static final Theme CUPERTINO_LIGHT = new Theme(
"cupertinoLight",
"cupertino",
new CupertinoLight(),
AppFontSizes.forOs(AppFontSizes.BASE_10_5, AppFontSizes.BASE_10, AppFontSizes.BASE_11),
() -> ColorHelper.withOpacity(
Platform.getPreferences().getAccentColor().darker().desaturate(), 0.3));
public static final Theme CUPERTINO_DARK = new Theme(
"cupertinoDark",
"cupertino",
new CupertinoDark(),
AppFontSizes.forOs(AppFontSizes.BASE_11, AppFontSizes.BASE_10, AppFontSizes.BASE_11),
() -> ColorHelper.withOpacity(
Platform.getPreferences().getAccentColor().desaturate().desaturate(), 0.2));
public static final Theme DRACULA = new Theme(
"dracula",
"dracula",
new Dracula(),
AppFontSizes.forOs(AppFontSizes.BASE_11, AppFontSizes.BASE_10, AppFontSizes.BASE_11),
() -> ColorHelper.withOpacity(
Platform.getPreferences().getAccentColor().desaturate().desaturate(), 0.2));
public static final Theme MOCHA = new DerivedTheme(
"mocha",
"mocha",
"Mocha",
new PrimerDark(),
AppFontSizes.forOs(AppFontSizes.BASE_11, AppFontSizes.BASE_10, AppFontSizes.BASE_11),
() -> ColorHelper.withOpacity(
Platform.getPreferences().getAccentColor().desaturate().desaturate(), 0.2),
115);
// Adjust this to create your own theme
public static final Theme CUSTOM = new DerivedTheme("custom", "primer", "Custom", new PrimerDark(), 115);
public static final Theme CUSTOM = new DerivedTheme(
"custom",
"primer",
"Custom",
new PrimerDark(),
AppFontSizes.forOs(AppFontSizes.BASE_10_5, AppFontSizes.BASE_10_5, AppFontSizes.BASE_11),
() -> ColorHelper.withOpacity(
Platform.getPreferences().getAccentColor().desaturate().desaturate(), 0.2),
115);
// Also include your custom theme here
public static final List<Theme> ALL = List.of(
@@ -259,6 +323,12 @@ public class AppTheme {
protected final atlantafx.base.theme.Theme theme;
@Getter
protected final AppFontSizes fontSizes;
@Getter
protected final Supplier<Color> contextMenuColor;
static Theme getDefaultLightTheme() {
return switch (OsType.getLocal()) {
case OsType.Windows windows -> PRIMER_LIGHT;
@@ -283,6 +353,13 @@ public class AppTheme {
Application.setUserAgentStylesheet(theme.getUserAgentStylesheetBSS());
}
protected String getPlatformPreferencesStylesheet() {
var c = contextMenuColor.get();
var hex = ColorHelper.toWeb(c);
var s = "* { -color-context-menu: " + hex + "; }";
return s;
}
public List<String> getAdditionalStylesheets() {
var r = AppResources.getResourceURL(AppResources.XPIPE_MODULE, "theme/" + getId() + ".css");
if (r.isEmpty()) {
@@ -1,5 +1,6 @@
package io.xpipe.app.core.check;
import io.xpipe.app.core.AppProperties;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.PlatformState;
@@ -9,6 +10,15 @@ import javafx.application.Platform;
public class AppGpuCheck {
public static void check() {
if (!AppProperties.get().isInitialLaunch()) {
return;
}
// We might launch the platform due to an error early
if (AppPrefs.get() == null) {
return;
}
if (PlatformState.getCurrent() != PlatformState.RUNNING) {
return;
}
@@ -11,7 +11,7 @@ public class AppJavaOptionsCheck {
}
var env = System.getenv("_JAVA_OPTIONS");
if (env == null) {
if (env == null || env.isBlank()) {
return;
}
@@ -22,7 +22,7 @@ public class AppShellCheck {
case OsType.MacOs macOs -> new AppShellChecker() {
@Override
protected boolean shouldAttemptFallback() {
protected boolean shouldAttemptFallbackForProcessStartFail() {
// We don't want to fall back on macOS as occasional zsh spawn issues would cause many users
// to use sh
return false;
@@ -5,6 +5,7 @@ import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.LocalShell;
import io.xpipe.app.util.ScriptHelper;
import io.xpipe.core.process.ProcessOutputException;
import io.xpipe.core.process.ShellSpawnException;
import lombok.Value;
@@ -16,10 +17,11 @@ public abstract class AppShellChecker {
var err = selfTestErrorCheck();
var canFallback = !ProcessControlProvider.get()
.getEffectiveLocalDialect()
.equals(ProcessControlProvider.get().getFallbackDialect())
&& shouldAttemptFallback();
if (err.isPresent() && canFallback) {
.getEffectiveLocalDialect()
.equals(ProcessControlProvider.get().getFallbackDialect());
if (err.isPresent()
&& canFallback
&& (shouldAttemptFallbackForProcessStartFail() || !err.get().isProcessSpawnIssue())) {
var msg = formatMessage(err.get().getMessage());
ErrorEvent.fromThrowable(new IllegalStateException(msg)).expected().handle();
toggleFallback();
@@ -41,7 +43,7 @@ public abstract class AppShellChecker {
}
}
protected boolean shouldAttemptFallback() {
protected boolean shouldAttemptFallbackForProcessStartFail() {
return true;
}
@@ -90,12 +92,15 @@ public abstract class AppShellChecker {
.readStdoutOrThrow();
if (!out.equals("test")) {
return Optional.of(new FailureResult(
"Expected output \"test\", got output \"" + out + "\" when running test script", true));
"Expected output \"test\", got output \"" + out + "\" when running test script", false, true));
}
} catch (ProcessOutputException ex) {
return Optional.of(new FailureResult(ex.getOutput() != null ? ex.getOutput() : ex.getMessage(), true));
return Optional.of(
new FailureResult(ex.getOutput() != null ? ex.getOutput() : ex.getMessage(), false, true));
} catch (ShellSpawnException ex) {
return Optional.of(new FailureResult(ex.getMessage(), true, true));
} catch (Throwable t) {
return Optional.of(new FailureResult(t.getMessage() != null ? t.getMessage() : t.toString(), false));
return Optional.of(new FailureResult(t.getMessage() != null ? t.getMessage() : t.toString(), false, false));
}
return Optional.empty();
}
@@ -104,6 +109,7 @@ public abstract class AppShellChecker {
public static class FailureResult {
String message;
boolean processSpawnIssue;
boolean canContinue;
}
}
@@ -13,12 +13,11 @@ import io.xpipe.app.core.window.AppMainWindow;
import io.xpipe.app.ext.ActionProvider;
import io.xpipe.app.ext.DataStoreProviders;
import io.xpipe.app.ext.ProcessControlProvider;
import io.xpipe.app.icon.SystemIconManager;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.resources.AppImages;
import io.xpipe.app.resources.AppResources;
import io.xpipe.app.resources.SystemIcons;
import io.xpipe.app.resources.*;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStorageSyncHandler;
import io.xpipe.app.terminal.TerminalLauncherManager;
@@ -26,7 +25,6 @@ import io.xpipe.app.terminal.TerminalView;
import io.xpipe.app.update.UpdateAvailableDialog;
import io.xpipe.app.update.UpdateChangelogAlert;
import io.xpipe.app.update.UpdateNagDialog;
import io.xpipe.app.update.XPipeDistributionType;
import io.xpipe.app.util.*;
import io.xpipe.core.util.XPipeDaemonMode;
@@ -47,7 +45,7 @@ public class BaseMode extends OperationMode {
}
@Override
public void onSwitchTo() throws Throwable {
public void onSwitchTo() {
if (initialized) {
return;
}
@@ -92,8 +90,9 @@ public class BaseMode extends OperationMode {
shellLoaded.countDown();
AppRosettaCheck.check();
AppTestCommandCheck.check();
XPipeDistributionType.init();
AppPrefs.setLocalDefaultsIfNeeded();
PlatformInit.init(true);
AppMainWindow.addUpdateTitleListener();
},
() -> {
shellLoaded.await();
@@ -134,8 +133,9 @@ public class BaseMode extends OperationMode {
() -> {
PlatformInit.init(true);
AppImages.init();
SystemIcons.init();
imagesLoaded.countDown();
storageLoaded.await();
SystemIconManager.init();
},
() -> {
BrowserIconManager.loadIfNecessary();
@@ -92,6 +92,11 @@ public abstract class OperationMode {
OperationMode.halt(1);
}
if (ex instanceof OutOfMemoryError) {
ex.printStackTrace();
OperationMode.halt(1);
}
ErrorEvent.fromThrowable(ex).unhandled(true).build().handle();
});
@@ -103,7 +108,8 @@ public abstract class OperationMode {
AppDebugModeCheck.printIfNeeded();
AppProperties.logSystemProperties();
AppProperties.get().logArguments();
AppExtensionManager.init(true);
AppDistributionType.init();
AppExtensionManager.init();
AppI18n.init();
AppPrefs.initLocal();
AppBeaconServer.setupPort();
@@ -245,7 +251,13 @@ public abstract class OperationMode {
var loc = AppProperties.get().isDevelopmentEnvironment()
? XPipeInstallation.getLocalDefaultInstallationBasePath()
: XPipeInstallation.getCurrentInstallationBasePath().toString();
var exec = XPipeInstallation.createExternalAsyncLaunchCommand(loc, XPipeDaemonMode.GUI, "", true);
var dataDir = AppProperties.get().getDataDir();
// We have to quote the arguments like this to make it work in PowerShell as well
var exec = XPipeInstallation.createExternalAsyncLaunchCommand(
loc,
XPipeDaemonMode.GUI,
"\"-Dio.xpipe.app.acceptEula=true\" \"-Dio.xpipe.app.dataDir=" + dataDir + "\"",
true);
LocalShell.getShell().executeSimpleCommand(exec);
}
@@ -3,7 +3,6 @@ package io.xpipe.app.core.window;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.base.ModalButton;
import io.xpipe.app.comp.base.ModalOverlay;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.util.PlatformInit;
import io.xpipe.app.util.PlatformThread;
@@ -11,6 +10,7 @@ import io.xpipe.app.util.ThreadHelper;
import javafx.animation.PauseTransition;
import javafx.application.Platform;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
@@ -102,14 +102,24 @@ public class AppDialog {
}
public static Comp<?> dialogTextKey(String s) {
return dialogText(AppI18n.get(s));
return dialogText(AppI18n.observable(s));
}
public static Comp<?> dialogText(String s) {
return Comp.of(() -> {
var text = new Text(s);
text.setWrappingWidth(450);
AppFont.medium(text);
var sp = new StackPane(text);
return sp;
})
.prefWidth(450);
}
public static Comp<?> dialogText(ObservableValue<String> s) {
return Comp.of(() -> {
var text = new Text();
text.textProperty().bind(s);
text.setWrappingWidth(450);
var sp = new StackPane(text);
return sp;
})
@@ -118,11 +128,20 @@ public class AppDialog {
public static boolean confirm(String translationKey) {
var confirmed = new AtomicBoolean(false);
var content = dialogTextKey(AppI18n.get(translationKey + "Content"));
var content = dialogTextKey(translationKey + "Content");
var modal = ModalOverlay.of(translationKey + "Title", content);
modal.addButton(ModalButton.cancel());
modal.addButton(ModalButton.ok(() -> confirmed.set(true)));
showAndWait(modal);
return confirmed.get();
}
public static boolean confirm(String titleKey, ObservableValue<String> content) {
var confirmed = new AtomicBoolean(false);
var modal = ModalOverlay.of(titleKey, dialogText(content));
modal.addButton(ModalButton.cancel());
modal.addButton(ModalButton.ok(() -> confirmed.set(true)));
showAndWait(modal);
return confirmed.get();
}
}

Some files were not shown because too many files have changed in this diff Show More