Squash merge branch 14-release into master

This commit is contained in:
crschnick 2025-01-16 07:29:55 +00:00
parent 48c9f96c03
commit 45f6545fc8
2705 changed files with 42081 additions and 20693 deletions

View file

@ -187,7 +187,8 @@ APPENDIX: How to apply the Apache License to your work.
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2024 Christopher Schnick
Copyright 2023 Christopher Schnick
Copyright 2023 XPipe UG (haftungsbeschränkt)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View file

@ -131,6 +131,9 @@ are not able to resolve and install any dependency packages.
### RHEL-based distros
The rpm releases are signed with the GPG key https://xpipe.io/signatures/crschnick.asc.
You can import it via `rpm --import https://xpipe.io/signatures/crschnick.asc` to allow your rpm-based package manager to verify the release signature.
The following rpm installers are available:
- [Linux .rpm Installer (x86-64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-installer-linux-x86_64.rpm)

View file

@ -23,8 +23,8 @@ dependencies {
api project(':beacon')
compileOnly 'org.hamcrest:hamcrest:3.0'
compileOnly 'org.junit.jupiter:junit-jupiter-api:5.11.3'
compileOnly 'org.junit.jupiter:junit-jupiter-params:5.11.3'
compileOnly 'org.junit.jupiter:junit-jupiter-api:5.11.4'
compileOnly 'org.junit.jupiter:junit-jupiter-params:5.11.4'
api 'com.vladsch.flexmark:flexmark:0.64.8'
api 'com.vladsch.flexmark:flexmark-util:0.64.8'
@ -56,10 +56,10 @@ dependencies {
exclude group: 'org.apache.commons', module: 'commons-lang3'
}
api 'org.apache.commons:commons-lang3:3.17.0'
api 'io.sentry:sentry:7.18.0'
api 'io.sentry:sentry:7.20.0'
api 'commons-io:commons-io:2.18.0'
api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.18.1"
api group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.18.1"
api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.18.2"
api group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.18.2"
api group: 'org.kordamp.ikonli', name: 'ikonli-material2-pack', version: "12.2.0"
api group: 'org.kordamp.ikonli', name: 'ikonli-materialdesign2-pack', version: "12.2.0"
api group: 'org.kordamp.ikonli', name: 'ikonli-javafx', version: "12.2.0"
@ -105,6 +105,9 @@ run {
workingDir = rootDir
jvmArgs += ['-XX:+EnableDynamicAgentLoading']
def exts = files(project.allExtensions.stream().map(p -> p.getTasksByName('jar', true)[0].outputs.files.singleFile).toList());
classpath += exts
}
task runAttachedDebugger(type: JavaExec) {
@ -120,6 +123,9 @@ task runAttachedDebugger(type: JavaExec) {
)
jvmArgs += ['-XX:+EnableDynamicAgentLoading']
systemProperties run.systemProperties
def exts = files(project.allExtensions.stream().map(p -> p.getTasksByName('jar', true)[0].outputs.files.singleFile).toList());
classpath += exts
}
processResources {

View file

@ -7,7 +7,7 @@ public class Main {
public static void main(String[] args) {
if (args.length == 1 && args[0].equals("version")) {
AppProperties.init();
AppProperties.init(args);
System.out.println(AppProperties.get().getVersion());
return;
}

View file

@ -96,7 +96,15 @@ public class BeaconRequestHandler<T> implements HttpHandler {
}
}
}
response = beaconInterface.handle(exchange, object);
var sync = beaconInterface.getSynchronizationObject();
if (sync != null) {
synchronized (sync) {
response = beaconInterface.handle(exchange, object);
}
} else {
response = beaconInterface.handle(exchange, object);
}
} catch (BeaconClientException clientException) {
ErrorEvent.fromThrowable(clientException).omit().expected().handle();
writeError(exchange, new BeaconClientErrorResponse(clientException.getMessage()), 400);
@ -193,7 +201,7 @@ public class BeaconRequestHandler<T> implements HttpHandler {
&& method.getParameters()[0].getType().equals(byte[].class))
.findFirst()
.orElseThrow();
setMethod.invoke(b, s);
setMethod.invoke(b, (Object) s);
var m = b.getClass().getDeclaredMethod("build");
m.setAccessible(true);

View file

@ -1,11 +1,14 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.prefs.ExternalApplicationType;
import io.xpipe.app.terminal.TerminalView;
import io.xpipe.app.util.AskpassAlert;
import io.xpipe.app.util.SecretManager;
import io.xpipe.app.util.SecretQueryState;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.api.AskpassExchange;
import io.xpipe.core.process.OsType;
import com.sun.net.httpserver.HttpExchange;
@ -50,17 +53,24 @@ public class AskpassExchangeImpl extends AskpassExchange {
}
var term = TerminalView.get().getTerminalInstances().stream()
.filter(instance ->
instance.getTerminalProcess().equals(found.get().getTerminal()))
.filter(instance -> instance.equals(found.get().getTerminal()))
.findFirst();
if (term.isEmpty()) {
return;
}
var control = term.get().controllable();
control.ifPresent(controllableTerminalSession -> {
controllableTerminalSession.focus();
});
if (control.isPresent()) {
control.get().focus();
} else {
if (OsType.getLocal() == OsType.MACOS) {
// Just focus the app, this is correct most of the time
var terminalType = AppPrefs.get().terminalType().getValue();
if (terminalType instanceof ExternalApplicationType.MacApplication m) {
m.focus();
}
}
}
}
@Override

View file

@ -0,0 +1,34 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreCategory;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.api.CategoryAddExchange;
import com.sun.net.httpserver.HttpExchange;
public class CategoryAddExchangeImpl extends CategoryAddExchange {
@Override
public Object handle(HttpExchange exchange, Request msg) throws Throwable {
if (DataStorage.get().getStoreCategoryIfPresent(msg.getParent()).isEmpty()) {
throw new BeaconClientException("Parent category with id " + msg.getParent() + " does not exist");
}
if (DataStorage.get().getStoreCategories().stream()
.anyMatch(dataStoreCategory -> msg.getParent().equals(dataStoreCategory.getParentCategory())
&& msg.getName().equals(dataStoreCategory.getName()))) {
throw new BeaconClientException(
"Category with name " + msg.getName() + " already exists in parent category");
}
var cat = DataStoreCategory.createNew(msg.getParent(), msg.getName());
DataStorage.get().addStoreCategory(cat);
return Response.builder().category(cat.getUuid()).build();
}
@Override
public Object getSynchronizationObject() {
return DataStorage.get();
}
}

View file

@ -3,6 +3,7 @@ package io.xpipe.app.beacon.impl;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.api.ConnectionAddExchange;
import io.xpipe.core.util.ValidationException;
@ -17,7 +18,17 @@ public class ConnectionAddExchangeImpl extends ConnectionAddExchange {
return Response.builder().connection(found.get().getUuid()).build();
}
if (msg.getCategory() != null
&& DataStorage.get()
.getStoreCategoryIfPresent(msg.getCategory())
.isEmpty()) {
throw new BeaconClientException("Category with id " + msg.getCategory() + " does not exist");
}
var entry = DataStoreEntry.createNew(msg.getName(), msg.getData());
if (msg.getCategory() != null) {
entry.setCategoryUuid(msg.getCategory());
}
try {
DataStorage.get().addStoreEntryInProgress(entry);
if (msg.getValidate()) {
@ -35,6 +46,22 @@ public class ConnectionAddExchangeImpl extends ConnectionAddExchange {
DataStorage.get().removeStoreEntryInProgress(entry);
}
DataStorage.get().addStoreEntryIfNotPresent(entry);
// Explicitly assign category
if (msg.getCategory() != null) {
DataStorage.get()
.updateCategory(
entry,
DataStorage.get()
.getStoreCategoryIfPresent(msg.getCategory())
.orElseThrow());
}
return Response.builder().connection(entry.getUuid()).build();
}
@Override
public Object getSynchronizationObject() {
return DataStorage.get();
}
}

View file

@ -24,4 +24,9 @@ public class ConnectionBrowseExchangeImpl extends ConnectionBrowseExchange {
AppLayoutModel.get().selectBrowser();
return Response.builder().build();
}
@Override
public Object getSynchronizationObject() {
return DataStorage.get();
}
}

View file

@ -56,19 +56,8 @@ public class ConnectionInfoExchangeImpl extends ConnectionInfoExchange {
return Response.builder().infos(list).build();
}
private Class<?> toWrapper(Class<?> clazz) {
if (!clazz.isPrimitive()) return clazz;
if (clazz == Integer.TYPE) return Integer.class;
if (clazz == Long.TYPE) return Long.class;
if (clazz == Boolean.TYPE) return Boolean.class;
if (clazz == Byte.TYPE) return Byte.class;
if (clazz == Character.TYPE) return Character.class;
if (clazz == Float.TYPE) return Float.class;
if (clazz == Double.TYPE) return Double.class;
if (clazz == Short.TYPE) return Short.class;
if (clazz == Void.TYPE) return Void.class;
return clazz;
@Override
public Object getSynchronizationObject() {
return DataStorage.get();
}
}

View file

@ -56,6 +56,11 @@ public class ConnectionQueryExchangeImpl extends ConnectionQueryExchange {
.build();
}
@Override
public Object getSynchronizationObject() {
return DataStorage.get();
}
private String toRegex(String pattern) {
// https://stackoverflow.com/a/17369948/6477761
StringBuilder sb = new StringBuilder(pattern.length());

View file

@ -21,4 +21,9 @@ public class ConnectionRefreshExchangeImpl extends ConnectionRefreshExchange {
}
return Response.builder().build();
}
@Override
public Object getSynchronizationObject() {
return DataStorage.get();
}
}

View file

@ -24,4 +24,9 @@ public class ConnectionRemoveExchangeImpl extends ConnectionRemoveExchange {
DataStorage.get().deleteWithChildren(entries.toArray(DataStoreEntry[]::new));
return Response.builder().build();
}
@Override
public Object getSynchronizationObject() {
return DataStorage.get();
}
}

View file

@ -22,4 +22,9 @@ public class ConnectionTerminalExchangeImpl extends ConnectionTerminalExchange {
TerminalLauncher.open(e, e.getName(), msg.getDirectory(), sc);
return Response.builder().build();
}
@Override
public Object getSynchronizationObject() {
return DataStorage.get();
}
}

View file

@ -24,4 +24,9 @@ public class ConnectionToggleExchangeImpl extends ConnectionToggleExchange {
}
return Response.builder().build();
}
@Override
public Object getSynchronizationObject() {
return DataStorage.get();
}
}

View file

@ -1,24 +1,39 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.core.launcher.LauncherInput;
import io.xpipe.app.core.AppOpenArguments;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.util.PlatformState;
import io.xpipe.app.util.PlatformInit;
import io.xpipe.beacon.BeaconServerException;
import io.xpipe.beacon.api.DaemonOpenExchange;
import io.xpipe.core.process.OsType;
import com.sun.net.httpserver.HttpExchange;
public class DaemonOpenExchangeImpl extends DaemonOpenExchange {
private int openCounter = 0;
@Override
public Object handle(HttpExchange exchange, Request msg) throws BeaconServerException {
if (msg.getArguments().isEmpty()) {
if (!OperationMode.switchToSyncIfPossible(OperationMode.GUI)) {
throw new BeaconServerException(PlatformState.getLastError());
try {
// At this point we are already loading this on another thread
// so this call will only perform the waiting
PlatformInit.init(true);
} catch (Throwable t) {
throw new BeaconServerException(t);
}
}
LauncherInput.handle(msg.getArguments());
// The open command is used as a default opener on Linux
// We don't want to overwrite the default startup mode
if (OsType.getLocal() == OsType.LINUX && openCounter++ == 0) {
return Response.builder().build();
}
OperationMode.switchToAsync(OperationMode.GUI);
} else {
AppOpenArguments.handle(msg.getArguments());
}
return Response.builder().build();
}
@ -26,4 +41,9 @@ public class DaemonOpenExchangeImpl extends DaemonOpenExchange {
public boolean requiresEnabledApi() {
return false;
}
@Override
public boolean requiresCompletedStartup() {
return false;
}
}

View file

@ -20,11 +20,10 @@ public class FsScriptExchangeImpl extends FsScriptExchange {
try (var in = BlobManager.get().getBlob(msg.getBlob())) {
data = new String(in.readAllBytes(), StandardCharsets.UTF_8);
}
data = shell.getControl().getShellDialect().prepareScriptContent(data);
var file = ScriptHelper.getExecScriptFile(shell.getControl());
shell.getControl()
.getShellDialect()
.createScriptTextFileWriteCommand(shell.getControl(), data, file.toString())
.execute();
shell.getControl().view().writeScriptFile(file, data);
file = ScriptHelper.fixScriptPermissions(shell.getControl(), file);
return Response.builder().path(file).build();
}
}

View file

