mirror of
https://github.com/xpipe-io/xpipe.git
synced 2025-04-15 08:43:35 +00:00
Squash merge branch 14-release into master
This commit is contained in:
parent
48c9f96c03
commit
45f6545fc8
2705 changed files with 42081 additions and 20693 deletions
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,4 +24,9 @@ public class ConnectionBrowseExchangeImpl extends ConnectionBrowseExchange {
|
|||
AppLayoutModel.get().selectBrowser();
|
||||
return Response.builder().build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getSynchronizationObject() {
|
||||
return DataStorage.get();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -21,4 +21,9 @@ public class ConnectionRefreshExchangeImpl extends ConnectionRefreshExchange {
|
|||
}
|
||||
return Response.builder().build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getSynchronizationObject() {
|
||||
return DataStorage.get();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,4 +24,9 @@ public class ConnectionToggleExchangeImpl extends ConnectionToggleExchange {
|
|||
}
|
||||
return Response.builder().build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getSynchronizationObject() {
|
||||
return DataStorage.get();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 ->
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
35
app/src/main/java/io/xpipe/app/comp/base/InputGroupComp.java
Normal file
35
app/src/main/java/io/xpipe/app/comp/base/InputGroupComp.java
Normal 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);
|
||||
}
|
||||
}
|
84
app/src/main/java/io/xpipe/app/comp/base/IntroComp.java
Normal file
84
app/src/main/java/io/xpipe/app/comp/base/IntroComp.java
Normal 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;
|
||||
}
|
||||
}
|
38
app/src/main/java/io/xpipe/app/comp/base/IntroListComp.java
Normal file
38
app/src/main/java/io/xpipe/app/comp/base/IntroListComp.java
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
74
app/src/main/java/io/xpipe/app/comp/base/ModalButton.java
Normal file
74
app/src/main/java/io/xpipe/app/comp/base/ModalButton.java
Normal 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);
|
||||
};
|
||||
}
|
||||
}
|
78
app/src/main/java/io/xpipe/app/comp/base/ModalOverlay.java
Normal file
78
app/src/main/java/io/xpipe/app/comp/base/ModalOverlay.java
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
() -> {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 -> {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
114
app/src/main/java/io/xpipe/app/core/AppArguments.java
Normal file
114
app/src/main/java/io/xpipe/app/core/AppArguments.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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
Loading…
Add table
Reference in a new issue