@ -30,6 +30,8 @@ import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.stage.Window;
import javafx.stage.WindowEvent;
import java.util.List;
import java.util.function.BiConsumer;
@ -50,8 +52,14 @@ public class BrowserFileChooserSessionComp extends DialogComp {
public static void openSingleFile(
Supplier<DataStoreEntryRef<? extends FileSystemStore>> store, Consumer<FileReference> file, boolean save) {
PlatformThread.runLaterIfNeeded(() -> {
var lastWindow = Window.getWindows().stream()
.filter(window -> window.isFocused())
.findFirst();
var model = new BrowserFileChooserSessionModel(BrowserFileSystemTabModel.SelectionMode.SINGLE_FILE);
DialogComp.showWindow(save ? "saveFileTitle" : "openFileTitle", stage -> {
stage.addEventFilter(WindowEvent.WINDOW_HIDDEN, event -> {
lastWindow.ifPresent(window -> window.requestFocus());
});
var comp = new BrowserFileChooserSessionComp(stage, model);
comp.apply(struc -> struc.get().setPrefSize(1200, 700))
.apply(struc -> AppFont.normal(struc.get()))
@ -116,7 +124,8 @@ public class BrowserFileChooserSessionComp extends DialogComp {
var bookmarkTopBar = new BrowserConnectionListFilterComp();
var bookmarksList = new BrowserConnectionListComp(
BindingsHelper.map(model.getSelectedEntry(), v -> v.getEntry().get()),
BindingsHelper.map(
model.getSelectedEntry(), v -> v != null ? v.getEntry().get() : null),
applicable,
action,
bookmarkTopBar.getCategory(),

View file

@ -34,20 +34,14 @@ public class BrowserFullSessionModel extends BrowserAbstractSessionModel<Browser
public static final BrowserFullSessionModel DEFAULT = new BrowserFullSessionModel();
static {
init();
}
@SneakyThrows
private static void init() {
public static void init() {
DEFAULT.openSync(new BrowserHistoryTabModel(DEFAULT), null);
if (AppPrefs.get().pinLocalMachineOnStartup().get()) {
var tab = new BrowserFileSystemTabModel(
DEFAULT, DataStorage.get().local().ref(), BrowserFileSystemTabModel.SelectionMode.ALL);
ThreadHelper.runFailableAsync(() -> {
DEFAULT.openSync(tab, null);
DEFAULT.pinTab(tab);
});
DEFAULT.openSync(tab, null);
DEFAULT.pinTab(tab);
}
}
@ -62,6 +56,10 @@ public class BrowserFullSessionModel extends BrowserAbstractSessionModel<Browser
return Bindings.createObjectBinding(
() -> {
var current = selectedEntry.getValue();
if (current == null) {
return null;
}
if (!current.isCloseable()) {
return null;
}
@ -176,6 +174,10 @@ public class BrowserFullSessionModel extends BrowserAbstractSessionModel<Browser
public void reset() {
synchronized (BrowserFullSessionModel.this) {
if (globalPinnedTab.getValue() != null) {
globalPinnedTab.setValue(null);
}
var all = new ArrayList<>(sessionEntries);
for (var o : all) {
// Don't close busy connections gracefully
@ -242,7 +244,7 @@ public class BrowserFullSessionModel extends BrowserAbstractSessionModel<Browser
if (split != null) {
split.close();
}
previousTabs.remove(e);
super.closeSync(e);
}
}

View file

@ -7,6 +7,7 @@ import javafx.beans.property.BooleanProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import lombok.Getter;
@ -15,12 +16,10 @@ public abstract class BrowserSessionTab {
protected final BooleanProperty busy = new SimpleBooleanProperty();
protected final BrowserAbstractSessionModel<?> browserModel;
protected final String name;
protected final Property<BrowserSessionTab> splitTab = new SimpleObjectProperty<>();
public BrowserSessionTab(BrowserAbstractSessionModel<?> browserModel, String name) {
public BrowserSessionTab(BrowserAbstractSessionModel<?> browserModel) {
this.browserModel = browserModel;
this.name = name;
}
public abstract Comp<?> comp();
@ -31,6 +30,8 @@ public abstract class BrowserSessionTab {
public abstract void close();
public abstract ObservableValue<String> getName();
public abstract String getIcon();
public abstract DataColor getColor();

View file

@ -276,7 +276,7 @@ public class BrowserSessionTabsComp extends SimpleComp {
var cm = ContextMenuHelper.create();
if (tabModel.isCloseable()) {
var unpin = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("unpinTab"));
var unpin = ContextMenuHelper.item(LabelGraphic.none(), "unpinTab");
unpin.visibleProperty()
.bind(PlatformThread.sync(Bindings.createBooleanBinding(
() -> {
@ -290,7 +290,7 @@ public class BrowserSessionTabsComp extends SimpleComp {
});
cm.getItems().add(unpin);
var pin = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("pinTab"));
var pin = ContextMenuHelper.item(LabelGraphic.none(), "pinTab");
pin.visibleProperty()
.bind(PlatformThread.sync(Bindings.createBooleanBinding(
() -> {
@ -304,7 +304,7 @@ public class BrowserSessionTabsComp extends SimpleComp {
cm.getItems().add(pin);
}
var select = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("selectTab"));
var select = ContextMenuHelper.item(LabelGraphic.none(), "selectTab");
select.acceleratorProperty()
.bind(Bindings.createObjectBinding(
() -> {
@ -325,7 +325,7 @@ public class BrowserSessionTabsComp extends SimpleComp {
cm.getItems().add(new SeparatorMenuItem());
var close = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("closeTab"));
var close = ContextMenuHelper.item(LabelGraphic.none(), "closeTab");
close.setAccelerator(new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN));
close.setOnAction(event -> {
if (tab.isClosable()) {
@ -335,7 +335,7 @@ public class BrowserSessionTabsComp extends SimpleComp {
});
cm.getItems().add(close);
var closeOthers = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("closeOtherTabs"));
var closeOthers = ContextMenuHelper.item(LabelGraphic.none(), "closeOtherTabs");
closeOthers.setOnAction(event -> {
tabs.getTabs()
.removeAll(tabs.getTabs().stream()
@ -345,7 +345,7 @@ public class BrowserSessionTabsComp extends SimpleComp {
});
cm.getItems().add(closeOthers);
var closeLeft = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("closeLeftTabs"));
var closeLeft = ContextMenuHelper.item(LabelGraphic.none(), "closeLeftTabs");
closeLeft.setOnAction(event -> {
var index = tabs.getTabs().indexOf(tab);
tabs.getTabs()
@ -356,7 +356,7 @@ public class BrowserSessionTabsComp extends SimpleComp {
});
cm.getItems().add(closeLeft);
var closeRight = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("closeRightTabs"));
var closeRight = ContextMenuHelper.item(LabelGraphic.none(), "closeRightTabs");
closeRight.setOnAction(event -> {
var index = tabs.getTabs().indexOf(tab);
tabs.getTabs()
@ -367,7 +367,7 @@ public class BrowserSessionTabsComp extends SimpleComp {
});
cm.getItems().add(closeRight);
var closeAll = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("closeAllTabs"));
var closeAll = ContextMenuHelper.item(LabelGraphic.none(), "closeAllTabs");
closeAll.setAccelerator(
new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN, KeyCombination.SHIFT_DOWN));
closeAll.setOnAction(event -> {
@ -425,13 +425,16 @@ public class BrowserSessionTabsComp extends SimpleComp {
tab.textProperty()
.bind(Bindings.createStringBinding(
() -> {
return tabModel.getName()
var n = tabModel.getName().getValue();
return (AppPrefs.get().censorMode().get() ? "*".repeat(n.length()) : n)
+ (global.getValue() == tabModel ? " (" + AppI18n.get("pinned") + ")" : "");
},
tabModel.getName(),
global,
AppPrefs.get().language()));
AppPrefs.get().language(),
AppPrefs.get().censorMode()));
} else {
tab.setText(tabModel.getName());
tab.textProperty().bind(tabModel.getName());
}
Comp<?> comp = tabModel.comp();

View file

@ -6,16 +6,26 @@ import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.core.store.DataStore;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import lombok.Getter;
@Getter
public abstract class BrowserStoreSessionTab<T extends DataStore> extends BrowserSessionTab {
protected final DataStoreEntryRef<? extends T> entry;
private final String name;
public BrowserStoreSessionTab(BrowserAbstractSessionModel<?> browserModel, DataStoreEntryRef<? extends T> entry) {
super(browserModel, DataStorage.get().getStoreEntryDisplayName(entry.get()));
super(browserModel);
this.entry = entry;
this.name = DataStorage.get().getStoreEntryDisplayName(entry.get());
}
@Override
public ObservableValue<String> getName() {
return new SimpleStringProperty(name);
}
public abstract Comp<?> comp();

View file

@ -4,7 +4,6 @@ import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.util.*;
import io.xpipe.app.util.PlatformThread;
import io.xpipe.core.process.OsType;
import io.xpipe.core.store.FileEntry;
import io.xpipe.core.store.FileInfo;
@ -29,10 +28,7 @@ import atlantafx.base.theme.Styles;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Objects;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import static io.xpipe.app.util.HumanReadableFormat.byteCount;
@ -283,12 +279,21 @@ public final class BrowserFileListComp extends SimpleComp {
}
try (var ignored = updateFromModel) {
fileList.getSelection().setAll(c.getList());
// Attempt to preserve ordering. Works at least when selecting single entries
var existing = new HashSet<>(fileList.getSelection());
c.getList().forEach(browserEntry -> {
if (!existing.contains(browserEntry)) {
fileList.getSelection().add(browserEntry);
}
});
fileList.getSelection().removeIf(browserEntry -> !c.getList().contains(browserEntry));
}
});
fileList.getSelection().addListener((ListChangeListener<? super BrowserEntry>) c -> {
if (c.getList().equals(table.getSelectionModel().getSelectedItems())) {
var existing = new HashSet<>(fileList.getSelection());
var toApply = new HashSet<>(c.getList());
if (existing.equals(toApply)) {
return;
}

View file

@ -1,5 +1,6 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.util.PasswdFile;
import io.xpipe.app.util.ShellControlCache;
import io.xpipe.core.process.CommandBuilder;
import io.xpipe.core.process.OsType;
@ -16,7 +17,7 @@ public class BrowserFileSystemCache extends ShellControlCache {
private final BrowserFileSystemTabModel model;
private final String username;
private final Map<Integer, String> users = new LinkedHashMap<>();
private final PasswdFile passwdFile;
private final Map<Integer, String> groups = new LinkedHashMap<>();
public BrowserFileSystemCache(BrowserFileSystemTabModel model) throws Exception {
@ -27,16 +28,16 @@ public class BrowserFileSystemCache extends ShellControlCache {
ShellDialect d = sc.getShellDialect();
// If there is no id command, we should still be fine with just assuming root
username = d.printUsernameCommand(sc).readStdoutIfPossible().orElse("root");
loadUsers();
passwdFile = PasswdFile.parse(sc);
loadGroups();
}
public Map<Integer, String> getUsers() {
return passwdFile.getUsers();
}
public int getUidForUser(String name) {
return users.entrySet().stream()
.filter(e -> e.getValue().equals(name))
.findFirst()
.map(e -> e.getKey())
.orElse(0);
return passwdFile.getUidForUser(name);
}
public int getGidForGroup(String name) {
@ -47,28 +48,6 @@ public class BrowserFileSystemCache extends ShellControlCache {
.orElse(0);
}
private void loadUsers() throws Exception {
var sc = model.getFileSystem().getShell().orElseThrow();
if (sc.getOsType() == OsType.WINDOWS || sc.getOsType() == OsType.MACOS) {
return;
}
var lines = sc.command(CommandBuilder.of().add("cat").addFile("/etc/passwd"))
.readStdoutIfPossible()
.orElse("");
lines.lines().forEach(s -> {
var split = s.split(":");
try {
users.putIfAbsent(Integer.parseInt(split[2]), split[0]);
} catch (Exception ignored) {
}
});
if (users.isEmpty()) {
users.put(0, "root");
}
}
private void loadGroups() throws Exception {
var sc = model.getFileSystem().getShell().orElseThrow();
if (sc.getOsType() == OsType.WINDOWS || sc.getOsType() == OsType.MACOS) {

View file

@ -51,15 +51,16 @@ public class BrowserFileSystemHelper {
}
var shell = model.getFileSystem().getShell();
if (shell.isEmpty() || !shell.get().isRunning()) {
if (shell.isEmpty() || !shell.get().isRunning(true)) {
return path;
}
try {
return shell.get()
var r = shell.get()
.getShellDialect()
.evaluateExpression(shell.get(), path)
.readStdoutOrThrow();
return !r.isBlank() ? r : null;
} catch (Exception ex) {
ErrorEvent.expected(ex);
throw ex;

View file

@ -65,7 +65,7 @@ public class BrowserFileSystemSavedState {
return state;
}
public void save() {
public synchronized void save() {
if (model == null) {
return;
}
@ -107,7 +107,7 @@ public class BrowserFileSystemSavedState {
}
}
private void updateRecent(String dir) {
private synchronized void updateRecent(String dir) {
var without = FileNames.removeTrailingSlash(dir);
var with = FileNames.toDirectory(dir);
recentDirectories.removeIf(recentEntry ->

View file

@ -43,8 +43,7 @@ public class BrowserFileSystemTabComp extends SimpleComp {
@Override
protected Region createSimple() {
var alertOverlay = new ModalOverlayComp(Comp.of(() -> createContent()), model.getOverlay());
return alertOverlay.createRegion();
return createContent();
}
private Region createContent() {

View file

@ -5,7 +5,6 @@ import io.xpipe.app.browser.BrowserFullSessionModel;
import io.xpipe.app.browser.BrowserStoreSessionTab;
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.base.ModalOverlayComp;
import io.xpipe.app.core.window.AppMainWindow;
import io.xpipe.app.ext.ProcessControlProvider;
import io.xpipe.app.ext.ShellStore;
@ -44,10 +43,10 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
private final BrowserFileListModel fileList;
private final ReadOnlyObjectWrapper<String> currentPath = new ReadOnlyObjectWrapper<>();
private final BrowserFileSystemHistory history = new BrowserFileSystemHistory();
private final Property<ModalOverlayComp.OverlayContent> overlay = new SimpleObjectProperty<>();
private final BooleanProperty inOverview = new SimpleBooleanProperty();
private final Property<BrowserTransferProgress> progress = new SimpleObjectProperty<>();
private final ObservableList<UUID> terminalRequests = FXCollections.observableArrayList();
private final BooleanProperty transferCancelled = new SimpleBooleanProperty();
private FileSystem fileSystem;
private BrowserFileSystemSavedState savedState;
private BrowserFileSystemCache cache;
@ -65,6 +64,14 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
fileList = new BrowserFileListModel(selectionMode, this);
}
public Optional<FileEntry> findFile(String path) {
return getFileList().getAll().getValue().stream()
.filter(browserEntry -> browserEntry.getFileName().equals(path)
|| browserEntry.getRawFileEntry().getPath().equals(path))
.findFirst()
.map(browserEntry -> browserEntry.getRawFileEntry());
}
@Override
public Comp<?> comp() {
return new BrowserFileSystemTabComp(this, true);
@ -142,6 +149,14 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
}
}
public void killTransfer() {
if (fileSystem == null) {
return;
}
transferCancelled.set(true);
}
public void withShell(FailableConsumer<ShellControl, Exception> c, boolean refresh) {
ThreadHelper.runFailableAsync(() -> {
if (fileSystem == null) {
@ -384,7 +399,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
startIfNeeded();
var op = BrowserFileTransferOperation.ofLocal(
entry, files, BrowserFileTransferMode.COPY, true, progress::setValue);
entry, files, BrowserFileTransferMode.COPY, true, progress::setValue, transferCancelled);
op.execute();
refreshSync();
});
@ -404,7 +419,8 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
}
startIfNeeded();
var op = new BrowserFileTransferOperation(target, files, mode, true, progress::setValue);
var op = new BrowserFileTransferOperation(
target, files, mode, true, progress::setValue, transferCancelled);
op.execute();
refreshSync();
});
@ -463,10 +479,6 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
}
public void runCommandAsync(CommandBuilder command, boolean refresh) {
if (name == null || name.isBlank()) {
return;
}
ThreadHelper.runFailableAsync(() -> {
BooleanScope.executeExclusive(busy, () -> {
if (fileSystem == null) {
@ -491,10 +503,6 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
}
public void runAsync(FailableRunnable<Exception> r, boolean refresh) {
if (name == null || name.isBlank()) {
return;
}
ThreadHelper.runFailableAsync(() -> {
BooleanScope.executeExclusive(busy, () -> {
if (fileSystem == null) {
@ -566,7 +574,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
fullSessionModel.splitTab(
this, new BrowserTerminalDockTabModel(browserModel, this, terminalRequests));
}
TerminalLauncher.open(entry.getEntry(), name, directory, processControl, uuid, !dock);
TerminalLauncher.open(entry.get(), name, directory, processControl, uuid, !dock);
// Restart connection as we will have to start it anyway, so we speed it up by doing it preemptively
startIfNeeded();
@ -575,11 +583,11 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
});
}
public void backSync(int i) throws Exception {
public void backSync(int i) {
cdSync(history.back(i));
}
public void forthSync(int i) throws Exception {
public void forthSync(int i) {
cdSync(history.forth(i));
}

View file

@ -3,6 +3,8 @@ package io.xpipe.app.browser.file;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.core.store.*;
import javafx.beans.property.BooleanProperty;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
@ -20,6 +22,7 @@ public class BrowserFileTransferOperation {
private final BrowserFileTransferMode transferMode;
private final boolean checkConflicts;
private final Consumer<BrowserTransferProgress> progress;
private final BooleanProperty cancelled;
BrowserAlerts.FileConflictChoice lastConflictChoice;
@ -28,12 +31,14 @@ public class BrowserFileTransferOperation {
List<FileEntry> files,
BrowserFileTransferMode transferMode,
boolean checkConflicts,
Consumer<BrowserTransferProgress> progress) {
Consumer<BrowserTransferProgress> progress,
BooleanProperty cancelled) {
this.target = target;
this.files = files;
this.transferMode = transferMode;
this.checkConflicts = checkConflicts;
this.progress = progress;
this.cancelled = cancelled;
}
public static BrowserFileTransferOperation ofLocal(
@ -41,7 +46,8 @@ public class BrowserFileTransferOperation {
List<Path> files,
BrowserFileTransferMode transferMode,
boolean checkConflicts,
Consumer<BrowserTransferProgress> progress) {
Consumer<BrowserTransferProgress> progress,
BooleanProperty cancelled) {
var entries = files.stream()
.map(path -> {
if (!Files.exists(path)) {
@ -56,7 +62,7 @@ public class BrowserFileTransferOperation {
})
.filter(entry -> entry != null)
.toList();
return new BrowserFileTransferOperation(target, entries, transferMode, checkConflicts, progress);
return new BrowserFileTransferOperation(target, entries, transferMode, checkConflicts, progress, cancelled);
}
private void updateProgress(BrowserTransferProgress progress) {
@ -112,12 +118,18 @@ public class BrowserFileTransferOperation {
return BrowserAlerts.FileConflictChoice.REPLACE;
}
private boolean cancelled() {
return cancelled.get();
}
public void execute() throws Exception {
if (files.isEmpty()) {
updateProgress(null);
return;
}
cancelled.set(false);
var same = files.getFirst().getFileSystem().equals(target.getFileSystem());
var doesMove = transferMode == BrowserFileTransferMode.MOVE
|| (same && transferMode == BrowserFileTransferMode.NORMAL);
@ -129,6 +141,10 @@ public class BrowserFileTransferOperation {
try {
for (var file : files) {
if (cancelled()) {
break;
}
if (same) {
handleSingleOnSameFileSystem(file);
} else {
@ -138,6 +154,10 @@ public class BrowserFileTransferOperation {
if (!same && doesMove) {
for (var file : files) {
if (cancelled()) {
break;
}
deleteSingle(file);
}
}
@ -207,7 +227,7 @@ public class BrowserFileTransferOperation {
var newFile =
targetFile.getParent().join(matcher.group(1) + " (" + (number + 1) + ")." + matcher.group(3));
return newFile.toString();
} catch (NumberFormatException e) {
} catch (NumberFormatException ignored) {
}
}
@ -242,6 +262,10 @@ public class BrowserFileTransferOperation {
var baseRelative = FileNames.toDirectory(FileNames.getParent(source.getPath()));
List<FileEntry> list = source.getFileSystem().listFilesRecursively(source.getPath());
for (FileEntry fileEntry : list) {
if (cancelled()) {
return;
}
var rel = FileNames.toUnix(FileNames.relativize(baseRelative, fileEntry.getPath()));
flatFiles.put(fileEntry, rel);
if (fileEntry.getKind() == FileKind.FILE) {
@ -264,6 +288,10 @@ public class BrowserFileTransferOperation {
var start = Instant.now();
AtomicLong transferred = new AtomicLong();
for (var e : flatFiles.entrySet()) {
if (cancelled()) {
return;
}
var sourceFile = e.getKey();
var fixedRelPath = new FilePath(e.getValue())
.fileSystemCompatible(
@ -298,6 +326,10 @@ public class BrowserFileTransferOperation {
private void transfer(
FileEntry sourceFile, String targetFile, AtomicLong transferred, AtomicLong totalSize, Instant start)
throws Exception {
if (cancelled()) {
return;
}
InputStream inputStream = null;
OutputStream outputStream = null;
try {
@ -377,7 +409,7 @@ public class BrowserFileTransferOperation {
AtomicLong transferred,
AtomicLong total,
Instant start)
throws IOException {
throws Exception {
// Initialize progress immediately prior to reading anything
updateProgress(new BrowserTransferProgress(sourceFile.getName(), transferred.get(), total.get(), start));
@ -385,9 +417,48 @@ public class BrowserFileTransferOperation {
byte[] buffer = new byte[bs];
int read;
while ((read = inputStream.read(buffer, 0, bs)) > 0) {
if (cancelled()) {
killStreams();
break;
}
if (!checkTransferValidity()) {
killStreams();
break;
}
outputStream.write(buffer, 0, read);
transferred.addAndGet(read);
updateProgress(new BrowserTransferProgress(sourceFile.getName(), transferred.get(), total.get(), start));
}
}
private boolean checkTransferValidity() {
var sourceFs = files.getFirst().getFileSystem();
var targetFs = target.getFileSystem();
var same = files.getFirst().getFileSystem().equals(target.getFileSystem());
if (!same) {
var sourceShell = sourceFs.getShell().orElseThrow();
var targetShell = targetFs.getShell().orElseThrow();
return !sourceShell.getStdout().isClosed()
&& !targetShell.getStdin().isClosed();
} else {
return true;
}
}
private void killStreams() throws Exception {
var sourceFs = files.getFirst().getFileSystem();
var targetFs = target.getFileSystem();
var same = files.getFirst().getFileSystem().equals(target.getFileSystem());
if (!same) {
var sourceShell = sourceFs.getShell().orElseThrow();
var targetShell = targetFs.getShell().orElseThrow();
try {
sourceShell.closeStdout();
} finally {
targetShell.closeStdin();
}
}
}
}

View file

@ -8,10 +8,10 @@ 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.PrettySvgComp;
import io.xpipe.app.comp.base.TileButtonComp;
import io.xpipe.app.core.AppFont;
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;
@ -20,7 +20,6 @@ import io.xpipe.app.util.ThreadHelper;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
@ -52,7 +51,7 @@ public class BrowserHistoryTabComp extends SimpleComp {
var vbox = new VBox(welcome, new Spacer(4, Orientation.VERTICAL));
vbox.setAlignment(Pos.CENTER_LEFT);
var img = new PrettySvgComp(new SimpleStringProperty("graphics/Hips.svg"), 50, 75)
var img = PrettyImageHelper.ofSpecificFixedSize("graphics/Hips.svg", 50, 61)
.padding(new Insets(5, 0, 0, 0))
.createRegion();
@ -148,17 +147,20 @@ public class BrowserHistoryTabComp extends SimpleComp {
var entry = DataStorage.get().getStoreEntryIfPresent(e.getUuid());
var graphic = entry.get().getEffectiveIconFile();
var view = PrettyImageHelper.ofFixedSize(graphic, 22, 16);
return new ButtonComp(
new SimpleStringProperty(DataStorage.get().getStoreEntryDisplayName(entry.get())),
view.createRegion(),
() -> {
ThreadHelper.runAsync(() -> {
var storageEntry = DataStorage.get().getStoreEntryIfPresent(e.getUuid());
if (storageEntry.isPresent()) {
model.openFileSystemAsync(storageEntry.get().ref(), null, disable);
}
});
})
var name = Bindings.createStringBinding(
() -> {
var n = DataStorage.get().getStoreEntryDisplayName(entry.get());
return AppPrefs.get().censorMode().get() ? "*".repeat(n.length()) : n;
},
AppPrefs.get().censorMode());
return new ButtonComp(name, view.createRegion(), () -> {
ThreadHelper.runAsync(() -> {
var storageEntry = DataStorage.get().getStoreEntryIfPresent(e.getUuid());
if (storageEntry.isPresent()) {
model.openFileSystemAsync(storageEntry.get().ref(), null, disable);
}
});
})
.minWidth(300)
.accessibleText(DataStorage.get().getStoreEntryDisplayName(entry.get()))
.disable(disable)
@ -168,7 +170,13 @@ public class BrowserHistoryTabComp extends SimpleComp {
}
private Comp<?> dirButton(BrowserHistorySavedState.Entry e, BooleanProperty disable) {
return new ButtonComp(new SimpleStringProperty(e.getPath()), null, () -> {
var name = Bindings.createStringBinding(
() -> {
var n = e.getPath();
return AppPrefs.get().censorMode().get() ? "*".repeat(n.length()) : n;
},
AppPrefs.get().censorMode());
return new ButtonComp(name, () -> {
ThreadHelper.runAsync(() -> {
model.restoreStateAsync(e, disable);
});

View file

@ -7,10 +7,12 @@ import io.xpipe.app.comp.Comp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.storage.DataColor;
import javafx.beans.value.ObservableValue;
public final class BrowserHistoryTabModel extends BrowserSessionTab {
public BrowserHistoryTabModel(BrowserAbstractSessionModel<?> browserModel) {
super(browserModel, " " + AppI18n.get("history") + " ");
super(browserModel);
}
@Override
@ -24,11 +26,16 @@ public final class BrowserHistoryTabModel extends BrowserSessionTab {
}
@Override
public void init() throws Exception {}
public void init() {}
@Override
public void close() {}
@Override
public ObservableValue<String> getName() {
return AppI18n.observable("history").map(s -> " " + s + " ");
}
@Override
public String getIcon() {
return null;

View file

@ -118,8 +118,10 @@ public class BrowserNavBarComp extends Comp<BrowserNavBarComp.Structure> {
path.addListener((observable, oldValue, newValue) -> {
ThreadHelper.runFailableAsync(() -> {
BooleanScope.executeExclusive(model.getBusy(), () -> {
var changed = model.cdSyncOrRetry(newValue, true);
changed.ifPresent(s -> Platform.runLater(() -> path.set(s)));
var changed = model.cdSyncOrRetry(newValue != null && !newValue.isBlank() ? newValue : null, true);
changed.ifPresent(s -> {
Platform.runLater(() -> path.set(!s.isBlank() ? s : null));
});
});
});
});
@ -148,8 +150,6 @@ public class BrowserNavBarComp extends Comp<BrowserNavBarComp.Structure> {
INVISIBLE, !val && !struc.get().isFocused());
});
});
struc.get().setPromptText("Overview of " + model.getName());
})
.accessibleText("Current path");
return pathBar;

View file

@ -60,20 +60,20 @@ public class BrowserOverviewComp extends SimpleComp {
});
});
var commonOverview = new BrowserFileOverviewComp(model, commonPlatform, false);
var commonPane = new SimpleTitledPaneComp(AppI18n.observable("common"), commonOverview)
var commonPane = new SimpleTitledPaneComp(AppI18n.observable("common"), commonOverview, false)
.apply(struc -> VBox.setVgrow(struc.get(), Priority.NEVER));
var roots = model.getFileSystem().listRoots().stream()
.map(s -> FileEntry.ofDirectory(model.getFileSystem(), s))
.toList();
var rootsOverview = new BrowserFileOverviewComp(model, FXCollections.observableArrayList(roots), false);
var rootsPane = new SimpleTitledPaneComp(AppI18n.observable("roots"), rootsOverview);
var rootsPane = new SimpleTitledPaneComp(AppI18n.observable("roots"), rootsOverview, false);
var recent = new DerivedObservableList<>(model.getSavedState().getRecentDirectories(), true)
.mapped(s -> FileEntry.ofDirectory(model.getFileSystem(), s.getDirectory()))
.getList();
var recentOverview = new BrowserFileOverviewComp(model, recent, true);
var recentPane = new SimpleTitledPaneComp(AppI18n.observable("recent"), recentOverview);
var recentPane = new SimpleTitledPaneComp(AppI18n.observable("recent"), recentOverview, false);
var vbox = new VerticalComp(List.of(recentPane, commonPane, rootsPane)).styleClass("overview");
var r = vbox.createRegion();

View file

@ -312,7 +312,10 @@ public class BrowserQuickAccessContextMenu extends ContextMenu {
browserActionMenu.show(menu.getStyleableNode(), Side.RIGHT, 0, 0);
shownBrowserActionsMenu = browserActionMenu;
Platform.runLater(() -> {
browserActionMenu.getItems().getFirst().getStyleableNode().requestFocus();
var items = browserActionMenu.getItems();
if (items.size() > 0) {
items.getFirst().getStyleableNode().requestFocus();
}
});
}
}

View file

@ -5,10 +5,13 @@ 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.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.util.BindingsHelper;
import io.xpipe.app.util.HumanReadableFormat;
import io.xpipe.app.util.PlatformThread;
import io.xpipe.app.util.ThreadHelper;
import javafx.beans.binding.Bindings;
import javafx.geometry.Pos;
@ -36,7 +39,8 @@ public class BrowserStatusBarComp extends SimpleComp {
createProgressEstimateStatus(),
Comp.hspacer(),
createClipboardStatus(),
createSelectionStatus()));
createSelectionStatus(),
createKillButton()));
bar.spacing(15);
bar.styleClass("status-bar");
@ -50,6 +54,33 @@ public class BrowserStatusBarComp extends SimpleComp {
return r;
}
private Comp<?> createKillButton() {
var button = new IconButtonComp("mdi2s-stop", () -> {
ThreadHelper.runAsync(() -> {
model.killTransfer();
});
});
button.accessibleText("Kill").tooltipKey("killTransfer");
var cancel = PlatformThread.sync(model.getTransferCancelled());
var hide = Bindings.createBooleanBinding(
() -> {
if (model.getProgress().getValue() == null
|| model.getProgress().getValue().done()) {
return true;
}
if (cancel.getValue()) {
return true;
}
return false;
},
cancel,
model.getProgress());
button.hide(hide);
return button;
}
private Comp<?> createProgressEstimateStatus() {
var text = BindingsHelper.map(model.getProgress(), p -> {
if (p == null) {
@ -97,7 +128,7 @@ public class BrowserStatusBarComp extends SimpleComp {
var progressComp = new LabelComp(text)
.styleClass("progress")
.apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT))
.prefWidth(180);
.hgrow();
return progressComp;
}

View file

@ -17,6 +17,7 @@ import io.xpipe.app.util.ThreadHelper;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.value.ObservableBooleanValue;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
import java.util.Optional;
@ -35,7 +36,7 @@ public final class BrowserTerminalDockTabModel extends BrowserSessionTab {
BrowserAbstractSessionModel<?> browserModel,
BrowserSessionTab origin,
ObservableList<UUID> terminalRequests) {
super(browserModel, AppI18n.get("terminal"));
super(browserModel);
this.origin = origin;
this.terminalRequests = terminalRequests;
}
@ -154,6 +155,11 @@ public final class BrowserTerminalDockTabModel extends BrowserSessionTab {
dockModel.onClose();
}
@Override
public ObservableValue<String> getName() {
return AppI18n.observable("terminal");
}
@Override
public String getIcon() {
return null;

View file

@ -13,12 +13,14 @@ import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.scene.control.ContentDisplay;
import javafx.scene.image.Image;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.DragEvent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.Region;
import javafx.scene.text.TextAlignment;
import org.kordamp.ikonli.javafx.FontIcon;
@ -41,6 +43,8 @@ public class BrowserTransferComp extends SimpleComp {
var background = new LabelComp(AppI18n.observable("transferDescription"))
.apply(struc -> struc.get().setGraphic(new FontIcon("mdi2d-download-outline")))
.apply(struc -> struc.get().setWrapText(true))
.apply(struc -> struc.get().setTextAlignment(TextAlignment.CENTER))
.apply(struc -> struc.get().setContentDisplay(ContentDisplay.TOP))
.visible(model.getEmpty());
var backgroundStack = new StackComp(List.of(background))
.grow(true, true)

View file

@ -120,8 +120,8 @@ public class BrowserTransferModel {
return;
}
if (item.getOpenFileSystemModel() != null
&& item.getOpenFileSystemModel().isClosed()) {
var itemModel = item.getOpenFileSystemModel();
if (itemModel == null || itemModel.isClosed()) {
return;
}
@ -134,15 +134,16 @@ public class BrowserTransferModel {
progress -> {
// Don't update item progress to keep it as finished
if (progress == null) {
item.getOpenFileSystemModel().getProgress().setValue(null);
itemModel.getProgress().setValue(null);
return;
}
synchronized (item.getProgress()) {
item.getProgress().setValue(progress);
}
item.getOpenFileSystemModel().getProgress().setValue(progress);
});
itemModel.getProgress().setValue(progress);
},
itemModel.getTransferCancelled());
op.execute();
} catch (Throwable t) {
ErrorEvent.fromThrowable(t).handle();

View file

@ -93,6 +93,18 @@ public abstract class Comp<S extends CompStructure<?>> {
}));
}
public void focusOnShow() {
onSceneAssign(struc -> {
Platform.runLater(() -> {
Platform.runLater(() -> {
Platform.runLater(() -> {
struc.get().requestFocus();
});
});
});
});
}
public Comp<S> minWidth(double width) {
return apply(struc -> struc.get().setMinWidth(width));
}

View file

@ -2,32 +2,35 @@ 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.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;
import io.xpipe.app.util.PlatformThread;
import javafx.beans.binding.Bindings;
import javafx.beans.value.ObservableValue;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.control.ButtonBase;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class AppLayoutComp extends Comp<CompStructure<Pane>> {
public class AppLayoutComp extends Comp<AppLayoutComp.Structure> {
private final AppLayoutModel model = AppLayoutModel.get();
@Override
public CompStructure<Pane> createBase() {
public Structure createBase() {
Map<Comp<?>, ObservableValue<Boolean>> map = model.getEntries().stream()
.filter(entry -> entry.comp() != null)
.collect(Collectors.toMap(
@ -67,6 +70,28 @@ public class AppLayoutComp extends Comp<CompStructure<Pane>> {
});
AppFont.normal(pane);
pane.getStyleClass().add("layout");
return new SimpleCompStructure<>(pane);
return new Structure(pane, multiR, sidebarR, new ArrayList<>(multiR.getChildren()));
}
public record Structure(BorderPane pane, StackPane stack, Region sidebar, List<Node> children)
implements CompStructure<BorderPane> {
public void prepareAddition() {
stack.getChildren().clear();
sidebar.setDisable(true);
}
public void show() {
for (var child : children) {
stack.getChildren().add(child);
PlatformThread.runNestedLoopIteration();
}
sidebar.setDisable(false);
}
@Override
public BorderPane get() {
return pane;
}
}
}

View file

@ -0,0 +1,122 @@
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.AppProperties;
import io.xpipe.app.core.window.AppDialog;
import io.xpipe.app.core.window.AppMainWindow;
import io.xpipe.app.resources.AppImages;
import io.xpipe.app.resources.AppResources;
import io.xpipe.app.util.PlatformThread;
import io.xpipe.core.process.OsType;
import javafx.animation.Animation;
import javafx.application.Platform;
import javafx.collections.ListChangeListener;
import javafx.geometry.Pos;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.stage.Window;
import atlantafx.base.util.Animations;
public class AppMainWindowContentComp extends SimpleComp {
private final Stage stage;
public AppMainWindowContentComp(Stage stage) {
this.stage = stage;
}
@Override
protected Region createSimple() {
var overlay = AppDialog.getModalOverlay();
var loaded = AppMainWindow.getLoadedContent();
var bg = Comp.of(() -> {
var loadingIcon = new ImageView();
loadingIcon.setFitWidth(64);
loadingIcon.setFitHeight(64);
var anim = Animations.pulse(loadingIcon, 1.1);
if (OsType.getLocal() != OsType.LINUX) {
anim.setRate(0.85);
anim.setCycleCount(Animation.INDEFINITE);
anim.play();
}
// This allows for assigning logos even if AppImages has not been initialized yet
var dir = "img/logo/";
AppResources.with(AppResources.XPIPE_MODULE, dir, path -> {
loadingIcon.setImage(AppImages.loadImage(path.resolve("loading.png")));
});
var version = new LabelComp((AppProperties.get().isStaging() ? "XPipe PTB" : "XPipe") + " "
+ AppProperties.get().getVersion());
version.apply(struc -> {
AppFont.setSize(struc.get(), 1);
struc.get().setOpacity(0.6);
});
var text = new LabelComp(AppMainWindow.getLoadingText());
text.apply(struc -> {
struc.get().setOpacity(0.8);
});
var vbox = new VBox(
Comp.vspacer().createRegion(),
loadingIcon,
Comp.vspacer(19).createRegion(),
version.createRegion(),
Comp.vspacer().createRegion(),
text.createRegion(),
Comp.vspacer(20).createRegion());
vbox.setAlignment(Pos.CENTER);
var pane = new StackPane(vbox);
pane.setAlignment(Pos.CENTER);
pane.getStyleClass().add("background");
loaded.subscribe(struc -> {
if (struc != null) {
PlatformThread.runNestedLoopIteration();
struc.prepareAddition();
anim.stop();
pane.getChildren().add(struc.get());
struc.show();
pane.getChildren().remove(vbox);
pane.getStyleClass().remove("background");
}
});
overlay.addListener((ListChangeListener<? super ModalOverlay>) c -> {
if (c.next() && c.wasAdded()) {
stage.requestFocus();
// Close blocking modal windows
var childWindows = Window.getWindows().stream()
.filter(window -> window instanceof Stage s && stage.equals(s.getOwner()))
.toList();
childWindows.forEach(window -> {
((Stage) window).close();
});
}
});
loaded.addListener((observable, oldValue, newValue) -> {
if (newValue != null) {
Platform.runLater(() -> {
stage.requestFocus();
});
}
});
return pane;
});
var modal = new ModalOverlayStackComp(bg, overlay);
return modal.createRegion();
}
}

View file

@ -3,9 +3,9 @@ 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.LabelGraphic;
import io.xpipe.app.util.PlatformThread;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.css.Size;
@ -13,14 +13,16 @@ import javafx.css.SizeUnits;
import javafx.scene.Node;
import javafx.scene.control.Button;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.kordamp.ikonli.javafx.FontIcon;
@Getter
@AllArgsConstructor
public class ButtonComp extends Comp<CompStructure<Button>> {
private final ObservableValue<String> name;
private final ObjectProperty<Node> graphic;
private final ObservableValue<LabelGraphic> graphic;
private final Runnable listener;
public ButtonComp(ObservableValue<String> name, Runnable listener) {
@ -31,33 +33,39 @@ public class ButtonComp extends Comp<CompStructure<Button>> {
public ButtonComp(ObservableValue<String> name, Node graphic, Runnable listener) {
this.name = name;
this.graphic = new SimpleObjectProperty<>(graphic);
this.graphic = new SimpleObjectProperty<>(new LabelGraphic.NodeGraphic(() -> graphic));
this.listener = listener;
}
public Node getGraphic() {
return graphic.get();
}
public ObjectProperty<Node> graphicProperty() {
return graphic;
}
@Override
public CompStructure<Button> createBase() {
var button = new Button(null);
if (name != null) {
button.textProperty().bind(PlatformThread.sync(name));
}
var graphic = getGraphic();
if (graphic instanceof FontIcon f) {
// f.iconColorProperty().bind(button.textFillProperty());
button.fontProperty().subscribe(c -> {
f.setIconSize((int) new Size(c.getSize(), SizeUnits.PT).pixels());
name.subscribe(t -> {
PlatformThread.runLaterIfNeeded(() -> button.setText(t));
});
}
if (graphic != null) {
graphic.subscribe(t -> {
PlatformThread.runLaterIfNeeded(() -> {
if (t == null) {
return;
}
button.setGraphic(getGraphic());
var n = t.createGraphicNode();
button.setGraphic(n);
if (n instanceof FontIcon f && button.getFont() != null) {
f.setIconSize((int) new Size(button.getFont().getSize(), SizeUnits.PT).pixels());
}
});
});
button.fontProperty().subscribe(c -> {
if (button.getGraphic() instanceof FontIcon f) {
f.setIconSize((int) new Size(c.getSize(), SizeUnits.PT).pixels());
}
});
}
button.setOnAction(e -> getListener().run());
button.getStyleClass().add("button-comp");
return new SimpleCompStructure<>(button);

View file

@ -92,5 +92,11 @@ public class ChoicePaneComp extends Comp<CompStructure<VBox>> {
return new SimpleCompStructure<>(vbox);
}
public record Entry(ObservableValue<String> name, Comp<?> comp) {}
public record Entry(ObservableValue<String> name, Comp<?> comp) {
@Override
public int hashCode() {
return name.hashCode();
}
}
}

View file

@ -5,12 +5,14 @@ import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.SimpleCompStructure;
import io.xpipe.app.util.PlatformThread;
import javafx.application.Platform;
import javafx.beans.property.Property;
import javafx.collections.FXCollections;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.util.Callback;
import java.util.List;
@ -34,6 +36,11 @@ public class ComboTextFieldComp extends Comp<CompStructure<ComboBox<String>>> {
@Override
public CompStructure<ComboBox<String>> createBase() {
var text = new ComboBox<>(FXCollections.observableList(predefinedValues));
text.addEventFilter(KeyEvent.ANY, event -> {
Platform.runLater(() -> {
text.commitValue();
});
});
text.setEditable(true);
text.setMaxWidth(2000);
text.setValue(value.getValue() != null ? value.getValue() : null);

View file

@ -9,12 +9,12 @@ import io.xpipe.app.core.window.AppWindowHelper;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.ContextualFileReference;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStorageSyncHandler;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileSystemStore;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.control.ListCell;
@ -27,7 +27,6 @@ import org.kordamp.ikonli.javafx.FontIcon;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;
@ -42,22 +41,22 @@ public class ContextualFileReferenceChoiceComp extends Comp<CompStructure<HBox>>
private final Property<DataStoreEntryRef<? extends FileSystemStore>> fileSystem;
private final Property<String> filePath;
private final boolean allowSync;
private final ContextualFileReferenceSync sync;
private final List<PreviousFileReference> previousFileReferences;
public <T extends FileSystemStore> ContextualFileReferenceChoiceComp(
Property<DataStoreEntryRef<T>> fileSystem,
Property<String> filePath,
boolean allowSync,
ContextualFileReferenceSync sync,
List<PreviousFileReference> previousFileReferences) {
this.allowSync = allowSync;
this.sync = sync;
this.previousFileReferences = previousFileReferences;
this.fileSystem = new SimpleObjectProperty<>();
fileSystem.subscribe(val -> {
this.fileSystem.setValue(val);
});
this.fileSystem.addListener((observable, oldValue, newValue) -> {
fileSystem.setValue(newValue != null ? newValue.get().ref() : null);
fileSystem.setValue(newValue != null ? newValue.asNeeded() : null);
});
this.filePath = filePath;
}
@ -79,7 +78,7 @@ public class ContextualFileReferenceChoiceComp extends Comp<CompStructure<HBox>>
},
false);
})
.styleClass(allowSync ? Styles.CENTER_PILL : Styles.RIGHT_PILL)
.styleClass(sync != null ? Styles.CENTER_PILL : Styles.RIGHT_PILL)
.grow(false, true);
var gitShareButton = new ButtonComp(null, new FontIcon("mdi2g-git"), () -> {
@ -99,9 +98,8 @@ public class ContextualFileReferenceChoiceComp extends Comp<CompStructure<HBox>>
}
try {
var data = DataStorage.get().getDataDir();
var f = data.resolve(FileNames.getFileName(currentPath.trim()));
var source = Path.of(currentPath.trim());
var target = sync.getTargetLocation().apply(source);
if (Files.exists(source)) {
var shouldCopy = AppWindowHelper.showConfirmationAlert(
"confirmGitShareTitle", "confirmGitShareHeader", "confirmGitShareContent");
@ -109,9 +107,11 @@ public class ContextualFileReferenceChoiceComp extends Comp<CompStructure<HBox>>
return;
}
Files.copy(source, f, StandardCopyOption.REPLACE_EXISTING);
var handler = DataStorageSyncHandler.getInstance();
var syncedTarget = handler.addDataFile(
source, target, sync.getPerUser().test(source));
Platform.runLater(() -> {
filePath.setValue(f.toString());
filePath.setValue(syncedTarget.toString());
});
}
} catch (Exception e) {
@ -120,11 +120,17 @@ public class ContextualFileReferenceChoiceComp extends Comp<CompStructure<HBox>>
});
gitShareButton.tooltipKey("gitShareFileTooltip");
gitShareButton.styleClass(Styles.RIGHT_PILL).grow(false, true);
gitShareButton.disable(Bindings.createBooleanBinding(
() -> {
return filePath.getValue() != null
&& ContextualFileReference.of(filePath.getValue()).isInDataDirectory();
},
filePath));
var nodes = new ArrayList<Comp<?>>();
nodes.add(path);
nodes.add(fileBrowseButton);
if (allowSync) {
if (sync != null) {
nodes.add(gitShareButton);
}
var layout = new HorizontalComp(nodes).apply(struc -> struc.get().setFillHeight(true));
@ -139,7 +145,9 @@ public class ContextualFileReferenceChoiceComp extends Comp<CompStructure<HBox>>
}
private Comp<?> createComboBox() {
var items = previousFileReferences.stream()
var allFiles = new ArrayList<>(previousFileReferences);
allFiles.addAll(sync != null ? sync.getExistingFiles() : List.of());
var items = allFiles.stream()
.map(previousFileReference -> previousFileReference.getPath().toString())
.toList();
var combo = new ComboTextFieldComp(filePath, items, param -> {
@ -151,7 +159,7 @@ public class ContextualFileReferenceChoiceComp extends Comp<CompStructure<HBox>>
return;
}
var display = previousFileReferences.stream()
var display = allFiles.stream()
.filter(ref -> ref.path.toString().equals(item))
.findFirst()
.map(previousFileReference -> previousFileReference.getDisplayName())

View file

@ -0,0 +1,34 @@
package io.xpipe.app.comp.base;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStorageSyncHandler;
import lombok.Value;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
@Value
public class ContextualFileReferenceSync {
Path existingFilesDir;
Predicate<Path> perUser;
UnaryOperator<Path> targetLocation;
public List<ContextualFileReferenceChoiceComp.PreviousFileReference> getExistingFiles() {
var dataDir = DataStorage.get().getDataDir();
var files = new ArrayList<ContextualFileReferenceChoiceComp.PreviousFileReference>();
DataStorageSyncHandler.getInstance().getSavedDataFiles().forEach(path -> {
if (!path.startsWith(dataDir.resolve(existingFilesDir))) {
return;
}
files.add(new ContextualFileReferenceChoiceComp.PreviousFileReference(
path.getFileName().toString() + " (Git)", path));
});
return files;
}
}

View file

@ -74,7 +74,7 @@ public abstract class DialogComp extends Comp<CompStructure<Region>> {
}
protected Comp<?> finishButton() {
return new ButtonComp(AppI18n.observable(finishKey()), null, this::finish)
return new ButtonComp(AppI18n.observable(finishKey()), this::finish)
.apply(struc -> struc.get().setDefaultButton(true))
.styleClass(Styles.ACCENT)
.styleClass("next");

View file

@ -1,63 +0,0 @@
package io.xpipe.app.comp.base;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.util.PlatformThread;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.control.TextArea;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import lombok.AccessLevel;
import lombok.experimental.FieldDefaults;
import org.kordamp.ikonli.javafx.FontIcon;
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
public class ErrorOverlayComp extends SimpleComp {
Comp<?> background;
Property<String> text;
public ErrorOverlayComp(Comp<?> background, Property<String> text) {
this.background = background;
this.text = text;
}
@Override
protected Region createSimple() {
var content = new SimpleObjectProperty<ModalOverlayComp.OverlayContent>();
this.text.addListener((observable, oldValue, newValue) -> {
PlatformThread.runLaterIfNeeded(() -> {
var comp = Comp.of(() -> {
var l = new TextArea();
l.textProperty().bind(PlatformThread.sync(text));
l.setWrapText(true);
l.getStyleClass().add("error-overlay-comp");
l.setEditable(false);
return l;
});
content.set(new ModalOverlayComp.OverlayContent(
"error",
comp,
Comp.of(() -> {
var graphic = new FontIcon("mdomz-warning");
graphic.setIconColor(Color.RED);
return new StackPane(graphic);
}),
null,
() -> {},
false));
});
});
content.addListener((observable, oldValue, newValue) -> {
// Handle close
if (newValue == null) {
this.text.setValue(null);
}
});
return new ModalOverlayComp(background, content).createRegion();
}
}

View file

@ -24,7 +24,6 @@ public class FontIconComp extends Comp<FontIconComp.Structure> {
@Override
public FontIconComp.Structure createBase() {
var fi = new FontIcon();
var obs = PlatformThread.sync(icon);
icon.subscribe(val -> {
PlatformThread.runLaterIfNeeded(() -> {
fi.setIconLiteral(val);

View file

@ -0,0 +1,35 @@
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.geometry.Pos;
import atlantafx.base.layout.InputGroup;
import java.util.List;
public class InputGroupComp extends Comp<CompStructure<InputGroup>> {
private final List<Comp<?>> entries;
public InputGroupComp(List<Comp<?>> comps) {
entries = List.copyOf(comps);
}
public Comp<CompStructure<InputGroup>> spacing(double spacing) {
return apply(struc -> struc.get().setSpacing(spacing));
}
@Override
public CompStructure<InputGroup> createBase() {
InputGroup b = new InputGroup();
b.getStyleClass().add("input-group-comp");
for (var entry : entries) {
b.getChildren().add(entry.createRegion());
}
b.setAlignment(Pos.CENTER);
return new SimpleCompStructure<>(b);
}
}

View file

@ -0,0 +1,84 @@
package io.xpipe.app.comp.base;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.util.LabelGraphic;
import io.xpipe.core.process.OsType;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import atlantafx.base.theme.Styles;
import lombok.Setter;
import org.kordamp.ikonli.javafx.FontIcon;
public class IntroComp extends SimpleComp {
private final String translationsKey;
private final LabelGraphic graphic;
@Setter
private LabelGraphic buttonGraphic;
@Setter
private Runnable buttonAction;
@Setter
private boolean buttonDefault;
public IntroComp(String translationsKey, LabelGraphic graphic) {
this.translationsKey = translationsKey;
this.graphic = graphic;
}
@Override
public Region createSimple() {
var title = new Label();
title.textProperty().bind(AppI18n.observable(translationsKey + "Header"));
if (OsType.getLocal() != OsType.MACOS) {
title.getStyleClass().add(Styles.TEXT_BOLD);
}
AppFont.setSize(title, 7);
var introDesc = new Label();
introDesc.textProperty().bind(AppI18n.observable(translationsKey + "Content"));
introDesc.setWrapText(true);
introDesc.setMaxWidth(470);
var img = graphic.createGraphicNode();
if (img instanceof FontIcon fontIcon) {
fontIcon.setIconSize(80);
}
var text = new VBox(title, introDesc);
text.setSpacing(5);
text.setAlignment(Pos.CENTER_LEFT);
var hbox = new HBox(img, text);
hbox.setSpacing(55);
hbox.setAlignment(Pos.CENTER);
var button = new ButtonComp(
AppI18n.observable(translationsKey + "Button"),
buttonGraphic != null ? buttonGraphic.createGraphicNode() : null,
buttonAction);
if (buttonDefault) {
button.apply(struc -> struc.get().setDefaultButton(true));
}
var buttonPane = new StackPane(button.createRegion());
buttonPane.setAlignment(Pos.CENTER);
var v = new VBox(hbox, buttonPane);
v.setMinWidth(Region.USE_PREF_SIZE);
v.setMaxWidth(Region.USE_PREF_SIZE);
v.setMinHeight(Region.USE_PREF_SIZE);
v.setMaxHeight(Region.USE_PREF_SIZE);
v.setSpacing(20);
v.getStyleClass().add("intro");
return v;
}
}

View file

@ -0,0 +1,38 @@
package io.xpipe.app.comp.base;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.SimpleComp;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import java.util.List;
import java.util.stream.Collectors;
public class IntroListComp extends SimpleComp {
private final List<IntroComp> intros;
public IntroListComp(List<IntroComp> intros) {
this.intros = intros;
}
@Override
public Region createSimple() {
List<Comp<?>> l = intros.stream().map(introComp -> (Comp<?>) introComp).collect(Collectors.toList());
var v = new VerticalComp(l).createStructure().get();
v.setSpacing(80);
v.setMinWidth(Region.USE_PREF_SIZE);
v.setMaxWidth(Region.USE_PREF_SIZE);
v.setMinHeight(Region.USE_PREF_SIZE);
v.setMaxHeight(Region.USE_PREF_SIZE);
var sp = new StackPane(v);
sp.setPadding(new Insets(40, 0, 0, 0));
sp.setAlignment(Pos.CENTER);
sp.setPickOnBounds(false);
return sp;
}
}

View file

@ -3,23 +3,33 @@ 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.LabelGraphic;
import io.xpipe.app.util.PlatformThread;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import lombok.AllArgsConstructor;
@AllArgsConstructor
public class LabelComp extends Comp<CompStructure<Label>> {
private final ObservableValue<String> text;
private final ObservableValue<LabelGraphic> graphic;
public LabelComp(String text, LabelGraphic graphic) {
this(new SimpleStringProperty(text), new SimpleObjectProperty<>(graphic));
}
public LabelComp(String text) {
this.text = new SimpleStringProperty(text);
this(new SimpleStringProperty(text));
}
public LabelComp(ObservableValue<String> text) {
this.text = text;
this(text, new SimpleObjectProperty<>());
}
@Override
@ -28,6 +38,9 @@ public class LabelComp extends Comp<CompStructure<Label>> {
text.subscribe(t -> {
PlatformThread.runLaterIfNeeded(() -> label.setText(t));
});
graphic.subscribe(t -> {
PlatformThread.runLaterIfNeeded(() -> label.setGraphic(t != null ? t.createGraphicNode() : null));
});
label.setAlignment(Pos.CENTER);
return new SimpleCompStructure<>(label);
}

View file

@ -16,6 +16,8 @@ import javafx.scene.control.ScrollPane;
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;
@ -35,10 +37,13 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
private final int limit = Integer.MAX_VALUE;
private final boolean scrollBar;
@Setter
private int platformPauseInterval = -1;
public ListBoxViewComp(
ObservableList<T> shown, ObservableList<T> all, Function<T, Comp<?>> compFunction, boolean scrollBar) {
this.shown = PlatformThread.sync(shown);
this.all = PlatformThread.sync(all);
this.shown = shown;
this.all = all;
this.compFunction = compFunction;
this.scrollBar = scrollBar;
}
@ -95,10 +100,17 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
// 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);

View file

@ -2,8 +2,11 @@ package io.xpipe.app.comp.base;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.util.PlatformThread;
import javafx.beans.property.ListProperty;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.geometry.Orientation;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
@ -19,16 +22,17 @@ import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
@Value
@EqualsAndHashCode(callSuper = true)
public class ListSelectorComp<T> extends SimpleComp {
List<T> values;
ObservableList<T> values;
Function<T, String> toString;
ListProperty<T> selected;
Predicate<T> disable;
boolean showAllSelector;
Supplier<Boolean> showAllSelector;
@Override
protected Region createSimple() {
@ -36,7 +40,23 @@ public class ListSelectorComp<T> extends SimpleComp {
vbox.setSpacing(8);
vbox.getStyleClass().add("list-content");
var cbs = new ArrayList<CheckBox>();
for (var v : values) {
update(vbox, cbs);
values.addListener((ListChangeListener<? super T>) c -> {
PlatformThread.runLaterIfNeeded(() -> {
update(vbox, cbs);
});
});
var sp = new ScrollPane(vbox);
sp.setFitToWidth(true);
sp.getStyleClass().add("list-selector-comp");
return sp;
}
private void update(VBox vbox, List<CheckBox> cbs) {
var currentVals = new ArrayList<>(values);
vbox.getChildren().clear();
cbs.clear();
for (var v : currentVals) {
var cb = new CheckBox(null);
if (disable.test(v)) {
cb.setDisable(true);
@ -65,7 +85,7 @@ public class ListSelectorComp<T> extends SimpleComp {
vbox.getChildren().add(l);
}
if (showAllSelector) {
if (showAllSelector.get()) {
var allSelector = new CheckBox(null);
allSelector.setSelected(
values.stream().filter(t -> !disable.test(t)).count() == selected.size());
@ -85,10 +105,5 @@ public class ListSelectorComp<T> extends SimpleComp {
vbox.getChildren().add(new Separator(Orientation.HORIZONTAL));
vbox.getChildren().add(l);
}
var sp = new ScrollPane(vbox);
sp.setFitToWidth(true);
sp.getStyleClass().add("list-selector-comp");
return sp;
}
}

View file

@ -18,8 +18,6 @@ import atlantafx.base.controls.RingProgressIndicator;
public class LoadingOverlayComp extends Comp<CompStructure<StackPane>> {
private static final double FPS = 30.0;
private static final double cycleDurationSeconds = 4.0;
private final Comp<?> comp;
private final ObservableValue<Boolean> showLoading;
private final ObservableValue<Number> progress;
@ -43,11 +41,6 @@ public class LoadingOverlayComp extends Comp<CompStructure<StackPane>> {
loading.progressProperty().bind(progress);
loading.visibleProperty().bind(Bindings.not(AppPrefs.get().performanceMode()));
// var pane = new StackPane();
// Parent node = new Indicator((int) (FPS * cycleDurationSeconds), 2.0).getNode();
// pane.getChildren().add(node);
// pane.setAlignment(Pos.CENTER);
var loadingOverlay = new StackPane(loading);
loadingOverlay.getStyleClass().add("loading-comp");
loadingOverlay.setVisible(showLoading.getValue());
@ -93,6 +86,9 @@ public class LoadingOverlayComp extends Comp<CompStructure<StackPane>> {
r.heightProperty()));
loading.prefHeightProperty().bind(loading.prefWidthProperty());
stack.prefWidthProperty().bind(r.prefWidthProperty());
stack.prefHeightProperty().bind(r.prefHeightProperty());
return new SimpleCompStructure<>(stack);
}
}

View file

@ -16,6 +16,7 @@ import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Insets;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.web.WebEngine;
@ -33,15 +34,19 @@ public class MarkdownComp extends Comp<CompStructure<StackPane>> {
private final ObservableValue<String> markdown;
private final UnaryOperator<String> htmlTransformation;
private final boolean bodyPadding;
public MarkdownComp(String markdown, UnaryOperator<String> htmlTransformation) {
public MarkdownComp(String markdown, UnaryOperator<String> htmlTransformation, boolean bodyPadding) {
this.markdown = new SimpleStringProperty(markdown);
this.htmlTransformation = htmlTransformation;
this.bodyPadding = bodyPadding;
}
public MarkdownComp(ObservableValue<String> markdown, UnaryOperator<String> htmlTransformation) {
public MarkdownComp(
ObservableValue<String> markdown, UnaryOperator<String> htmlTransformation, boolean bodyPadding) {
this.markdown = markdown;
this.htmlTransformation = htmlTransformation;
this.bodyPadding = bodyPadding;
}
private static Path TEMP;
@ -55,13 +60,19 @@ public class MarkdownComp extends Comp<CompStructure<StackPane>> {
return null;
}
var hash = markdown.hashCode();
int hash;
// Rebuild files for updates in case the css have been changed
if (AppProperties.get().isImage()) {
hash = markdown.hashCode() + AppProperties.get().getVersion().hashCode();
} else {
hash = markdown.hashCode();
}
var file = TEMP.resolve("md-" + hash + ".html");
if (Files.exists(file)) {
return file;
}
var html = MarkdownHelper.toHtml(markdown, s -> s, htmlTransformation, null);
var html = MarkdownHelper.toHtml(markdown, s -> s, htmlTransformation, bodyPadding ? "padded" : null);
try {
// Workaround for https://bugs.openjdk.org/browse/JDK-8199014
FileUtils.forceMkdir(file.getParent().toFile());
@ -79,10 +90,10 @@ public class MarkdownComp extends Comp<CompStructure<StackPane>> {
var wv = new WebView();
wv.getEngine().setJavaScriptEnabled(false);
wv.setContextMenuEnabled(false);
wv.setPageFill(Color.TRANSPARENT);
wv.getEngine()
.setUserDataDirectory(
AppProperties.get().getDataDir().resolve("webview").toFile());
wv.setPageFill(Color.TRANSPARENT);
var theme = AppPrefs.get() != null
&& AppPrefs.get().theme.getValue() != null
&& AppPrefs.get().theme.getValue().isDark()
@ -99,6 +110,14 @@ public class MarkdownComp extends Comp<CompStructure<StackPane>> {
}
});
// Fix initial scrollbar size
wv.lookupAll(".scroll-bar").stream().findFirst().ifPresent(node -> {
Region region = (Region) node;
region.setMinWidth(0);
region.setPrefWidth(7);
region.setMaxWidth(7);
});
wv.getStyleClass().add("markdown-comp");
addLinkHandler(wv.getEngine());
return wv;
@ -109,7 +128,7 @@ public class MarkdownComp extends Comp<CompStructure<StackPane>> {
.stateProperty()
.addListener((observable, oldValue, newValue) -> Platform.runLater(() -> {
String toBeopen = engine.getLoadWorker().getMessage().trim().replace("Loading ", "");
if (toBeopen.contains("http://") || toBeopen.contains("https://")) {
if (toBeopen.contains("http://") || toBeopen.contains("https://") || toBeopen.contains("mailto:")) {
engine.getLoadWorker().cancel();
Hyperlinks.open(toBeopen);
}

View file

@ -40,7 +40,7 @@ public class MarkdownEditorComp extends Comp<MarkdownEditorComp.Structure> {
@Override
public Structure createBase() {
var markdown = new MarkdownComp(value, s -> s).createRegion();
var markdown = new MarkdownComp(value, s -> s, true).createRegion();
var editButton = createOpenButton();
var pane = new AnchorPane(markdown, editButton);
pane.setPickOnBounds(false);

View file

@ -0,0 +1,74 @@
package io.xpipe.app.comp.base;
import io.xpipe.app.core.mode.OperationMode;
import javafx.beans.property.Property;
import javafx.scene.control.Button;
import lombok.Value;
import lombok.experimental.NonFinal;
import java.util.function.Consumer;
@Value
public class ModalButton {
String key;
Runnable action;
boolean close;
boolean defaultButton;
public ModalButton(String key, Runnable action, boolean close, boolean defaultButton) {
this.key = key;
this.action = action;
this.close = close;
this.defaultButton = defaultButton;
}
@NonFinal
Consumer<Button> augment;
public static ModalButton finish(Runnable action) {
return new ModalButton("finish", action, true, true);
}
public static ModalButton ok(Runnable action) {
return new ModalButton("ok", action, true, true);
}
public static ModalButton ok() {
return new ModalButton("ok", null, true, true);
}
public static ModalButton cancel() {
return new ModalButton("cancel", null, true, false);
}
public static ModalButton skip() {
return new ModalButton("skip", null, true, false);
}
public static ModalButton confirm(Runnable action) {
return new ModalButton("confirm", action, true, true);
}
public static ModalButton quit() {
return new ModalButton(
"quit",
() -> {
OperationMode.halt(1);
},
true,
false);
}
public ModalButton augment(Consumer<Button> augment) {
this.augment = augment;
return this;
}
public static Runnable toggle(Property<Boolean> prop) {
return () -> {
prop.setValue(true);
};
}
}

View file

@ -0,0 +1,78 @@
package io.xpipe.app.comp.base;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.core.window.AppDialog;
import io.xpipe.app.util.LabelGraphic;
import lombok.*;
import lombok.experimental.NonFinal;
import java.util.ArrayList;
import java.util.List;
@Value
@With
@Builder(toBuilder = true)
public class ModalOverlay {
public static ModalOverlay of(Comp<?> content) {
return of(null, content, null);
}
public static ModalOverlay of(String titleKey, Comp<?> content) {
return of(titleKey, content, null);
}
public static ModalOverlay of(String titleKey, Comp<?> content, LabelGraphic graphic) {
return new ModalOverlay(titleKey, content, graphic, new ArrayList<>(), false, null);
}
public ModalOverlay withDefaultButtons(Runnable action) {
addButton(ModalButton.cancel());
addButton(ModalButton.ok(action));
return this;
}
public ModalOverlay withDefaultButtons() {
return withDefaultButtons(() -> {});
}
String titleKey;
Comp<?> content;
LabelGraphic graphic;
@Singular
List<Object> buttons;
@NonFinal
boolean persistent;
@NonFinal
@Setter
Runnable onClose;
public ModalButton addButton(ModalButton button) {
buttons.add(button);
return button;
}
public void addButtonBarComp(Comp<?> comp) {
buttons.add(comp);
}
public void persist() {
persistent = true;
}
public void show() {
AppDialog.show(this, false);
}
public void showAndWait() {
AppDialog.showAndWait(this);
}
public void close() {
AppDialog.closeDialog(this);
}
}

View file

@ -4,11 +4,18 @@ import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppLogs;
import io.xpipe.app.util.PlatformThread;
import io.xpipe.core.process.OsType;
import javafx.animation.*;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.beans.value.ObservableDoubleValue;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonBar;
import javafx.scene.control.Label;
@ -17,18 +24,19 @@ import javafx.scene.input.KeyEvent;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.util.Duration;
import atlantafx.base.controls.ModalPane;
import atlantafx.base.layout.ModalBox;
import atlantafx.base.theme.Styles;
import lombok.Value;
import atlantafx.base.util.Animations;
public class ModalOverlayComp extends SimpleComp {
private final Comp<?> background;
private final Property<OverlayContent> overlayContent;
private final Property<ModalOverlay> overlayContent;
public ModalOverlayComp(Comp<?> background, Property<OverlayContent> overlayContent) {
public ModalOverlayComp(Comp<?> background, Property<ModalOverlay> overlayContent) {
this.background = background;
this.overlayContent = overlayContent;
}
@ -37,7 +45,9 @@ public class ModalOverlayComp extends SimpleComp {
protected Region createSimple() {
var bgRegion = background.createRegion();
var modal = new ModalPane();
AppFont.small(modal);
modal.setInTransitionFactory(OsType.getLocal() == OsType.LINUX ? null : node -> fadeInDelyed(node));
modal.setOutTransitionFactory(
OsType.getLocal() == OsType.LINUX ? null : node -> Animations.fadeOut(node, Duration.millis(200)));
modal.focusedProperty().addListener((observable, oldValue, newValue) -> {
var c = modal.getContent();
if (newValue && c != null) {
@ -46,6 +56,7 @@ public class ModalOverlayComp extends SimpleComp {
});
modal.getStyleClass().add("modal-overlay-comp");
var pane = new StackPane(bgRegion, modal);
pane.setAlignment(Pos.CENTER);
pane.setPickOnBounds(false);
pane.focusedProperty().addListener((observable, oldValue, newValue) -> {
if (newValue) {
@ -57,88 +68,236 @@ public class ModalOverlayComp extends SimpleComp {
}
});
PlatformThread.sync(overlayContent).addListener((observable, oldValue, newValue) -> {
if (oldValue != null && newValue == null && modal.isDisplay()) {
modal.hide(true);
return;
modal.contentProperty().addListener((observable, oldValue, newValue) -> {
if (newValue == null) {
overlayContent.setValue(null);
bgRegion.setDisable(false);
}
if (newValue != null) {
var l = new Label(
AppI18n.get(newValue.titleKey),
newValue.graphic != null ? newValue.graphic.createRegion() : null);
l.setGraphicTextGap(6);
AppFont.normal(l);
var r = newValue.content.createRegion();
var box = new VBox(l, r);
box.focusedProperty().addListener((o, old, n) -> {
if (n) {
r.requestFocus();
}
});
box.setSpacing(10);
box.setPadding(new Insets(10, 15, 15, 15));
if (newValue.finishKey != null) {
var finishButton = new Button(AppI18n.get(newValue.finishKey));
finishButton.getStyleClass().add(Styles.ACCENT);
finishButton.setOnAction(event -> {
newValue.onFinish.run();
overlayContent.setValue(null);
event.consume();
});
var buttonBar = new ButtonBar();
buttonBar.getButtons().addAll(finishButton);
box.getChildren().add(buttonBar);
}
var modalBox = new ModalBox(box);
modalBox.setOnClose(event -> {
overlayContent.setValue(null);
modal.hide(true);
event.consume();
});
modalBox.prefWidthProperty().bind(box.widthProperty());
modalBox.prefHeightProperty().bind(box.heightProperty());
modalBox.maxWidthProperty().bind(box.widthProperty());
modalBox.maxHeightProperty().bind(box.heightProperty());
modalBox.focusedProperty().addListener((o, old, n) -> {
if (n) {
box.requestFocus();
}
});
modal.show(modalBox);
if (newValue.finishOnEnter) {
modalBox.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
if (event.getCode() == KeyCode.ENTER) {
newValue.onFinish.run();
overlayContent.setValue(null);
event.consume();
}
});
}
// Wait 2 pulses before focus so that the scene can be assigned to r
Platform.runLater(() -> {
Platform.runLater(() -> {
modalBox.requestFocus();
});
});
bgRegion.setDisable(true);
}
});
modal.displayProperty().addListener((observable, oldValue, newValue) -> {
if (!newValue) {
overlayContent.setValue(null);
bgRegion.setDisable(false);
}
});
modal.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
if (event.getCode() == KeyCode.ENTER) {
var ov = overlayContent.getValue();
if (ov != null) {
var def = ov.getButtons().stream()
.filter(modalButton -> modalButton instanceof ModalButton mb && mb.isDefaultButton())
.findFirst();
if (def.isPresent()) {
var mb = (ModalButton) def.get();
if (mb.getAction() != null) {
mb.getAction().run();
}
if (mb.isClose()) {
overlayContent.setValue(null);
}
event.consume();
}
}
}
});
overlayContent.addListener((observable, oldValue, newValue) -> {
PlatformThread.runLaterIfNeeded(() -> {
if (oldValue != null && modal.isDisplay()) {
if (newValue == null) {
modal.hide(false);
}
if (oldValue.getContent() instanceof ModalOverlayContentComp mocc) {
mocc.onClose();
}
var runnable = oldValue.getOnClose();
if (runnable != null) {
runnable.run();
}
if (oldValue.getContent() instanceof ModalOverlayContentComp mocc) {
mocc.setModalOverlay(null);
}
}
try {
if (newValue != null) {
if (newValue.getContent() instanceof ModalOverlayContentComp mocc) {
mocc.setModalOverlay(newValue);
}
showModalBox(modal, newValue);
}
} catch (Throwable t) {
AppLogs.get().logException(null, t);
Platform.runLater(() -> {
overlayContent.setValue(null);
});
}
});
});
var current = overlayContent.getValue();
if (current != null) {
showModalBox(modal, current);
}
return pane;
}
@Value
public static class OverlayContent {
private void showModalBox(ModalPane modal, ModalOverlay overlay) {
var modalBox = toBox(modal, overlay);
modal.setPersistent(overlay.isPersistent());
modal.show(modalBox);
if (overlay.isPersistent() || overlay.getTitleKey() == null) {
var closeButton = modalBox.lookup(".close-button");
if (closeButton != null) {
closeButton.setVisible(false);
}
}
}
String titleKey;
Comp<?> content;
Comp<?> graphic;
String finishKey;
Runnable onFinish;
boolean finishOnEnter;
private Region toBox(ModalPane pane, ModalOverlay newValue) {
Region r = newValue.getContent().createRegion();
var content = new VBox(r);
content.focusedProperty().addListener((o, old, n) -> {
if (n) {
r.requestFocus();
}
});
content.setSpacing(25);
content.setPadding(new Insets(13, 27, 20, 27));
if (newValue.getTitleKey() != null) {
var l = new Label(
AppI18n.get(newValue.getTitleKey()),
newValue.getGraphic() != null ? newValue.getGraphic().createGraphicNode() : null);
l.setGraphicTextGap(8);
AppFont.normal(l);
content.getChildren().addFirst(l);
} else {
content.getChildren().addFirst(Comp.vspacer(0).createRegion());
}
if (newValue.getButtons().size() > 0) {
var buttonBar = new ButtonBar();
for (var o : newValue.getButtons()) {
var node = o instanceof ModalButton mb ? toButton(mb) : ((Comp<?>) o).createRegion();
buttonBar.getButtons().add(node);
ButtonBar.setButtonUniformSize(node, o instanceof ModalButton);
if (o instanceof ModalButton) {
node.prefHeightProperty().bind(buttonBar.heightProperty());
}
}
content.getChildren().add(buttonBar);
AppFont.small(buttonBar);
}
var modalBox = new ModalBox(content) {
@Override
protected void setCloseButtonPosition() {
setTopAnchor(closeButton, 10d);
setRightAnchor(closeButton, 19d);
}
};
modalBox.setOnClose(event -> {
overlayContent.setValue(null);
event.consume();
});
r.maxHeightProperty().bind(pane.heightProperty().subtract(200));
content.prefWidthProperty().bind(modalBox.widthProperty());
modalBox.setMinWidth(100);
modalBox.setMinHeight(100);
modalBox.prefWidthProperty().bind(modalBoxWidth(pane, r));
modalBox.maxWidthProperty().bind(modalBox.prefWidthProperty());
modalBox.prefHeightProperty().bind(modalBoxHeight(pane, content));
modalBox.setMaxHeight(Region.USE_PREF_SIZE);
modalBox.focusedProperty().addListener((o, old, n) -> {
if (n) {
content.requestFocus();
}
});
if (newValue.getContent() instanceof ModalOverlayContentComp mocc) {
var busy = mocc.busy();
if (busy != null) {
var loading = LoadingOverlayComp.noProgress(Comp.of(() -> modalBox), busy);
return loading.createRegion();
}
}
return modalBox;
}
private ObservableDoubleValue modalBoxWidth(ModalPane pane, Region r) {
return Bindings.createDoubleBinding(
() -> {
var max = pane.getWidth() - 50;
if (r.getPrefWidth() != Region.USE_COMPUTED_SIZE) {
return Math.min(max, r.getPrefWidth() + 50);
}
return max;
},
pane.widthProperty(),
r.prefWidthProperty());
}
private ObservableDoubleValue modalBoxHeight(ModalPane pane, Region content) {
return Bindings.createDoubleBinding(
() -> {
var max = pane.getHeight() - 20;
if (content.getPrefHeight() != Region.USE_COMPUTED_SIZE) {
return Math.min(max, content.getPrefHeight());
}
return Math.min(max, content.getHeight());
},
pane.heightProperty(),
pane.prefHeightProperty(),
content.prefHeightProperty(),
content.heightProperty(),
content.maxHeightProperty());
}
private Button toButton(ModalButton mb) {
var button = new Button(mb.getKey() != null ? AppI18n.get(mb.getKey()) : null);
if (mb.isDefaultButton()) {
button.getStyleClass().add(Styles.ACCENT);
}
if (mb.getAugment() != null) {
mb.getAugment().accept(button);
}
button.setOnAction(event -> {
if (mb.getAction() != null) {
mb.getAction().run();
}
if (mb.isClose()) {
overlayContent.setValue(null);
}
event.consume();
});
return button;
}
private Timeline fadeInDelyed(Node node) {
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)));
t.statusProperty().addListener((obs, old, val) -> {
if (val == Animation.Status.STOPPED) {
node.setOpacity(1);
}
});
return t;
}
}

View file

@ -0,0 +1,23 @@
package io.xpipe.app.comp.base;
import io.xpipe.app.comp.SimpleComp;
import javafx.beans.value.ObservableValue;
import lombok.Getter;
public abstract class ModalOverlayContentComp extends SimpleComp {
@Getter
protected ModalOverlay modalOverlay;
void setModalOverlay(ModalOverlay modalOverlay) {
this.modalOverlay = modalOverlay;
}
protected void onClose() {}
protected ObservableValue<Boolean> busy() {
return null;
}
}

View file

@ -0,0 +1,55 @@
package io.xpipe.app.comp.base;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.SimpleComp;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.layout.Region;
import java.util.concurrent.atomic.AtomicInteger;
public class ModalOverlayStackComp extends SimpleComp {
private final Comp<?> background;
private final ObservableList<ModalOverlay> modalOverlay;
public ModalOverlayStackComp(Comp<?> background, ObservableList<ModalOverlay> modalOverlay) {
this.background = background;
this.modalOverlay = modalOverlay;
}
@Override
protected Region createSimple() {
var current = background;
for (var i = 0; i < 5; i++) {
current = buildModalOverlay(current, i);
}
return current.createRegion();
}
private Comp<?> buildModalOverlay(Comp<?> current, int index) {
AtomicInteger currentIndex = new AtomicInteger(index);
var prop = new SimpleObjectProperty<>(modalOverlay.size() > index ? modalOverlay.get(index) : null);
modalOverlay.addListener((ListChangeListener<? super ModalOverlay>) c -> {
var ex = prop.get();
// Don't shift just for an index change
if (ex != null && modalOverlay.contains(ex)) {
currentIndex.set(modalOverlay.indexOf(ex));
return;
} else {
currentIndex.set(index);
}
prop.set(modalOverlay.size() > index ? modalOverlay.get(index) : null);
});
prop.addListener((observable, oldValue, newValue) -> {
if (newValue == null && modalOverlay.indexOf(oldValue) == currentIndex.get()) {
modalOverlay.remove(oldValue);
}
});
var comp = new ModalOverlayComp(current, prop);
return comp;
}
}

View file

@ -53,7 +53,9 @@ public class OptionsComp extends Comp<CompStructure<Pane>> {
firstComp = compRegion;
}
if (entry.name() != null && entry.description() != null) {
var showVertical = (entry.name() != null
&& (entry.description() != null || entry.comp() instanceof SimpleTitledPaneComp));
if (showVertical) {
var line = new VBox();
line.prefWidthProperty().bind(pane.widthProperty());
line.setSpacing(5);
@ -70,51 +72,61 @@ public class OptionsComp extends Comp<CompStructure<Pane>> {
}
line.getChildren().add(name);
var description = new Label();
description.setWrapText(true);
description.getStyleClass().add("description");
description.textProperty().bind(entry.description());
description.setAlignment(Pos.CENTER_LEFT);
description.setMinHeight(Region.USE_PREF_SIZE);
if (compRegion != null) {
description.visibleProperty().bind(PlatformThread.sync(compRegion.visibleProperty()));
description.managedProperty().bind(PlatformThread.sync(compRegion.managedProperty()));
}
if (entry.description() != null) {
var description = new Label();
description.setWrapText(true);
description.getStyleClass().add("description");
description.textProperty().bind(entry.description());
description.setAlignment(Pos.CENTER_LEFT);
description.setMinHeight(Region.USE_PREF_SIZE);
if (compRegion != null) {
description.visibleProperty().bind(PlatformThread.sync(compRegion.visibleProperty()));
description.managedProperty().bind(PlatformThread.sync(compRegion.managedProperty()));
}
if (entry.longDescriptionSource() != null) {
var markDown = new MarkdownComp(entry.longDescriptionSource(), s -> s)
.apply(struc -> struc.get().setMaxWidth(500))
.apply(struc -> struc.get().setMaxHeight(400));
var popover = new Popover(markDown.createRegion());
popover.setCloseButtonEnabled(false);
popover.setHeaderAlwaysVisible(false);
popover.setDetachable(true);
AppFont.small(popover.getContentNode());
if (entry.longDescriptionSource() != null) {
var markDown = new MarkdownComp(entry.longDescriptionSource(), s -> s, true)
.apply(struc -> struc.get().setMaxWidth(500))
.apply(struc -> struc.get().setMaxHeight(400));
var popover = new Popover(markDown.createRegion());
popover.setCloseButtonEnabled(false);
popover.setHeaderAlwaysVisible(false);
popover.setDetachable(true);
AppFont.small(popover.getContentNode());
var extendedDescription = new Button("... ?");
extendedDescription.setMinWidth(Region.USE_PREF_SIZE);
extendedDescription.getStyleClass().add(Styles.BUTTON_OUTLINED);
extendedDescription.getStyleClass().add(Styles.ACCENT);
extendedDescription.getStyleClass().add("long-description");
extendedDescription.setAccessibleText("Help");
AppFont.normal(extendedDescription);
extendedDescription.setOnAction(e -> {
popover.show(extendedDescription);
e.consume();
});
var extendedDescription = new Button("... ?");
extendedDescription.setMinWidth(Region.USE_PREF_SIZE);
extendedDescription.getStyleClass().add(Styles.BUTTON_OUTLINED);
extendedDescription.getStyleClass().add(Styles.ACCENT);
extendedDescription.getStyleClass().add("long-description");
extendedDescription.setAccessibleText("Help");
AppFont.normal(extendedDescription);
extendedDescription.setOnAction(e -> {
popover.show(extendedDescription);
e.consume();
});
var descriptionBox = new HBox(description, new Spacer(Orientation.HORIZONTAL), extendedDescription);
descriptionBox.setSpacing(5);
HBox.setHgrow(descriptionBox, Priority.ALWAYS);
descriptionBox.setAlignment(Pos.CENTER_LEFT);
line.getChildren().add(descriptionBox);
} else {
line.getChildren().add(description);
var descriptionBox =
new HBox(description, new Spacer(Orientation.HORIZONTAL), extendedDescription);
descriptionBox.setSpacing(5);
HBox.setHgrow(descriptionBox, Priority.ALWAYS);
descriptionBox.setAlignment(Pos.CENTER_LEFT);
line.getChildren().add(descriptionBox);
if (compRegion != null) {
descriptionBox.visibleProperty().bind(PlatformThread.sync(compRegion.visibleProperty()));
descriptionBox.managedProperty().bind(PlatformThread.sync(compRegion.managedProperty()));
}
} else {
line.getChildren().add(description);
}
}
if (compRegion != null) {
compRegion.accessibleTextProperty().bind(name.textProperty());
compRegion.accessibleHelpProperty().bind(description.textProperty());
if (entry.description() != null) {
compRegion.accessibleHelpProperty().bind(PlatformThread.sync(entry.description()));
}
line.getChildren().add(compRegion);
}
@ -151,6 +163,11 @@ public class OptionsComp extends Comp<CompStructure<Pane>> {
}
}
if (entries.size() == 1 && firstComp != null) {
pane.visibleProperty().bind(PlatformThread.sync(firstComp.visibleProperty()));
pane.managedProperty().bind(PlatformThread.sync(firstComp.managedProperty()));
}
if (entries.stream().anyMatch(entry -> entry.name() != null && entry.description() == null)) {
var nameWidthBinding = Bindings.createDoubleBinding(
() -> {

View file

@ -1,17 +1,19 @@
package io.xpipe.app.comp.base;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.core.App;
import io.xpipe.app.core.window.AppMainWindow;
import io.xpipe.app.resources.AppImages;
import io.xpipe.app.util.BindingsHelper;
import io.xpipe.core.store.FileNames;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableDoubleValue;
import javafx.beans.value.ObservableValue;
import java.util.List;
import java.util.Optional;
import java.util.stream.IntStream;
public class PrettyImageHelper {
@ -33,7 +35,11 @@ public class PrettyImageHelper {
return Optional.empty();
}
private static ObservableValue<String> rasterizedImageIfExistsScaled(String img, int height) {
private static ObservableValue<String> rasterizedImageIfExistsScaled(
String img, int height, int... availableSizes) {
ObservableDoubleValue obs = AppMainWindow.getInstance() != null
? AppMainWindow.getInstance().displayScale()
: new SimpleDoubleProperty(1.0);
return Bindings.createStringBinding(
() -> {
if (img == null) {
@ -44,11 +50,11 @@ public class PrettyImageHelper {
return rasterizedImageIfExists(img, height).orElse(null);
}
var sizes = List.of(16, 24, 40, 80);
var mult = Math.round(App.getApp().displayScale().get() * height);
var mult = Math.round(obs.get() * height);
var base = FileNames.getBaseName(img);
var available = sizes.stream()
var available = IntStream.of(availableSizes)
.filter(integer -> AppImages.hasNormalImage(base + "-" + integer + ".png"))
.boxed()
.toList();
var closest = available.stream()
.filter(integer -> integer >= mult)
@ -56,7 +62,7 @@ public class PrettyImageHelper {
.orElse(available.size() > 0 ? available.getLast() : 0);
return rasterizedImageIfExists(img, closest).orElse(null);
},
App.getApp().displayScale());
obs);
}
public static Comp<?> ofFixedSizeSquare(String img, int size) {
@ -73,8 +79,13 @@ public class PrettyImageHelper {
}
var binding = BindingsHelper.flatMap(img, s -> {
return rasterizedImageIfExistsScaled(s, h);
return rasterizedImageIfExistsScaled(s, h, 16, 24, 40, 80);
});
return new PrettyImageComp(binding, w, h);
}
public static Comp<?> ofSpecificFixedSize(String img, int w, int h) {
var b = rasterizedImageIfExistsScaled(img, h, h, h * 2);
return new PrettyImageComp(b, w, h);
}
}

View file

@ -94,6 +94,7 @@ public class SecretFieldComp extends Comp<SecretFieldComp.Structure> {
.createRegion();
var ig = new InputGroup(text);
ig.setFillHeight(true);
ig.getStyleClass().add("secret-field-comp");
if (allowCopy) {
ig.getChildren().add(copyButton);

View file

@ -5,7 +5,7 @@ import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.SimpleCompStructure;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.update.UpdateAvailableAlert;
import io.xpipe.app.update.UpdateAvailableDialog;
import io.xpipe.app.update.XPipeDistributionType;
import io.xpipe.app.util.PlatformThread;
@ -119,7 +119,7 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
}
{
var b = new IconButtonComp("mdi2u-update", () -> UpdateAvailableAlert.showIfNeeded())
var b = new IconButtonComp("mdi2u-update", () -> UpdateAvailableDialog.showIfNeeded())
.tooltipKey("updateAvailableTooltip")
.accessibleTextKey("updateAvailableTooltip");
b.apply(struc -> {

View file

@ -11,19 +11,23 @@ public class SimpleTitledPaneComp extends Comp<CompStructure<TitledPane>> {
private final ObservableValue<String> name;
private final Comp<?> content;
private final boolean collapsible;
public SimpleTitledPaneComp(ObservableValue<String> name, Comp<?> content) {
public SimpleTitledPaneComp(ObservableValue<String> name, Comp<?> content, boolean collapsible) {
this.name = name;
this.content = content;
this.collapsible = collapsible;
}
@Override
public CompStructure<TitledPane> createBase() {
var tp = new TitledPane(null, content.createRegion());
var r = content.createRegion();
r.getStyleClass().add("content");
var tp = new TitledPane(null, r);
tp.textProperty().bind(name);
tp.getStyleClass().add("simple-titled-pane-comp");
tp.setExpanded(true);
tp.setCollapsible(false);
tp.setCollapsible(collapsible);
return new SimpleCompStructure<>(tp);
}
}

View file

@ -13,17 +13,15 @@ import javafx.event.ActionEvent;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.Value;
import atlantafx.base.controls.Spacer;
import lombok.*;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.function.Consumer;
@AllArgsConstructor
@Getter
public class TileButtonComp extends Comp<TileButtonComp.Structure> {
@ -32,6 +30,12 @@ public class TileButtonComp extends Comp<TileButtonComp.Structure> {
private final ObservableValue<String> icon;
private final Consumer<ActionEvent> action;
@Setter
private double iconSize = 0.55;
@Setter
private Comp<?> right;
public TileButtonComp(String nameKey, String descriptionKey, String icon, Consumer<ActionEvent> action) {
this.name = AppI18n.observable(nameKey);
this.description = AppI18n.observable(descriptionKey);
@ -39,12 +43,25 @@ public class TileButtonComp extends Comp<TileButtonComp.Structure> {
this.action = action;
}
public TileButtonComp(
ObservableValue<String> name,
ObservableValue<String> description,
ObservableValue<String> icon,
Consumer<ActionEvent> action) {
this.name = name;
this.description = description;
this.icon = icon;
this.action = action;
}
@Override
public Structure createBase() {
var bt = new Button();
bt.getStyleClass().add("tile-button-comp");
bt.setOnAction(e -> {
action.accept(e);
if (action != null) {
action.accept(e);
}
});
var header = new Label();
@ -59,6 +76,11 @@ public class TileButtonComp extends Comp<TileButtonComp.Structure> {
var fi = new FontIconComp(icon).createStructure();
var pane = fi.getPane();
var hbox = new HBox(pane, text);
Region rightRegion = right != null ? right.createRegion() : null;
if (rightRegion != null) {
hbox.getChildren().add(new Spacer());
hbox.getChildren().add(rightRegion);
}
hbox.setSpacing(8);
pane.prefWidthProperty()
.bind(Bindings.createDoubleBinding(
@ -72,7 +94,7 @@ public class TileButtonComp extends Comp<TileButtonComp.Structure> {
desc.heightProperty()));
pane.prefHeightProperty().addListener((c, o, n) -> {
var size = Math.min(n.intValue(), 100);
fi.getIcon().setIconSize((int) (size * 0.55));
fi.getIcon().setIconSize((int) (size * iconSize));
});
bt.setGraphic(hbox);
return Structure.builder()
@ -81,6 +103,7 @@ public class TileButtonComp extends Comp<TileButtonComp.Structure> {
.content(hbox)
.name(header)
.description(desc)
.right(rightRegion)
.build();
}
@ -92,6 +115,7 @@ public class TileButtonComp extends Comp<TileButtonComp.Structure> {
FontIcon graphic;
Label name;
Label description;
Region right;
@Override
public Button get() {

View file

@ -1,6 +1,8 @@
package io.xpipe.app.comp.base;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.SimpleCompStructure;
import io.xpipe.app.util.LabelGraphic;
import io.xpipe.app.util.PlatformThread;
@ -10,7 +12,6 @@ import javafx.css.PseudoClass;
import javafx.geometry.Pos;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.Region;
import atlantafx.base.controls.ToggleSwitch;
import lombok.EqualsAndHashCode;
@ -18,14 +19,14 @@ import lombok.Value;
@Value
@EqualsAndHashCode(callSuper = true)
public class ToggleSwitchComp extends SimpleComp {
public class ToggleSwitchComp extends Comp<CompStructure<ToggleSwitch>> {
Property<Boolean> selected;
ObservableValue<String> name;
ObservableValue<LabelGraphic> graphic;
@Override
protected Region createSimple() {
public CompStructure<ToggleSwitch> createBase() {
var s = new ToggleSwitch();
s.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
if (event.getCode() == KeyCode.SPACE || event.getCode() == KeyCode.ENTER) {
@ -52,6 +53,6 @@ public class ToggleSwitchComp extends SimpleComp {
.bind(PlatformThread.sync(graphic.map(labelGraphic -> labelGraphic.createGraphicNode())));
s.pseudoClassStateChanged(PseudoClass.getPseudoClass("has-graphic"), true);
}
return s;
return new SimpleCompStructure<>(s);
}
}

View file

@ -2,12 +2,9 @@ package io.xpipe.app.comp.store;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.augment.GrowAugment;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.PlatformThread;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@ -33,18 +30,9 @@ public class DenseStoreEntryComp extends StoreEntryComp {
: Comp.empty();
information.setGraphic(state.createRegion());
ObservableValue<String> info = new SimpleStringProperty();
if (getWrapper().getEntry().getProvider() != null) {
try {
info = getWrapper().getEntry().getProvider().informationString(section);
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
}
}
ObservableValue<String> finalInfo = info;
var summary = getWrapper().getSummary();
var summary = getWrapper().getShownSummary();
if (getWrapper().getEntry().getProvider() != null) {
var info = getWrapper().getShownInformation();
information
.textProperty()
.bind(PlatformThread.sync(Bindings.createStringBinding(
@ -53,10 +41,10 @@ public class DenseStoreEntryComp extends StoreEntryComp {
var p = getWrapper().getEntry().getProvider();
if (val != null && grid.isHover() && p.alwaysShowSummary()) {
return val;
} else if (finalInfo.getValue() == null && p.alwaysShowSummary()) {
} else if (info.getValue() == null && p.alwaysShowSummary()) {
return val;
} else {
return finalInfo.getValue();
return info.getValue();
}
},
grid.hoverProperty(),
@ -84,6 +72,7 @@ public class DenseStoreEntryComp extends StoreEntryComp {
},
grid.widthProperty()));
var notes = new StoreNotesComp(getWrapper()).createRegion();
var userIcon = createUserIcon().createRegion();
if (showIcon) {
var storeIcon = createIcon(28, 24);
@ -106,7 +95,7 @@ public class DenseStoreEntryComp extends StoreEntryComp {
grid.getColumnConstraints().addAll(nameCC);
var active = new StoreActiveComp(getWrapper()).createRegion();
var nameBox = new HBox(name, notes);
var nameBox = new HBox(name, userIcon, notes);
getWrapper().getSessionActive().subscribe(aBoolean -> {
if (!aBoolean) {
nameBox.getChildren().remove(active);

View file

@ -2,8 +2,6 @@ package io.xpipe.app.comp.store;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.PlatformThread;
import io.xpipe.core.process.OsType;
import javafx.geometry.HPos;
@ -25,7 +23,7 @@ public class StandardStoreEntryComp extends StoreEntryComp {
private Label createSummary() {
var summary = new Label();
summary.textProperty().bind(getWrapper().getSummary());
summary.textProperty().bind(getWrapper().getShownSummary());
summary.getStyleClass().add("summary");
AppFont.small(summary);
return summary;
@ -34,6 +32,7 @@ public class StandardStoreEntryComp extends StoreEntryComp {
protected Region createContent() {
var name = createName().createRegion();
var notes = new StoreNotesComp(getWrapper()).createRegion();
var userIcon = createUserIcon().createRegion();
var grid = new GridPane();
grid.setHgap(6);
@ -44,7 +43,7 @@ public class StandardStoreEntryComp extends StoreEntryComp {
grid.getColumnConstraints().add(new ColumnConstraints(56));
var active = new StoreActiveComp(getWrapper()).createRegion();
var nameBox = new HBox(name, notes);
var nameBox = new HBox(name, userIcon, notes);
nameBox.setSpacing(6);
nameBox.setAlignment(Pos.CENTER_LEFT);
grid.add(nameBox, 1, 0);
@ -98,16 +97,7 @@ public class StandardStoreEntryComp extends StoreEntryComp {
private Label createInformation() {
var information = new Label();
information.setGraphicTextGap(7);
if (getWrapper().getEntry().getProvider() != null) {
try {
information
.textProperty()
.bind(PlatformThread.sync(
getWrapper().getEntry().getProvider().informationString(section)));
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
}
}
information.textProperty().bind(getWrapper().getShownInformation());
information.getStyleClass().add("information");
var state = getWrapper().getEntry().getProvider() != null

View file

@ -6,15 +6,18 @@ import io.xpipe.app.comp.augment.ContextMenuAugment;
import io.xpipe.app.comp.base.*;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataColor;
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.SimpleStringProperty;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@ -47,19 +50,27 @@ public class StoreCategoryComp extends SimpleComp {
@Override
protected Region createSimple() {
var name = new LazyTextFieldComp(category.nameProperty())
.styleClass("name")
.createRegion();
var prop = new SimpleStringProperty(category.getName().getValue());
AppPrefs.get().censorMode().subscribe(aBoolean -> {
var n = category.getName().getValue();
prop.setValue(aBoolean ? "*".repeat(n.length()) : n);
});
prop.addListener((observable, oldValue, newValue) -> {
if (!AppPrefs.get().censorMode().get()) {
category.getName().setValue(newValue);
}
});
var name = new LazyTextFieldComp(prop).styleClass("name").createRegion();
var showing = new SimpleBooleanProperty();
var expandIcon = Bindings.createObjectBinding(
() -> {
var exp = category.getExpanded().get()
&& category.getChildren().size() > 0;
&& category.getChildren().getList().size() > 0;
return new LabelGraphic.IconGraphic(exp ? "mdal-keyboard_arrow_down" : "mdal-keyboard_arrow_right");
},
category.getExpanded(),
category.getChildren());
category.getChildren().getList());
var expandButton = new IconButtonComp(expandIcon, () -> {
category.toggleExpanded();
})
@ -69,7 +80,7 @@ public class StoreCategoryComp extends SimpleComp {
struc.get().setPadding(new Insets(-2, 0, 0, 0));
struc.get().setFocusTraversable(false);
})
.disable(Bindings.isEmpty(category.getChildren()))
.disable(Bindings.isEmpty(category.getChildren().getList()))
.styleClass("expand-button")
.tooltipKey("expand", new KeyCodeCombination(KeyCode.SPACE));
@ -81,7 +92,10 @@ public class StoreCategoryComp extends SimpleComp {
}
if (!DataStorage.get().supportsSharing()
|| !category.getCategory().canShare()) {
|| (!category.getCategory().canShare()
&& !category.getCategory()
.getUuid()
.equals(DataStorage.LOCAL_IDENTITIES_CATEGORY_UUID))) {
return new LabelGraphic.IconGraphic("mdi2g-git");
}
@ -93,7 +107,7 @@ public class StoreCategoryComp extends SimpleComp {
.apply(struc -> AppFont.small(struc.get()))
.apply(struc -> {
struc.get().setAlignment(Pos.CENTER);
struc.get().setPadding(new Insets(0, 0, 7, 0));
struc.get().setPadding(new Insets(0, 0, 0, 0));
struc.get().setFocusTraversable(false);
hover.bind(struc.get().hoverProperty());
})
@ -105,7 +119,8 @@ public class StoreCategoryComp extends SimpleComp {
}))
.styleClass("status-button");
var shownList = new DerivedObservableList<>(category.getAllContainedEntries(), true)
var shownList = new DerivedObservableList<>(
category.getAllContainedEntries().getList(), true)
.filtered(
storeEntryWrapper -> {
return storeEntryWrapper.matchesFilter(
@ -113,7 +128,8 @@ public class StoreCategoryComp extends SimpleComp {
},
StoreViewState.get().getFilterString())
.getList();
var count = new CountComp<>(shownList, category.getAllContainedEntries(), string -> "(" + string + ")");
var count =
new CountComp<>(shownList, category.getAllContainedEntries().getList(), string -> "(" + string + ")");
count.visible(Bindings.isNotEmpty(shownList));
var showStatus = hover.or(new SimpleBooleanProperty(DataStorage.get().supportsSharing()))
@ -134,7 +150,7 @@ public class StoreCategoryComp extends SimpleComp {
.styleClass("category-button")
.apply(struc -> hover.bind(struc.get().hoverProperty()))
.apply(struc -> focus.bind(struc.get().focusedProperty()))
.accessibleText(category.nameProperty())
.accessibleText(prop)
.grow(true, false);
categoryButton.apply(new ContextMenuAugment<>(
mouseEvent -> mouseEvent.getButton() == MouseButton.SECONDARY,
@ -150,6 +166,7 @@ public class StoreCategoryComp extends SimpleComp {
});
var l = category.getChildren()
.getList()
.sorted(Comparator.comparing(storeCategoryWrapper ->
storeCategoryWrapper.nameProperty().getValue().toLowerCase(Locale.ROOT)));
var children =
@ -159,9 +176,9 @@ public class StoreCategoryComp extends SimpleComp {
var hide = Bindings.createBooleanBinding(
() -> {
return !category.getExpanded().get()
|| category.getChildren().isEmpty();
|| category.getChildren().getList().isEmpty();
},
category.getChildren(),
category.getChildren().getList(),
category.getExpanded());
var v = new VerticalComp(List.of(categoryButton, children.hide(hide)));
v.styleClass("category");
@ -182,6 +199,13 @@ public class StoreCategoryComp extends SimpleComp {
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"));
copyId.setOnAction(event ->
ClipboardHelper.copyText(category.getCategory().getUuid().toString()));
contextMenu.getItems().add(copyId);
}
var newCategory = new MenuItem(AppI18n.get("newCategory"), new FontIcon("mdi2p-plus-thick"));
newCategory.setOnAction(event -> {
DataStorage.get()
@ -252,6 +276,7 @@ public class StoreCategoryComp extends SimpleComp {
del.setOnAction(event -> {
category.delete();
});
del.setDisable(!DataStorage.get().canDeleteStoreCategory(category.getCategory()));
contextMenu.getItems().add(del);
return contextMenu;

View file

@ -5,16 +5,19 @@ import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataColor;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreCategory;
import io.xpipe.app.util.DerivedObservableList;
import io.xpipe.app.util.PlatformThread;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.value.ObservableStringValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import lombok.Getter;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Optional;
@Getter
@ -27,11 +30,12 @@ public class StoreCategoryWrapper {
private final Property<Instant> lastAccess;
private final Property<StoreSortMode> sortMode;
private final Property<Boolean> sync;
private final ObservableList<StoreCategoryWrapper> children;
private final ObservableList<StoreEntryWrapper> directContainedEntries;
private final ObservableList<StoreEntryWrapper> allContainedEntries;
private final DerivedObservableList<StoreCategoryWrapper> children;
private final DerivedObservableList<StoreEntryWrapper> directContainedEntries;
private final DerivedObservableList<StoreEntryWrapper> allContainedEntries;
private final BooleanProperty expanded = new SimpleBooleanProperty();
private final Property<DataColor> color = new SimpleObjectProperty<>();
private StoreCategoryWrapper cachedParent;
public StoreCategoryWrapper(DataStoreCategory category) {
var d = 0;
@ -52,27 +56,54 @@ public class StoreCategoryWrapper {
this.lastAccess = new SimpleObjectProperty<>(category.getLastAccess());
this.sortMode = new SimpleObjectProperty<>(category.getSortMode());
this.sync = new SimpleObjectProperty<>(category.isSync());
this.children = FXCollections.observableArrayList();
this.allContainedEntries = FXCollections.observableArrayList();
this.directContainedEntries = FXCollections.observableArrayList();
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();
}
public ObservableStringValue getShownName() {
return Bindings.createStringBinding(
() -> {
var n = nameProperty().getValue();
return AppPrefs.get().censorMode().get() ? "*".repeat(n.length()) : n;
},
AppPrefs.get().censorMode(),
nameProperty());
}
public StoreCategoryWrapper getRoot() {
return StoreViewState.get().getCategoryWrapper(root);
}
public StoreCategoryWrapper getParent() {
return StoreViewState.get().getCategories().getList().stream()
.filter(storeCategoryWrapper ->
storeCategoryWrapper.getCategory().getUuid().equals(category.getParentCategory()))
.findAny()
.orElse(null);
if (category.getParentCategory() == null) {
return null;
}
if (cachedParent == null) {
cachedParent = StoreViewState.get().getCategories().getList().stream()
.filter(storeCategoryWrapper ->
storeCategoryWrapper.getCategory().getUuid().equals(category.getParentCategory()))
.findAny()
.orElse(null);
}
return cachedParent;
}
public boolean contains(StoreEntryWrapper entry) {
return entry.getEntry().getCategoryUuid().equals(category.getUuid()) || allContainedEntries.contains(entry);
if (entry.getCategory().getValue() == this) {
return true;
}
for (var c : children.getList()) {
if (c.contains(entry)) {
return true;
}
}
return false;
}
public void select() {
@ -82,6 +113,9 @@ public class StoreCategoryWrapper {
}
public void delete() {
for (var c : children.getList()) {
c.delete();
}
DataStorage.get().deleteStoreCategory(category);
}
@ -137,22 +171,24 @@ public class StoreCategoryWrapper {
expanded.setValue(category.isExpanded());
color.setValue(category.getColor());
directContainedEntries.setAll(StoreViewState.get().getAllEntries().getList().stream()
var allEntries = new ArrayList<>(StoreViewState.get().getAllEntries().getList());
directContainedEntries.setContent(allEntries.stream()
.filter(entry -> {
return entry.getEntry().getCategoryUuid().equals(category.getUuid());
})
.toList());
allContainedEntries.setAll(StoreViewState.get().getAllEntries().getList().stream()
allContainedEntries.setContent(allEntries.stream()
.filter(entry -> {
return entry.getEntry().getCategoryUuid().equals(category.getUuid())
|| (AppPrefs.get()
.showChildCategoriesInParentCategory()
.get()
&& children.stream()
&& children.getList().stream()
.anyMatch(storeCategoryWrapper -> storeCategoryWrapper.contains(entry)));
})
.toList());
children.setAll(StoreViewState.get().getCategories().getList().stream()
children.setContent(StoreViewState.get().getCategories().getList().stream()
.filter(storeCategoryWrapper -> getCategory()
.getUuid()
.equals(storeCategoryWrapper.getCategory().getParentCategory()))
@ -169,8 +205,17 @@ public class StoreCategoryWrapper {
if (original.equals("All scripts")) {
return AppI18n.get("allScripts");
}
if (original.equals("Predefined")) {
return AppI18n.get("predefined");
if (original.equals("All identities")) {
return AppI18n.get("allIdentities");
}
if (original.equals("Local")) {
return AppI18n.get("local");
}
if (original.equals("Synced")) {
return AppI18n.get("synced");
}
if (original.equals("Predefined") || original.equals("Samples")) {
return AppI18n.get("samples");
}
if (original.equals("Custom")) {
return AppI18n.get("custom");

View file

@ -107,7 +107,7 @@ public class StoreChoiceComp<T extends DataStore> extends SimpleComp {
},
sec -> {
if (applicable.test(sec.getWrapper())) {
selected.setValue(sec.getWrapper().getEntry().ref());
this.selected.setValue(sec.getWrapper().getEntry().ref());
popover.hide();
}
});
@ -193,9 +193,8 @@ public class StoreChoiceComp<T extends DataStore> extends SimpleComp {
var button = new ButtonComp(
Bindings.createStringBinding(
() -> {
return selected.getValue() != null
? toName(selected.getValue().getEntry())
: null;
var val = selected.getValue();
return val != null ? toName(val.get()) : null;
},
selected),
() -> {});
@ -205,7 +204,12 @@ public class StoreChoiceComp<T extends DataStore> extends SimpleComp {
Comp<?> graphic = PrettyImageHelper.ofFixedSize(
Bindings.createStringBinding(
() -> {
return selected.getValue().get().getEffectiveIconFile();
var val = selected.getValue();
if (val == null) {
return null;
}
return val.get().getEffectiveIconFile();
},
selected),
16,

View file

@ -2,17 +2,13 @@ package io.xpipe.app.comp.store;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.augment.GrowAugment;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.comp.base.DialogComp;
import io.xpipe.app.comp.base.ErrorOverlayComp;
import io.xpipe.app.comp.base.PopupMenuButtonComp;
import io.xpipe.app.comp.base.*;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.window.AppWindowHelper;
import io.xpipe.app.ext.DataStoreCreationCategory;
import io.xpipe.app.ext.DataStoreProvider;
import io.xpipe.app.ext.DataStoreProviders;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.ExceptionConverter;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
@ -31,17 +27,20 @@ import javafx.geometry.Orientation;
import javafx.scene.control.*;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
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;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.function.Predicate;
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
@ -54,7 +53,7 @@ public class StoreCreationComp extends DialogComp {
Predicate<DataStoreProvider> filter;
BooleanProperty busy = new SimpleBooleanProperty();
Property<Validator> validator = new SimpleObjectProperty<>(new SimpleValidator());
Property<String> messageProp = new SimpleStringProperty();
Property<ModalOverlay> messageProp = new SimpleObjectProperty<>();
BooleanProperty finished = new SimpleBooleanProperty();
ObservableValue<DataStoreEntry> entry;
BooleanProperty changedSinceError = new SimpleBooleanProperty();
@ -132,15 +131,16 @@ public class StoreCreationComp extends DialogComp {
.getRootCategory(DataStorage.get()
.getStoreCategoryIfPresent(targetCategory)
.orElseThrow());
// Don't put connections in the scripts category ever
// Don't put it in the wrong root category
if ((provider.getValue().getCreationCategory() == null
|| !provider.getValue()
.getCreationCategory()
.equals(DataStoreCreationCategory.SCRIPT))
&& rootCategory.equals(DataStorage.get().getAllScriptsCategory())) {
targetCategory = DataStorage.get()
.getDefaultConnectionsCategory()
.getUuid();
|| !provider.getValue()
.getCreationCategory()
.getCategory()
.equals(rootCategory.getUuid()))) {
targetCategory = provider.getValue().getCreationCategory() != null
? provider.getValue().getCreationCategory().getCategory()
: DataStorage.ALL_CONNECTIONS_CATEGORY_UUID;
}
// Don't use the all connections category
@ -151,6 +151,21 @@ public class StoreCreationComp extends DialogComp {
.getUuid();
}
// Don't use the all scripts category
if (targetCategory.equals(
DataStorage.get().getAllScriptsCategory().getUuid())) {
targetCategory = DataStorage.CUSTOM_SCRIPTS_CATEGORY_UUID;
}
// Don't use the all identities category
if (targetCategory.equals(
DataStorage.get().getAllIdentitiesCategory().getUuid())) {
targetCategory = DataStorage.LOCAL_IDENTITIES_CATEGORY_UUID;
}
// Custom category stuff
targetCategory = provider.getValue().getTargetCategory(store.getValue(), targetCategory);
return DataStoreEntry.createNew(
UUID.randomUUID(), targetCategory, name.getValue(), store.getValue());
},
@ -194,10 +209,14 @@ public class StoreCreationComp extends DialogComp {
}
public static void showCreation(DataStoreProvider selected, DataStoreCreationCategory category) {
showCreation(selected != null ? selected.defaultStore() : null, category);
showCreation(selected != null ? selected.defaultStore() : null, category, dataStoreEntry -> {}, true);
}
public static void showCreation(DataStore base, DataStoreCreationCategory category) {
public static void showCreation(
DataStore base,
DataStoreCreationCategory category,
Consumer<DataStoreEntry> listener,
boolean selectCategory) {
var prov = base != null ? DataStoreProviders.byStore(base) : null;
show(
null,
@ -207,13 +226,26 @@ public class StoreCreationComp extends DialogComp {
|| dataStoreProvider.equals(prov),
(e, validated) -> {
try {
DataStorage.get().addStoreEntryIfNotPresent(e);
var returned = DataStorage.get().addStoreEntryIfNotPresent(e);
listener.accept(returned);
if (validated
&& e.getProvider().shouldShowScan()
&& AppPrefs.get()
.openConnectionSearchWindowOnConnectionCreation()
.get()) {
ScanAlert.showAsync(e);
ScanDialog.showAsync(e);
}
if (selectCategory) {
// Select new category if needed
var cat = DataStorage.get()
.getStoreCategoryIfPresent(e.getCategoryUuid())
.orElseThrow();
PlatformThread.runLaterIfNeeded(() -> {
StoreViewState.get()
.getActiveCategory()
.setValue(StoreViewState.get().getCategoryWrapper(cat));
});
}
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
@ -262,7 +294,7 @@ public class StoreCreationComp extends DialogComp {
@Override
protected List<Comp<?>> customButtons() {
return List.of(
new ButtonComp(AppI18n.observable("skipValidation"), null, () -> {
new ButtonComp(AppI18n.observable("skipValidation"), () -> {
if (showInvalidConfirmAlert()) {
commit(false);
} else {
@ -270,7 +302,7 @@ public class StoreCreationComp extends DialogComp {
}
})
.visible(skippable),
new ButtonComp(AppI18n.observable("connect"), null, () -> {
new ButtonComp(AppI18n.observable("connect"), () -> {
var temp = DataStoreEntry.createTempWrapper(store.getValue());
var action = provider.getValue().launchAction(temp);
ThreadHelper.runFailableAsync(() -> {
@ -319,12 +351,7 @@ public class StoreCreationComp extends DialogComp {
.getFirst()
.getText();
TrackEvent.info(msg);
var newMessage = msg;
// Temporary fix for equal error message not showing up again
if (Objects.equals(newMessage, messageProp.getValue())) {
newMessage = newMessage + " ";
}
messageProp.setValue(newMessage);
messageProp.setValue(createErrorOverlay(msg));
changedSinceError.setValue(false);
return;
}
@ -340,19 +367,19 @@ public class StoreCreationComp extends DialogComp {
entry.getValue().validateOrThrow();
commit(true);
} catch (Throwable ex) {
String message;
if (ex instanceof ValidationException) {
ErrorEvent.expected(ex);
message = ex.getMessage();
} else if (ex instanceof StackOverflowError) {
// Cycles in connection graphs can fail hard but are expected
ErrorEvent.expected(ex);
message = "StackOverflowError";
} else {
message = ex.getMessage();
}
var newMessage = ExceptionConverter.convertMessage(ex);
// Temporary fix for equal error message not showing up again
if (Objects.equals(newMessage, messageProp.getValue())) {
newMessage = newMessage + " ";
}
messageProp.setValue(newMessage);
messageProp.setValue(createErrorOverlay(message));
changedSinceError.setValue(false);
ErrorEvent.fromThrowable(ex).omit().handle();
@ -370,7 +397,24 @@ public class StoreCreationComp extends DialogComp {
@Override
protected Comp<?> pane(Comp<?> content) {
var back = super.pane(content);
return new ErrorOverlayComp(back, messageProp);
return new ModalOverlayComp(back, messageProp);
}
private ModalOverlay createErrorOverlay(String message) {
var comp = Comp.of(() -> {
var l = new TextArea();
l.setText(message);
l.setWrapText(true);
l.getStyleClass().add("error-overlay-comp");
l.setEditable(false);
return l;
});
var overlay = ModalOverlay.of("error", comp, new LabelGraphic.NodeGraphic(() -> {
var graphic = new FontIcon("mdomz-warning");
graphic.setIconColor(Color.RED);
return new StackPane(graphic);
}));
return overlay;
}
@Override

View file

@ -4,7 +4,7 @@ import io.xpipe.app.comp.base.PrettyImageHelper;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.DataStoreCreationCategory;
import io.xpipe.app.ext.DataStoreProviders;
import io.xpipe.app.util.ScanAlert;
import io.xpipe.app.util.ScanDialog;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuButton;
@ -22,7 +22,7 @@ public class StoreCreationMenu {
automatically.setGraphic(new FontIcon("mdi2e-eye-plus-outline"));
automatically.textProperty().bind(AppI18n.observable("addAutomatically"));
automatically.setOnAction(event -> {
ScanAlert.showAsync(null);
ScanDialog.showAsync(null);
event.consume();
});
menu.getItems().add(automatically);
@ -37,12 +37,22 @@ public class StoreCreationMenu {
menu.getItems().add(category("addCommand", "mdi2c-code-greater-than", DataStoreCreationCategory.COMMAND, null));
menu.getItems()
.add(category("addService", "mdi2l-link-plus", DataStoreCreationCategory.SERVICE, "customService"));
menu.getItems()
.add(category(
"addTunnel", "mdi2v-vector-polyline-plus", DataStoreCreationCategory.TUNNEL, "customService"));
"addTunnel", "mdi2v-vector-polyline-plus", DataStoreCreationCategory.TUNNEL, "sshLocalTunnel"));
menu.getItems().add(category("addSerial", "mdi2s-serial-port", DataStoreCreationCategory.SERIAL, "serial"));
menu.getItems()
.add(category(
"addIdentity",
"mdi2a-account-multiple-plus",
DataStoreCreationCategory.IDENTITY,
"localIdentity"));
// menu.getItems().add(category("addDatabase", "mdi2d-database-plus", DataStoreCreationCategory.DATABASE,
// null));
}

View file

@ -97,7 +97,7 @@ public abstract class StoreEntryComp extends SimpleComp {
button.setPadding(Insets.EMPTY);
button.setMaxWidth(5000);
button.setFocusTraversable(true);
button.accessibleTextProperty().bind(getWrapper().nameProperty());
button.accessibleTextProperty().bind(getWrapper().getShownName());
button.setOnAction(event -> {
event.consume();
ThreadHelper.runFailableAsync(() -> {
@ -137,12 +137,7 @@ public abstract class StoreEntryComp extends SimpleComp {
.augment(button);
var loading = LoadingOverlayComp.noProgress(
Comp.of(() -> button),
getWrapper().getEntry().getValidity().isUsable()
? getWrapper()
.getBusy()
.or(getWrapper().getEntry().getProvider().busy(getWrapper()))
: getWrapper().getBusy());
Comp.of(() -> button), getWrapper().getEffectiveBusy());
AppFont.normal(button);
return loading.createRegion();
}
@ -169,12 +164,24 @@ public abstract class StoreEntryComp extends SimpleComp {
}
protected Comp<?> createName() {
LabelComp name = new LabelComp(getWrapper().nameProperty());
LabelComp name = new LabelComp(getWrapper().getShownName());
name.apply(struc -> struc.get().setTextOverrun(OverrunStyle.CENTER_ELLIPSIS));
name.styleClass("name");
return name;
}
protected Comp<?> createUserIcon() {
var button = new IconButtonComp("mdi2a-account");
button.styleClass("user-icon");
button.tooltipKey("personalConnection");
button.apply(struc -> {
AppFont.medium(struc.get());
struc.get().setOpacity(1.0);
});
button.hide(Bindings.not(getWrapper().getPerUser()));
return button;
}
protected Node createIcon(int w, int h) {
return new StoreIconComp(getWrapper(), w, h).createRegion();
}
@ -327,7 +334,8 @@ public abstract class StoreEntryComp extends SimpleComp {
contextMenu.getItems().add(color);
}
if (getWrapper().getEntry().getProvider() != null) {
if (getWrapper().getEntry().getProvider() != null
&& getWrapper().getEntry().getProvider().canMoveCategories()) {
var move = new Menu(AppI18n.get("moveTo"), new FontIcon("mdi2f-folder-move-outline"));
StoreViewState.get()
.getSortedCategories(getWrapper().getCategory().getValue().getRoot())

View file

@ -31,6 +31,7 @@ public class StoreEntryListComp extends SimpleComp {
return custom;
},
true);
content.setPlatformPauseInterval(50);
content.apply(struc -> {
// Reset scroll
StoreViewState.get().getActiveCategory().addListener((observable, oldValue, newValue) -> {
@ -70,6 +71,22 @@ public class StoreEntryListComp extends SimpleComp {
},
StoreViewState.get().getAllEntries().getList(),
StoreViewState.get().getActiveCategory());
var showIdentitiesIntro = Bindings.createBooleanBinding(
() -> {
var allCat = StoreViewState.get().getAllIdentitiesCategory();
var connections = StoreViewState.get().getAllEntries().getList().stream()
.filter(wrapper -> allCat.equals(
wrapper.getCategory().getValue().getRoot()))
.toList();
return 0 == connections.size()
&& StoreViewState.get()
.getActiveCategory()
.getValue()
.getRoot()
.equals(allCat);
},
StoreViewState.get().getAllEntries().getList(),
StoreViewState.get().getActiveCategory());
var showScriptsIntro = Bindings.createBooleanBinding(
() -> {
if (StoreViewState.get()
@ -123,6 +140,7 @@ public class StoreEntryListComp extends SimpleComp {
map.put(createList(), showList);
map.put(new StoreIntroComp(), showIntro);
map.put(new StoreScriptsIntroComp(scriptsIntroShowing), showScriptsIntro);
map.put(new StoreIdentitiesIntroComp(), showIdentitiesIntro);
return new MultiContentComp(map).createRegion();
}

View file

@ -54,7 +54,11 @@ public class StoreEntryListOverviewComp extends SimpleComp {
categoryWrapper -> AppI18n.observable(
categoryWrapper.getRoot().equals(StoreViewState.get().getAllConnectionsCategory())
? "connections"
: "scripts"));
: categoryWrapper
.getRoot()
.equals(StoreViewState.get().getAllScriptsCategory())
? "scripts"
: "identities"));
label.textProperty().bind(name);
label.getStyleClass().add("name");

View file

@ -3,15 +3,19 @@ package io.xpipe.app.comp.store;
import io.xpipe.app.ext.ActionProvider;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataColor;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreCategory;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.util.PlatformThread;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.SingletonSessionStore;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.value.ObservableStringValue;
import javafx.collections.FXCollections;
import lombok.Getter;
@ -46,6 +50,12 @@ public class StoreEntryWrapper {
private final Property<String> customIcon = new SimpleObjectProperty<>();
private final Property<String> iconFile = new SimpleObjectProperty<>();
private final BooleanProperty sessionActive = new SimpleBooleanProperty();
private final Property<DataStore> store = new SimpleObjectProperty<>();
private final Property<String> information = new SimpleStringProperty();
private final BooleanProperty perUser = new SimpleBooleanProperty();
private boolean effectiveBusyProviderBound = false;
private final BooleanProperty effectiveBusy = new SimpleBooleanProperty();
public StoreEntryWrapper(DataStoreEntry entry) {
this.entry = entry;
@ -86,7 +96,7 @@ public class StoreEntryWrapper {
}
public boolean isInStorage() {
return DataStorage.get().getStoreEntries().contains(entry);
return DataStorage.get() != null && DataStorage.get().getStoreEntries().contains(entry);
}
public void editDialog() {
@ -139,6 +149,30 @@ public class StoreEntryWrapper {
name.setValue(entry.getName());
}
if (effectiveBusyProviderBound && !getValidity().getValue().isUsable()) {
this.effectiveBusyProviderBound = false;
this.effectiveBusy.unbind();
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());
@ -153,17 +187,19 @@ public class StoreEntryWrapper {
notes.setValue(new StoreNotes(entry.getNotes(), entry.getNotes()));
customIcon.setValue(entry.getIcon());
iconFile.setValue(entry.getEffectiveIconFile());
busy.setValue(entry.getBusyCounter().get() != 0);
deletable.setValue(entry.getConfiguration().isDeletable());
sessionActive.setValue(entry.getStore() instanceof SingletonSessionStore<?> ss
&& entry.getStore() instanceof ShellStore
&& ss.isSessionRunning());
category.setValue(StoreViewState.get()
.getCategoryWrapper(DataStorage.get()
.getStoreCategoryIfPresent(entry.getCategoryUuid())
.orElseThrow()));
category.setValue(StoreViewState.get().getCategories().getList().stream()
.filter(storeCategoryWrapper ->
storeCategoryWrapper.getCategory().getUuid().equals(entry.getCategoryUuid()))
.findFirst()
.orElse(StoreViewState.get().getAllConnectionsCategory()));
perUser.setValue(
!category.getValue().getRoot().equals(StoreViewState.get().getAllIdentitiesCategory())
&& entry.isPerUserStore());
if (!entry.getValidity().isUsable()) {
summary.setValue(null);
@ -207,6 +243,16 @@ public class StoreEntryWrapper {
ErrorEvent.fromThrowable(ex).handle();
}
}
if (!effectiveBusyProviderBound && getValidity().getValue().isUsable()) {
this.effectiveBusyProviderBound = true;
this.effectiveBusy.unbind();
this.effectiveBusy.bind(busy.or(getEntry().getProvider().busy(this)));
}
if (!this.effectiveBusy.isBound() && !getValidity().getValue().isUsable()) {
this.effectiveBusy.bind(busy);
}
}
public boolean showActionProvider(ActionProvider p) {
@ -296,4 +342,34 @@ public class StoreEntryWrapper {
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);
}
}

View file

@ -1,7 +1,7 @@
package io.xpipe.app.comp.store;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.base.PrettyImageHelper;
import io.xpipe.app.comp.base.*;
import io.xpipe.app.resources.SystemIcon;
import javafx.beans.property.Property;

View file

@ -0,0 +1,69 @@
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.storage.DataStoreEntry;
import io.xpipe.app.util.Hyperlinks;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import lombok.Getter;
import org.kordamp.ikonli.javafx.FontIcon;
public class StoreIconChoiceDialog {
public static void show(DataStoreEntry entry) {
var dialog = new StoreIconChoiceDialog(entry);
dialog.getOverlay().show();
}
private final ObjectProperty<SystemIcon> selected = new SimpleObjectProperty<>();
private final DataStoreEntry entry;
@Getter
private final ModalOverlay overlay;
public StoreIconChoiceDialog(DataStoreEntry entry) {
this.entry = entry;
this.overlay = createOverlay();
}
private ModalOverlay createOverlay() {
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);
})
.grow(false, true);
var modal = ModalOverlay.of(
"chooseCustomIcon",
new StoreIconChoiceComp(selected, SystemIcons.getSystemIcons(), 5, filterText, () -> {
finish();
})
.prefWidth(600));
modal.addButtonBarComp(github);
modal.addButtonBarComp(filter);
modal.addButton(new ModalButton(
"clear",
() -> {
selected.setValue(null);
finish();
},
true,
false));
modal.addButton(ModalButton.ok(() -> {
finish();
}))
.augment(button -> button.disableProperty().bind(selected.isNull()));
return modal;
}
private void finish() {
entry.setIcon(selected.get() != null ? selected.getValue().getIconName() : null, true);
overlay.close();
}
}

View file

@ -1,99 +0,0 @@
package io.xpipe.app.comp.store;
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.DialogComp;
import io.xpipe.app.comp.base.FilterComp;
import io.xpipe.app.comp.base.HorizontalComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.window.AppWindowHelper;
import io.xpipe.app.resources.SystemIcon;
import io.xpipe.app.resources.SystemIcons;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.util.Hyperlinks;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.scene.layout.Region;
import javafx.stage.Modality;
import javafx.stage.Stage;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
public class StoreIconChoiceDialogComp extends SimpleComp {
public static void show(DataStoreEntry entry) {
var window = AppWindowHelper.sideWindow(
AppI18n.get("chooseCustomIcon"), stage -> new StoreIconChoiceDialogComp(entry, stage), false, null);
window.initModality(Modality.APPLICATION_MODAL);
window.show();
}
private final ObjectProperty<SystemIcon> selected = new SimpleObjectProperty<>();
private final DataStoreEntry entry;
private final Stage dialogStage;
public StoreIconChoiceDialogComp(DataStoreEntry entry, Stage dialogStage) {
this.entry = entry;
this.dialogStage = dialogStage;
}
@Override
protected Region createSimple() {
var filterText = new SimpleStringProperty();
var filter = new FilterComp(filterText).apply(struc -> {
dialogStage.setOnShowing(event -> {
struc.get().requestFocus();
event.consume();
});
});
var github = new ButtonComp(null, new FontIcon("mdi2g-github"), () -> {
Hyperlinks.open(Hyperlinks.SELFHST_ICONS);
})
.grow(false, true);
var dialog = new DialogComp() {
@Override
protected void finish() {
entry.setIcon(selected.get() != null ? selected.getValue().getIconName() : null, true);
dialogStage.close();
}
@Override
protected void discard() {}
@Override
public Comp<?> content() {
return new StoreIconChoiceComp(selected, SystemIcons.getSystemIcons(), 5, filterText, () -> {
finish();
});
}
@Override
protected Comp<?> pane(Comp<?> content) {
return content;
}
@Override
public Comp<?> bottom() {
var clear = new ButtonComp(AppI18n.observable("clear"), () -> {
selected.setValue(null);
finish();
})
.grow(false, true);
return new HorizontalComp(List.of(github, filter.hgrow(), clear)).spacing(10);
}
@Override
protected Comp<?> finishButton() {
return super.finishButton().disable(selected.isNull());
}
};
dialog.prefWidth(600);
dialog.prefHeight(600);
return dialog.createRegion();
}
}

View file

@ -54,7 +54,7 @@ public class StoreIconComp extends SimpleComp {
stack.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
if (event.getButton() == MouseButton.PRIMARY) {
StoreIconChoiceDialogComp.show(wrapper.getEntry());
StoreIconChoiceDialog.show(wrapper.getEntry());
event.consume();
}
});

View file

@ -0,0 +1,136 @@
package io.xpipe.app.comp.store;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.DataStoreCreationCategory;
import io.xpipe.app.ext.DataStoreProviders;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.core.process.OsType;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import atlantafx.base.theme.Styles;
import org.kordamp.ikonli.javafx.FontIcon;
public class StoreIdentitiesIntroComp extends SimpleComp {
private Region createIntro() {
var title = new Label();
title.textProperty().bind(AppI18n.observable("identitiesIntroTitle"));
if (OsType.getLocal() != OsType.MACOS) {
title.getStyleClass().add(Styles.TEXT_BOLD);
}
AppFont.setSize(title, 7);
var introDesc = new Label();
introDesc.textProperty().bind(AppI18n.observable("identitiesIntroText"));
introDesc.setWrapText(true);
introDesc.setMaxWidth(470);
var img = new FontIcon("mdi2a-account-group");
img.setIconSize(80);
var text = new VBox(title, introDesc);
text.setSpacing(5);
text.setAlignment(Pos.CENTER_LEFT);
var hbox = new HBox(img, text);
hbox.setSpacing(55);
hbox.setAlignment(Pos.CENTER);
var addButton = new Button(null, new FontIcon("mdi2p-play-circle"));
addButton.textProperty().bind(AppI18n.observable("createIdentity"));
addButton.setOnAction(event -> {
var canSync = DataStorage.get().supportsSharing();
var prov = canSync
? DataStoreProviders.byName("syncedIdentity").orElseThrow()
: DataStoreProviders.byName("localIdentity").orElseThrow();
StoreCreationComp.showCreation(prov, DataStoreCreationCategory.IDENTITY);
event.consume();
});
var addPane = new StackPane(addButton);
addPane.setAlignment(Pos.CENTER);
var v = new VBox(hbox, addPane);
v.setMinWidth(Region.USE_PREF_SIZE);
v.setMaxWidth(Region.USE_PREF_SIZE);
v.setMinHeight(Region.USE_PREF_SIZE);
v.setMaxHeight(Region.USE_PREF_SIZE);
v.setSpacing(20);
v.getStyleClass().add("intro");
return v;
}
private Region createBottom() {
var title = new Label();
title.textProperty().bind(AppI18n.observable("identitiesIntroBottomTitle"));
if (OsType.getLocal() != OsType.MACOS) {
title.getStyleClass().add(Styles.TEXT_BOLD);
}
AppFont.setSize(title, 7);
var importDesc = new Label();
importDesc.textProperty().bind(AppI18n.observable("identitiesIntroBottomText"));
importDesc.setWrapText(true);
importDesc.setMaxWidth(470);
var syncButton = new Button(null, new FontIcon("mdi2p-play-circle"));
syncButton.textProperty().bind(AppI18n.observable("setupSync"));
syncButton.setOnAction(event -> {
AppPrefs.get().selectCategory("sync");
event.consume();
});
var syncPane = new StackPane(syncButton);
syncPane.setAlignment(Pos.CENTER);
var fi = new FontIcon("mdi2g-git");
fi.setIconSize(80);
var img = new StackPane(fi);
img.setPrefWidth(100);
img.setPrefHeight(150);
var text = new VBox(title, importDesc);
text.setSpacing(5);
text.setAlignment(Pos.CENTER_LEFT);
var hbox = new HBox(img, text);
hbox.setSpacing(35);
hbox.setAlignment(Pos.CENTER);
var v = new VBox(hbox, syncPane);
v.setMinWidth(Region.USE_PREF_SIZE);
v.setMaxWidth(Region.USE_PREF_SIZE);
v.setMinHeight(Region.USE_PREF_SIZE);
v.setMaxHeight(Region.USE_PREF_SIZE);
v.setSpacing(20);
v.getStyleClass().add("intro");
return v;
}
@Override
public Region createSimple() {
var intro = createIntro();
var introImport = createBottom();
var v = new VBox(intro, introImport);
v.setSpacing(80);
v.setMinWidth(Region.USE_PREF_SIZE);
v.setMaxWidth(Region.USE_PREF_SIZE);
v.setMinHeight(Region.USE_PREF_SIZE);
v.setMaxHeight(Region.USE_PREF_SIZE);
var sp = new StackPane(v);
sp.setPadding(new Insets(40, 0, 0, 0));
sp.setAlignment(Pos.CENTER);
sp.setPickOnBounds(false);
return sp;
}
}

View file

@ -1,15 +1,14 @@
package io.xpipe.app.comp.store;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.base.PrettySvgComp;
import io.xpipe.app.comp.base.PrettyImageHelper;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.ScanAlert;
import io.xpipe.app.util.ScanDialog;
import io.xpipe.core.process.OsType;
import javafx.beans.property.SimpleStringProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
@ -39,12 +38,13 @@ public class StoreIntroComp extends SimpleComp {
var scanButton = new Button(null, new FontIcon("mdi2m-magnify"));
scanButton.textProperty().bind(AppI18n.observable("detectConnections"));
scanButton.setOnAction(event -> ScanAlert.showAsync(DataStorage.get().local()));
scanButton.setOnAction(event -> ScanDialog.showAsync(DataStorage.get().local()));
scanButton.setDefaultButton(true);
var scanPane = new StackPane(scanButton);
scanPane.setAlignment(Pos.CENTER);
var img = new PrettySvgComp(new SimpleStringProperty("graphics/Wave.svg"), 80, 150).createRegion();
var img = PrettyImageHelper.ofSpecificFixedSize("graphics/Wave.svg", 80, 144)
.createRegion();
var text = new VBox(title, introDesc);
text.setSpacing(5);
text.setAlignment(Pos.CENTER_LEFT);

View file

@ -43,8 +43,8 @@ public class StoreQuickAccessButtonComp extends Comp<CompStructure<Button>> {
var w = section.getWrapper();
var graphic = w.getEntry().getEffectiveIconFile();
if (c.getList().isEmpty()) {
var item = ContextMenuHelper.item(
new LabelGraphic.ImageGraphic(graphic, 16), w.getName().getValue());
var item = new MenuItem(
w.getName().getValue(), new LabelGraphic.ImageGraphic(graphic, 16).createGraphicNode());
item.setOnAction(event -> {
action.accept(section);
contextMenu.hide();

View file

@ -139,14 +139,13 @@ public class StoreSectionComp extends Comp<CompStructure<VBox>> {
section.getWrapper().getExpanded(),
section.getAllChildren().getList());
var content = new ListBoxViewComp<>(
listSections.getList(),
section.getAllChildren().getList(),
(StoreSection e) -> {
return StoreSection.customSection(e, false).apply(GrowAugment.create(true, false));
},
false)
.minHeight(0)
.hgrow();
listSections.getList(),
section.getAllChildren().getList(),
(StoreSection e) -> {
return StoreSection.customSection(e, false).apply(GrowAugment.create(true, false));
},
false);
content.minHeight(0).hgrow();
var expanded = Bindings.createBooleanBinding(
() -> {
@ -192,6 +191,10 @@ public class StoreSectionComp extends Comp<CompStructure<VBox>> {
}
struc.get().getStyleClass().setAll(newList);
});
section.getWrapper().getPerUser().subscribe(val -> {
struc.get().pseudoClassStateChanged(PseudoClass.getPseudoClass("per-user"), val);
});
})
.createStructure();
}

View file

@ -46,7 +46,7 @@ public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
var list = new ArrayList<Comp<?>>();
BooleanProperty expanded;
if (section.getWrapper() != null) {
var root = new ButtonComp(section.getWrapper().nameProperty(), () -> {})
var root = new ButtonComp(section.getWrapper().getShownName(), () -> {})
.apply(struc -> {
struc.get()
.setGraphic(PrettyImageHelper.ofFixedSize(

View file

@ -25,6 +25,10 @@ public class StoreSidebarComp extends SimpleComp {
.styleClass("color-box")
.styleClass("gray")
.styleClass("bar"),
new StoreCategoryListComp(StoreViewState.get().getAllIdentitiesCategory())
.styleClass("color-box")
.styleClass("gray")
.styleClass("bar"),
Comp.of(() -> new Region())
.styleClass("color-box")
.styleClass("gray")

View file

@ -76,8 +76,8 @@ public class StoreToggleComp extends SimpleComp {
var val = new SimpleBooleanProperty();
ObservableValue<LabelGraphic> g = graphic
? val.map(aBoolean -> aBoolean
? new LabelGraphic.IconGraphic("mdi2c-circle-slice-8")
: new LabelGraphic.IconGraphic("mdi2c-circle-half-full"))
? new LabelGraphic.IconGraphic("mdi2e-eye-plus")
: new LabelGraphic.IconGraphic("mdi2e-eye-minus"))
: null;
var t = new StoreToggleComp(
nameKey,
@ -91,7 +91,7 @@ public class StoreToggleComp extends SimpleComp {
StoreViewState.get().toggleStoreListUpdate();
});
});
t.tooltipKey("showAllChildren");
t.tooltipKey("showNonRunningChildren");
t.value.subscribe((newValue) -> {
val.set(newValue);
});

View file

@ -55,6 +55,7 @@ public class StoreViewState {
INSTANCE = new StoreViewState();
INSTANCE.updateContent();
INSTANCE.initSections();
INSTANCE.updateContent();
INSTANCE.initFilterJump();
}
@ -101,7 +102,7 @@ public class StoreViewState {
var matchingCats = categories.getList().stream()
.filter(storeCategoryWrapper ->
storeCategoryWrapper.getRoot().equals(all))
.filter(storeCategoryWrapper -> storeCategoryWrapper.getDirectContainedEntries().stream()
.filter(storeCategoryWrapper -> storeCategoryWrapper.getDirectContainedEntries().getList().stream()
.anyMatch(wrapper -> wrapper.matchesFilter(newValue)))
.toList();
if (matchingCats.size() == 1) {
@ -239,13 +240,13 @@ public class StoreViewState {
@Override
public void onCategoryAdd(DataStoreCategory category) {
var l = new StoreCategoryWrapper(category);
l.update();
Platform.runLater(() -> {
// Don't update anything if we have already reset
if (INSTANCE == null) {
return;
}
l.update();
synchronized (this) {
categories.getList().add(l);
}
@ -284,21 +285,27 @@ public class StoreViewState {
@Override
public void onEntryCategoryChange(DataStoreCategory from, DataStoreCategory to) {
synchronized (this) {
categories.getList().forEach(storeCategoryWrapper -> storeCategoryWrapper.update());
}
Platform.runLater(() -> {
synchronized (this) {
categories.getList().forEach(storeCategoryWrapper -> storeCategoryWrapper.update());
}
});
}
});
}
public Optional<StoreSection> getParentSectionForWrapper(StoreEntryWrapper wrapper) {
public Optional<StoreSection> getSectionForWrapper(StoreEntryWrapper wrapper) {
if (currentTopLevelSection == null) {
return Optional.empty();
}
StoreSection current = getCurrentTopLevelSection();
while (true) {
var child = current.getAllChildren().getList().stream()
.filter(section -> section.getWrapper().equals(wrapper))
.findFirst();
if (child.isPresent()) {
return Optional.of(current);
return child;
}
var traverse = current.getAllChildren().getList().stream()
@ -325,35 +332,37 @@ public class StoreViewState {
return 1;
}
if (o1.getParent() == null && o2.getParent() == null) {
var p1 = o1.getParent();
var p2 = o2.getParent();
if (p1 == null && p2 == null) {
return 0;
}
if (o1.getParent() == null) {
if (p1 == null) {
return -1;
}
if (o2.getParent() == null) {
if (p2 == null) {
return 1;
}
if (o1.getDepth() > o2.getDepth()) {
if (o1.getParent() == o2) {
if (p1 == o2) {
return 1;
}
return compare(o1.getParent(), o2);
return compare(p1, o2);
}
if (o1.getDepth() < o2.getDepth()) {
if (o2.getParent() == o1) {
if (p2 == o1) {
return -1;
}
return compare(o1, o2.getParent());
return compare(o1, p2);
}
var parent = compare(o1.getParent(), o2.getParent());
var parent = compare(p1, p2);
if (parent != 0) {
return parent;
}
@ -384,6 +393,14 @@ public class StoreViewState {
.orElseThrow();
}
public StoreCategoryWrapper getAllIdentitiesCategory() {
return categories.getList().stream()
.filter(storeCategoryWrapper ->
storeCategoryWrapper.getCategory().getUuid().equals(DataStorage.ALL_IDENTITIES_CATEGORY_UUID))
.findFirst()
.orElseThrow();
}
public StoreEntryWrapper getEntryWrapper(DataStoreEntry entry) {
return allEntries.getList().stream()
.filter(storeCategoryWrapper -> storeCategoryWrapper.getEntry().equals(entry))

View file

@ -1,17 +1,8 @@
package io.xpipe.app.core;
import io.xpipe.app.comp.base.AppLayoutComp;
import io.xpipe.app.core.window.AppMainWindow;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.update.XPipeDistributionType;
import io.xpipe.app.util.LicenseProvider;
import io.xpipe.app.util.PlatformThread;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.value.ObservableDoubleValue;
import javafx.stage.Stage;
import lombok.Getter;
@ -34,43 +25,4 @@ public class App extends Application {
APP = this;
stage = primaryStage;
}
public void setupWindow() {
var content = new AppLayoutComp();
var t = LicenseProvider.get().licenseTitle();
var u = XPipeDistributionType.get().getUpdateHandler().getPreparedUpdate();
var titleBinding = Bindings.createStringBinding(
() -> {
var base = String.format(
"XPipe %s (%s)", t.getValue(), AppProperties.get().getVersion());
var prefix = AppProperties.get().isStaging() ? "[Public Test Build, Not a proper release] " : "";
var suffix = u.getValue() != null
? " " + AppI18n.get("updateReadyTitle", u.getValue().getVersion())
: "";
return prefix + base + suffix;
},
u,
t,
AppPrefs.get().language());
var appWindow = AppMainWindow.init(stage);
appWindow.getStage().titleProperty().bind(PlatformThread.sync(titleBinding));
appWindow.initialize();
appWindow.setContent(content);
TrackEvent.info("Application window initialized");
}
public void focus() {
PlatformThread.runLaterIfNeeded(() -> {
stage.requestFocus();
});
}
public ObservableDoubleValue displayScale() {
if (getStage() == null) {
return new SimpleDoubleProperty(1.0);
}
return getStage().outputScaleXProperty();
}
}

View file

@ -1,9 +1,7 @@
package io.xpipe.app.core;
import io.xpipe.app.core.launcher.LauncherInput;
import io.xpipe.app.core.window.AppWindowHelper;
import io.xpipe.app.core.window.AppDialog;
import javafx.scene.control.Alert;
import javafx.scene.input.Clipboard;
import javafx.scene.input.DataFormat;
@ -26,7 +24,7 @@ public class AppActionLinkDetector {
}
public static void handle(String content, boolean showAlert) {
var detected = LauncherInput.of(content);
var detected = AppOpenArguments.parseActions(content);
if (detected.size() == 0) {
return;
}
@ -35,7 +33,7 @@ public class AppActionLinkDetector {
return;
}
LauncherInput.handle(List.of(content));
AppOpenArguments.handle(List.of(content));
}
public static void detectOnFocus() {
@ -61,15 +59,6 @@ public class AppActionLinkDetector {
}
private static boolean showAlert() {
return AppWindowHelper.showBlockingAlert(alert -> {
alert.setAlertType(Alert.AlertType.CONFIRMATION);
alert.setTitle(AppI18n.get("clipboardActionDetectedTitle"));
alert.setHeaderText(AppI18n.get("clipboardActionDetectedHeader"));
alert.getDialogPane()
.setContent(
AppWindowHelper.alertContentText(AppI18n.get("clipboardActionDetectedContent")));
})
.map(buttonType -> buttonType.getButtonData().isDefaultButton())
.orElse(false);
return AppDialog.confirm("clipboardActionDetected");
}
}

View file

@ -0,0 +1,114 @@
package io.xpipe.app.core;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.LogErrorHandler;
import io.xpipe.core.util.XPipeDaemonMode;
import lombok.Value;
import picocli.CommandLine;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.regex.Pattern;
@Value
public class AppArguments {
List<String> rawArgs;
List<String> resolvedArgs;
XPipeDaemonMode modeArg;
List<String> openArgs;
private static final Pattern PROPERTY_PATTERN = Pattern.compile("^-[DP](.+)=(.+)$");
public static AppArguments init(String[] args) {
var rawArgs = Arrays.asList(args);
var resolvedArgs = Arrays.asList(parseProperties(args));
var command = LauncherCommand.resolveLauncher(resolvedArgs.toArray(String[]::new));
return new AppArguments(rawArgs, resolvedArgs, command.mode, command.inputs);
}
private static String[] parseProperties(String[] args) {
List<String> newArgs = new ArrayList<>();
for (var a : args) {
var m = PROPERTY_PATTERN.matcher(a);
if (m.matches()) {
var k = m.group(1);
var v = m.group(2);
System.setProperty(k, v);
} else {
newArgs.add(a);
}
}
return newArgs.toArray(String[]::new);
}
public static class ModeConverter implements CommandLine.ITypeConverter<XPipeDaemonMode> {
@Override
public XPipeDaemonMode convert(String value) {
return XPipeDaemonMode.get(value);
}
}
@CommandLine.Command()
public static class LauncherCommand implements Callable<Integer> {
@CommandLine.Parameters(paramLabel = "<input>")
final List<String> inputs = List.of();
@CommandLine.Option(
names = {"--mode"},
description = "The mode to launch the daemon in or switch too",
paramLabel = "<mode id>",
converter = ModeConverter.class)
XPipeDaemonMode mode;
public static LauncherCommand resolveLauncher(String[] args) {
var cmd = new CommandLine(new LauncherCommand());
cmd.setExecutionExceptionHandler((ex, commandLine, parseResult) -> {
var event = ErrorEvent.fromThrowable(ex).term().build();
// Print error in case we launched from the command-line
new LogErrorHandler().handle(event);
event.handle();
return 1;
});
cmd.setParameterExceptionHandler((ex, args1) -> {
var event = ErrorEvent.fromThrowable(ex).term().expected().build();
// Print error in case we launched from the command-line
new LogErrorHandler().handle(event);
event.handle();
return 1;
});
if (AppLogs.get() != null) {
// Use original output streams for command output
cmd.setOut(new PrintWriter(AppLogs.get().getOriginalSysOut()));
cmd.setErr(new PrintWriter(AppLogs.get().getOriginalSysErr()));
}
try {
cmd.parseArgs(args);
} catch (Throwable t) {
// Fix serialization issues with exception class
var converted = t instanceof CommandLine.UnmatchedArgumentException u
? new IllegalArgumentException(u.getMessage())
: t;
var e = ErrorEvent.fromThrowable(converted).term().build();
// Print error in case we launched from the command-line
new LogErrorHandler().handle(e);
e.handle();
}
return cmd.getCommand();
}
@Override
public Integer call() {
return 0;
}
}
}

View file

@ -1,7 +1,6 @@
package io.xpipe.app.core;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.JsonConfigHelper;
import io.xpipe.core.util.JacksonMapper;
import com.fasterxml.jackson.databind.ObjectMapper;
@ -100,8 +99,7 @@ public class AppCache {
try {
FileUtils.forceMkdirParent(path.toFile());
var tree = JacksonMapper.getDefault().valueToTree(val);
JsonConfigHelper.writeConfig(path, tree);
JacksonMapper.getDefault().writeValue(path.toFile(), val);
} catch (Exception e) {
ErrorEvent.fromThrowable("Could not write cache data for key " + key, e)
.omitted(true)

View file

@ -1,10 +1,10 @@
package io.xpipe.app.core;
import io.xpipe.app.Main;
import io.xpipe.app.core.launcher.LauncherInput;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorageUserHandler;
import io.xpipe.app.util.PlatformState;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.process.OsType;
@ -16,13 +16,7 @@ import javax.imageio.ImageIO;
public class AppDesktopIntegration {
public static void setupDesktopIntegrations() {
// Check if we were/are able to initialize the platform
// If not, we don't have to attempt the awt setup as well
if (!PlatformState.initPlatformIfNeeded()) {
return;
}
public static void init() {
try {
if (Desktop.isDesktopSupported()) {
Desktop.getDesktop().addAppEventListener(new SystemSleepListener() {
@ -31,10 +25,11 @@ public class AppDesktopIntegration {
@Override
public void systemAwoke(SystemSleepEvent e) {
var handler = DataStorageUserHandler.getInstance();
if (AppPrefs.get() != null
&& AppPrefs.get().lockVaultOnHibernation().get()
&& AppPrefs.get().getLockCrypt().get() != null
&& !AppPrefs.get().getLockCrypt().get().isBlank()) {
&& handler != null
&& handler.getActiveUser() != null) {
// If we run this at the same time as the system is sleeping, there might be exceptions
// because the platform does not like being shut down while sleeping
// This assures that it will be run later, on system wake
@ -62,7 +57,7 @@ public class AppDesktopIntegration {
// URL open operations have to be handled in a special way on macOS!
Desktop.getDesktop().setOpenURIHandler(e -> {
LauncherInput.handle(List.of(e.getURI().toString()));
AppOpenArguments.handle(List.of(e.getURI().toString()));
});
// Do it this way to prevent IDE inspections from complaining

View file

@ -140,7 +140,7 @@ public class AppExtensionManager {
}
private void loadAllExtensions() {
for (var ext : List.of("proc", "uacc")) {
for (var ext : List.of("system", "proc", "uacc")) {
var extension = findAndParseExtension(ext, baseLayer)
.orElseThrow(() -> ExtensionException.corrupt("Missing module " + ext));
loadedExtensions.add(extension);
@ -158,16 +158,20 @@ public class AppExtensionManager {
private Optional<Extension> findAndParseExtension(String name, ModuleLayer parent) {
var inModulePath = ModuleLayer.boot().findModule("io.xpipe.ext." + name);
if (inModulePath.isPresent()) {
TrackEvent.info("Loaded extension " + name + " from boot module path");
return Optional.of(new Extension(null, inModulePath.get().getName(), name, inModulePath.get(), 0));
}
for (Path extensionBaseDirectory : extensionBaseDirectories) {
var found = parseExtensionDirectory(extensionBaseDirectory.resolve(name), parent);
var extensionDir = extensionBaseDirectory.resolve(name);
var found = parseExtensionDirectory(extensionDir, parent);
if (found.isPresent()) {
TrackEvent.info("Loaded extension " + name + " from module " + extensionDir);
return found;
}
}
TrackEvent.info("Unable to locate module " + name);
return Optional.empty();
}

View file

@ -7,6 +7,8 @@ import io.xpipe.core.process.OsType;
import javafx.scene.Node;
import javafx.scene.text.Font;
import org.kordamp.ikonli.javafx.FontIcon;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
@ -46,7 +48,12 @@ public class AppFont {
}
public static void init() {
TrackEvent.info("Loading fonts ...");
// Load ikonli fonts
TrackEvent.info("Loading ikonli fonts ...");
new FontIcon("mdi2s-stop");
new FontIcon("mdi2m-magnify");
TrackEvent.info("Loading bundled fonts ...");
AppResources.with(
AppResources.XPIPE_MODULE,
"fonts",

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