Merge branch prefs into master

The changes have been squashed as the commit history and messages were not very carefully crafted. There isn't that much value in preserving random commit messages.

Also due to diverging branches, rebasing or merging it was difficult.
This commit is contained in:
crschnick 2024-02-28 07:36:31 +00:00
parent ce45ff9ec6
commit 3e7fbe89ac
442 changed files with 11614 additions and 6865 deletions

5
.gitignore vendored
View file

@ -7,7 +7,8 @@ lib/
dev.properties
extensions.txt
dev_storage
local*/
local/
local_*/
.vs
.vscode
obj
@ -15,3 +16,5 @@ out
bin
.DS_Store
ComponentsGenerated.wxs
!dist/javafx/**/lib
!dist/javafx/**/bin

View file

@ -18,7 +18,7 @@ There are no real formal contribution guidelines right now, they will maybe come
All XPipe components target [Java 21](https://openjdk.java.net/projects/jdk/20/) and make full use of the Java Module System (JPMS).
All components are modularized, including all their dependencies.
In case a dependency is (sadly) not modularized yet, module information is manually added using [moditect](https://github.com/moditect/moditect-gradle-plugin).
In case a dependency is (sadly) not modularized yet, module information is manually added using [extra-java-module-info](https://github.com/gradlex-org/extra-java-module-info).
Further, note that as this is a pretty complicated Java project that fully utilizes modularity,
many IDEs still have problems building this project properly.

View file

@ -159,7 +159,7 @@ Alternatively, you can also use [Homebrew](https://github.com/xpipe-io/homebrew-
XPipe utilizes an open core model, which essentially means that the main application is open source while certain other components are not. Select parts are not open source yet, but may be added to this repository in the future.
This mainly concerns the features only available in the professional tier and the shell handling library implementation. Furthermore, some tests and especially test environments and that run on private servers are also not included in this repository.
This mainly concerns the features only available in the professional tier and the shell handling library implementation. Furthermore, some CI pipelines and tests that run on private servers are also not included in this repository.
## More links

View file

@ -2,15 +2,11 @@ plugins {
id 'java-library'
id 'maven-publish'
id 'signing'
id "org.moditect.gradleplugin" version "1.0.0-rc3"
}
apply from: "$rootDir/gradle/gradle_scripts/java.gradle"
apply from: "$rootDir/gradle/gradle_scripts/junit.gradle"
System.setProperty('excludeExtensionLibrary', 'true')
apply from: "$rootDir/gradle/gradle_scripts/extension_test.gradle"
version = rootProject.versionString
group = 'io.xpipe'
archivesBaseName = 'xpipe-api'
@ -19,14 +15,14 @@ repositories {
mavenCentral()
}
test {
enabled = false
dependencies {
testImplementation project(':api')
}
dependencies {
api project(':core')
implementation project(':beacon')
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.15.2"
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.16.1"
}
configurations {
@ -38,6 +34,5 @@ task dist(type: Copy) {
into "${project(':dist').buildDir}/dist/libraries"
}
apply from: 'publish.gradle'
apply from: "$rootDir/gradle/gradle_scripts/publish-base.gradle"

View file

@ -1,6 +1,6 @@
package io.xpipe.api.test;
import io.xpipe.beacon.BeaconDaemonController;
import io.xpipe.beacon.test.BeaconDaemonController;
import io.xpipe.core.util.XPipeDaemonMode;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;

View file

@ -0,0 +1,14 @@
package io.xpipe.api.test;
import io.xpipe.beacon.test.BeaconDaemonController;
import io.xpipe.core.util.XPipeDaemonMode;
import org.junit.jupiter.api.Test;
public class StartupTest {
@Test
public void test() throws Exception {
BeaconDaemonController.start(XPipeDaemonMode.TRAY);
BeaconDaemonController.stop();
}
}

View file

@ -1,131 +1,70 @@
plugins {
id 'application'
id "org.moditect.gradleplugin" version "1.0.0-rc3"
id 'jvm-test-suite'
id 'java-library'
}
repositories {
mavenCentral()
}
configurations {
dep
}
apply from: "$rootDir/gradle/gradle_scripts/java.gradle"
apply from: "$rootDir/gradle/gradle_scripts/javafx.gradle"
apply from: "$projectDir/gradle_scripts/richtextfx.gradle"
apply from: "$rootDir/gradle/gradle_scripts/commons.gradle"
apply from: "$rootDir/gradle/gradle_scripts/prettytime.gradle"
apply from: "$projectDir/gradle_scripts/sentry.gradle"
apply from: "$rootDir/gradle/gradle_scripts/lombok.gradle"
apply from: "$projectDir/gradle_scripts/github-api.gradle"
apply from: "$projectDir/gradle_scripts/flexmark.gradle"
apply from: "$rootDir/gradle/gradle_scripts/picocli.gradle"
apply from: "$rootDir/gradle/gradle_scripts/versioncompare.gradle"
apply from: "$rootDir/gradle/gradle_scripts/markdowngenerator.gradle"
configurations {
implementation.extendsFrom(dep)
implementation.extendsFrom(javafx)
}
dependencies {
compileOnly project(':api')
implementation project(':core')
implementation project(':beacon')
api project(':core')
api project(':beacon')
compileOnly 'org.hamcrest:hamcrest:2.2'
compileOnly 'org.junit.jupiter:junit-jupiter-api:5.9.3'
compileOnly 'org.junit.jupiter:junit-jupiter-params:5.9.3'
compileOnly 'org.junit.jupiter:junit-jupiter-api:5.10.2'
compileOnly 'org.junit.jupiter:junit-jupiter-params:5.10.2'
implementation 'net.java.dev.jna:jna-jpms:5.13.0'
implementation 'net.java.dev.jna:jna-platform-jpms:5.13.0'
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.15.2"
implementation group: 'com.fasterxml.jackson.module', name: 'jackson-module-parameter-names', version: "2.15.2"
implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.15.2"
implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jdk8', version: "2.15.2"
implementation group: 'org.kordamp.ikonli', name: 'ikonli-material2-pack', version: "12.2.0"
implementation group: 'org.kordamp.ikonli', name: 'ikonli-materialdesign2-pack', version: "12.2.0"
implementation group: 'org.kordamp.ikonli', name: 'ikonli-javafx', version: "12.2.0"
implementation group: 'org.kordamp.ikonli', name: 'ikonli-material-pack', version: "12.2.0"
implementation group: 'org.kordamp.ikonli', name: 'ikonli-feather-pack', version: "12.2.0"
implementation (name: 'preferencesfx-core-11.15.0')
implementation (group: 'com.dlsc.formsfx', name: 'formsfx-core', version: '11.6.0') {
exclude group: 'org.openjfx', module: 'javafx-controls'
exclude group: 'org.openjfx', module: 'javafx-fxml'
}
implementation group: 'org.slf4j', name: 'slf4j-api', version: '2.0.7'
implementation 'io.xpipe:modulefs:0.1.4'
implementation 'com.jfoenix:jfoenix:9.0.10'
implementation 'org.controlsfx:controlsfx:11.1.2'
implementation 'net.synedra:validatorfx:0.4.2'
implementation ('io.github.mkpaz:atlantafx-base:2.0.1') {
api 'com.vladsch.flexmark:flexmark:0.64.0'
api 'com.vladsch.flexmark:flexmark-util-data:0.64.0'
api 'com.vladsch.flexmark:flexmark-util-ast:0.64.0'
api 'com.vladsch.flexmark:flexmark-util-builder:0.64.0'
api 'com.vladsch.flexmark:flexmark-util-sequence:0.64.0'
api 'com.vladsch.flexmark:flexmark-util-misc:0.64.0'
api 'com.vladsch.flexmark:flexmark-util-dependency:0.64.0'
api 'com.vladsch.flexmark:flexmark-util-collection:0.64.0'
api 'com.vladsch.flexmark:flexmark-util-format:0.64.0'
api 'com.vladsch.flexmark:flexmark-util-html:0.64.0'
api 'com.vladsch.flexmark:flexmark-util-visitor:0.64.0'
api files("$rootDir/gradle/gradle_scripts/markdowngenerator-1.3.1.1.jar")
api 'info.picocli:picocli:4.7.5'
api 'org.kohsuke:github-api:1.318'
api 'io.sentry:sentry:7.3.0'
api 'org.ocpsoft.prettytime:prettytime:5.0.2.Final'
api 'commons-io:commons-io:2.15.1'
api 'net.java.dev.jna:jna-jpms:5.14.0'
api 'net.java.dev.jna:jna-platform-jpms:5.14.0'
api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.16.1"
api group: 'com.fasterxml.jackson.module', name: 'jackson-module-parameter-names', version: "2.16.1"
api group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.16.1"
api group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jdk8', version: "2.16.1"
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"
api group: 'org.kordamp.ikonli', name: 'ikonli-material-pack', version: "12.2.0"
api group: 'org.kordamp.ikonli', name: 'ikonli-feather-pack', version: "12.2.0"
api group: 'org.slf4j', name: 'slf4j-api', version: '2.0.11'
api 'io.xpipe:modulefs:0.1.5'
api 'net.synedra:validatorfx:0.4.2'
api ('io.github.mkpaz:atlantafx-base:2.0.1') {
exclude group: 'org.openjfx', module: 'javafx-base'
exclude group: 'org.openjfx', module: 'javafx-controls'
}
implementation name: 'jSystemThemeDetector-3.8'
implementation group: 'com.github.oshi', name: 'oshi-core-java11', version: '6.4.2'
implementation 'org.jetbrains:annotations:24.0.1'
implementation ('de.jangassen:jfa:1.2.0') {
exclude group: 'net.java.dev.jna', module: 'jna'
}
}
apply from: "$rootDir/gradle/gradle_scripts/junit.gradle"
sourceSets {
main {
output.resourcesDir("${project.layout.buildDirectory.get()}/classes/java/main")
}
}
dependencies {
testImplementation project(':api')
testImplementation project(':core')
}
project.allExtensions.forEach((Project p) -> {
dependencies {
testCompileOnly p
}
})
project.ext {
jvmRunArgs = [
"--add-exports", "javafx.graphics/com.sun.javafx.scene=com.jfoenix",
"--add-exports", "javafx.graphics/com.sun.javafx.stage=com.jfoenix",
"--add-exports", "javafx.base/com.sun.javafx.binding=com.jfoenix",
"--add-exports", "javafx.base/com.sun.javafx.event=com.jfoenix",
"--add-exports", "javafx.controls/com.sun.javafx.scene.control=com.jfoenix",
"--add-exports", "javafx.controls/com.sun.javafx.scene.control.behavior=com.jfoenix",
"--add-exports", "javafx.graphics/com.sun.javafx.scene.traversal=org.controlsfx.controls",
"--add-exports", "javafx.graphics/com.sun.javafx.scene=org.controlsfx.controls",
"--add-exports", "org.apache.commons.lang3/org.apache.commons.lang3.math=io.xpipe.app",
"--add-opens", "java.base/java.lang=io.xpipe.app",
"--add-opens", "java.base/java.nio.file=io.xpipe.app",
"--add-opens", "java.base/java.lang.reflect=com.jfoenix",
"--add-opens", "java.base/java.lang.reflect=com.jfoenix",
"--add-opens", "java.base/java.lang=io.xpipe.core",
"--add-opens", "java.desktop/java.awt=io.xpipe.app",
"--add-opens", "net.synedra.validatorfx/net.synedra.validatorfx=io.xpipe.app",
"--add-opens", 'com.dlsc.preferencesfx/com.dlsc.preferencesfx.view=io.xpipe.app',
"--add-opens", 'com.dlsc.preferencesfx/com.dlsc.preferencesfx.model=io.xpipe.app',
"-Xmx8g",
"-Dio.xpipe.app.arch=$rootProject.arch",
"-Dfile.encoding=UTF-8",
// Disable this for now as it requires Windows 10+
// '-XX:+UseZGC',
"-Dvisualvm.display.name=XPipe"
]
}
import org.gradle.internal.os.OperatingSystem
if (OperatingSystem.current() == OperatingSystem.LINUX) {
jvmRunArgs.addAll("--add-opens", "java.desktop/sun.awt.X11=io.xpipe.app")
}
apply from: "$rootDir/gradle/gradle_scripts/local_junit_suite.gradle"
def extensionJarDepList = project.allExtensions.stream().map(p -> p.getTasksByName('jar', true)).toList();
jar {
finalizedBy(extensionJarDepList)
}
@ -146,14 +85,19 @@ run {
systemProperty 'io.xpipe.app.developerMode', "true"
systemProperty 'io.xpipe.app.logLevel', "trace"
systemProperty 'io.xpipe.app.fullVersion', rootProject.fullVersion
systemProperty 'io.xpipe.app.showcase', 'false'
systemProperty 'io.xpipe.app.showcase', 'true'
systemProperty 'io.xpipe.app.staging', isStage
// systemProperty "io.xpipe.beacon.port", "21724"
// systemProperty "io.xpipe.beacon.printMessages", "true"
// systemProperty 'io.xpipe.app.debugPlatform', "true"
// systemProperty "io.xpipe.beacon.localProxy", "true"
// Apply passed xpipe properties
for (final def e in System.getProperties().entrySet()) {
if (e.getKey().toString().contains("xpipe")) {
systemProperty e.getKey().toString(), e.getValue()
}
}
systemProperty 'java.library.path', "./lib"
workingDir = rootDir
}
@ -181,7 +125,7 @@ processResources {
javaexec {
workingDir = project.projectDir
jvmArgs += "--module-path=$sourceSets.main.runtimeClasspath.asPath,"
jvmArgs += "--module-path=${configurations.javafx.asFileTree.asPath},"
jvmArgs += "--add-modules=javafx.graphics"
main = "com.sun.javafx.css.parser.Css2Bin"
args css

View file

@ -0,0 +1,4 @@
open module io.xpipe.app.localTest {
requires org.junit.jupiter.api;
requires io.xpipe.app;
}

View file

@ -0,0 +1,13 @@
package test;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.test.LocalExtensionTest;
public class Test extends LocalExtensionTest {
@org.junit.jupiter.api.Test
public void test() {
System.out.println("a");
System.out.println(DataStorage.get().getStoreEntries());
}
}

View file

@ -14,10 +14,10 @@ public class Main {
// Since this is not marked as a console application, it will not print anything when you run it in a console
// So sadly there can't be a help command
// if (args.length == 1 && args[0].equals("--help")) {
// System.out.println("HELP");
// return;
// }
// if (args.length == 1 && args[0].equals("--help")) {
// System.out.println("HELP");
// return;
// }
OperationMode.init(args);
}

View file

@ -5,12 +5,42 @@ import io.xpipe.app.core.AppWindowHelper;
import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileSystem;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonBar;
import javafx.scene.control.ButtonType;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.stream.Collectors;
public class BrowserAlerts {
public static FileConflictChoice showFileConflictAlert(String file, boolean multiple) {
var map = new LinkedHashMap<ButtonType, FileConflictChoice>();
map.put(new ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE), FileConflictChoice.CANCEL);
if (multiple) {
map.put(new ButtonType("Skip", ButtonBar.ButtonData.OTHER), FileConflictChoice.SKIP);
map.put(new ButtonType("Skip All", ButtonBar.ButtonData.OTHER), FileConflictChoice.SKIP_ALL);
}
map.put(new ButtonType("Replace", ButtonBar.ButtonData.OTHER), FileConflictChoice.REPLACE);
if (multiple) {
map.put(new ButtonType("Replace All", ButtonBar.ButtonData.OTHER), FileConflictChoice.REPLACE_ALL);
}
return AppWindowHelper.showBlockingAlert(alert -> {
alert.setTitle(AppI18n.get("fileConflictAlertTitle"));
alert.setHeaderText(AppI18n.get("fileConflictAlertHeader"));
AppWindowHelper.setContent(
alert,
AppI18n.get(
multiple ? "fileConflictAlertContentMultiple" : "fileConflictAlertContent", file));
alert.setAlertType(Alert.AlertType.CONFIRMATION);
alert.getButtonTypes().clear();
map.sequencedKeySet()
.forEach(buttonType -> alert.getButtonTypes().add(buttonType));
})
.map(map::get)
.orElse(FileConflictChoice.CANCEL);
}
public static boolean showMoveAlert(List<FileSystem.FileEntry> source, FileSystem.FileEntry target) {
if (source.stream().noneMatch(entry -> entry.getKind() == FileKind.DIRECTORY)) {
return true;
@ -52,4 +82,12 @@ public class BrowserAlerts {
}
return names;
}
public enum FileConflictChoice {
CANCEL,
SKIP,
SKIP_ALL,
REPLACE,
REPLACE_ALL
}
}

View file

@ -0,0 +1,137 @@
package io.xpipe.app.browser;
import atlantafx.base.theme.Styles;
import io.xpipe.app.comp.store.StoreEntryWrapper;
import io.xpipe.app.comp.store.StoreSection;
import io.xpipe.app.comp.store.StoreSectionMiniComp;
import io.xpipe.app.comp.store.StoreViewState;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.FilterComp;
import io.xpipe.app.fxcomps.impl.HorizontalComp;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.DataStoreCategoryChoiceComp;
import io.xpipe.app.util.FixedHierarchyStore;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.ShellStore;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.css.PseudoClass;
import javafx.geometry.Point2D;
import javafx.scene.input.DragEvent;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.function.Predicate;
final class BrowserBookmarkComp extends SimpleComp {
public static final Timer DROP_TIMER = new Timer("dnd", true);
private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
private final BrowserModel model;
private Point2D lastOver = new Point2D(-1, -1);
private TimerTask activeTask;
BrowserBookmarkComp(BrowserModel model) {
this.model = model;
}
@Override
protected Region createSimple() {
var filterText = new SimpleStringProperty();
var open = PlatformThread.sync(model.getSelected());
Predicate<StoreEntryWrapper> applicable = storeEntryWrapper -> {
return (storeEntryWrapper.getEntry().getStore() instanceof ShellStore
|| storeEntryWrapper.getEntry().getStore() instanceof FixedHierarchyStore)
&& storeEntryWrapper.getEntry().getValidity().isUsable();
};
var selectedCategory = new SimpleObjectProperty<>(
StoreViewState.get().getActiveCategory().getValue());
var section = StoreSectionMiniComp.createList(
StoreSection.createTopLevel(
StoreViewState.get().getAllEntries(), storeEntryWrapper -> true, filterText, selectedCategory),
(s, comp) -> {
BooleanProperty busy = new SimpleBooleanProperty(false);
comp.disable(Bindings.createBooleanBinding(
() -> {
return busy.get() || !applicable.test(s.getWrapper());
},
busy));
comp.apply(struc -> {
open.addListener((observable, oldValue, newValue) -> {
struc.get()
.pseudoClassStateChanged(
SELECTED,
newValue != null
&& newValue.getEntry()
.get()
.equals(s.getWrapper()
.getEntry()));
});
struc.get().setOnAction(event -> {
ThreadHelper.runFailableAsync(() -> {
var entry = s.getWrapper().getEntry();
if (!entry.getValidity().isUsable()) {
return;
}
if (entry.getStore() instanceof ShellStore fileSystem) {
model.openFileSystemAsync(entry.ref(), null, busy);
} else if (entry.getStore() instanceof FixedHierarchyStore) {
BooleanScope.execute(busy, () -> {
s.getWrapper().refreshChildren();
});
}
});
event.consume();
});
});
});
var category = new DataStoreCategoryChoiceComp(
StoreViewState.get().getAllConnectionsCategory(),
StoreViewState.get().getActiveCategory(),
selectedCategory)
.styleClass(Styles.LEFT_PILL);
var filter =
new FilterComp(filterText).styleClass(Styles.RIGHT_PILL).hgrow().apply(struc -> {});
var top = new HorizontalComp(List.of(category.minWidth(Region.USE_PREF_SIZE), filter.hgrow()))
.styleClass("categories")
.apply(struc -> {
AppFont.medium(struc.get());
struc.get().setFillHeight(true);
})
.createRegion();
var r = section.vgrow().createRegion();
var content = new VBox(top, r);
content.setFillWidth(true);
content.getStyleClass().add("bookmark-list");
return content;
}
private void handleHoverTimer(DataStore store, DragEvent event) {
if (lastOver.getX() == event.getX() && lastOver.getY() == event.getY()) {
return;
}
lastOver = (new Point2D(event.getX(), event.getY()));
activeTask = new TimerTask() {
@Override
public void run() {
if (activeTask != this) {}
// Platform.runLater(() -> model.openExistingFileSystemIfPresent(store.asNeeded()));
}
};
DROP_TIMER.schedule(activeTask, 500);
}
}

View file

@ -66,7 +66,7 @@ public class BrowserBreadcrumbBar extends SimpleComp {
var elements = FileNames.splitHierarchy(val);
var modifiedElements = new ArrayList<>(elements);
if (val.startsWith("/")) {
modifiedElements.add(0, "/");
modifiedElements.addFirst("/");
}
Breadcrumbs.BreadCrumbItem<String> items =
Breadcrumbs.buildTreeModel(modifiedElements.toArray(String[]::new));

View file

@ -2,7 +2,7 @@ package io.xpipe.app.browser;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.process.ShellDialects;
import io.xpipe.core.process.ProcessControlProvider;
import io.xpipe.core.store.FileSystem;
import io.xpipe.core.util.FailableRunnable;
import javafx.beans.property.Property;
@ -24,18 +24,6 @@ import java.util.stream.Collectors;
public class BrowserClipboard {
@Value
public static class Instance {
UUID uuid;
FileSystem.FileEntry baseDirectory;
List<FileSystem.FileEntry> entries;
public String toClipboardString() {
return entries.stream().map(fileEntry -> "\"" + fileEntry.getPath() + "\"").collect(
Collectors.joining(ShellDialects.getPlatformDefault().getNewLine().getNewLineString()));
}
}
public static final Property<Instance> currentCopyClipboard = new SimpleObjectProperty<>();
public static Instance currentDragClipboard;
@ -45,7 +33,7 @@ public class BrowserClipboard {
.addFlavorListener(e -> ThreadHelper.runFailableAsync(new FailableRunnable<>() {
@Override
@SuppressWarnings("unchecked")
public void run() throws Throwable {
public void run() {
Clipboard clipboard = (Clipboard) e.getSource();
try {
if (!clipboard.isDataFlavorAvailable(DataFlavor.javaFileListFlavor)) {
@ -53,7 +41,8 @@ public class BrowserClipboard {
}
List<File> data = (List<File>) clipboard.getData(DataFlavor.javaFileListFlavor);
var files = data.stream().map(string -> string.toPath()).toList();
var files =
data.stream().map(string -> string.toPath()).toList();
if (files.size() == 0) {
return;
}
@ -121,4 +110,20 @@ public class BrowserClipboard {
return null;
}
@Value
public static class Instance {
UUID uuid;
FileSystem.FileEntry baseDirectory;
List<FileSystem.FileEntry> entries;
public String toClipboardString() {
return entries.stream()
.map(fileEntry -> "\"" + fileEntry.getPath() + "\"")
.collect(Collectors.joining(ProcessControlProvider.get()
.getEffectiveLocalDialect()
.getNewLine()
.getNewLineString()));
}
}
}

View file

@ -58,34 +58,33 @@ public class BrowserComp extends SimpleComp {
FileIconManager.loadIfNecessary();
});
var bookmarksList = new BrowserBookmarkList(model).vgrow();
var localDownloadStage = new BrowserTransferComp(model.getLocalTransfersStage()).hide(
PlatformThread.sync(Bindings.createBooleanBinding(() -> {
if (model.getOpenFileSystems().size() == 0) {
return true;
}
var bookmarksList = new BrowserBookmarkComp(model).vgrow();
var localDownloadStage = new BrowserTransferComp(model.getLocalTransfersStage())
.hide(PlatformThread.sync(Bindings.createBooleanBinding(
() -> {
if (model.getOpenFileSystems().size() == 0) {
return true;
}
if (model.getMode().isChooser()) {
return true;
}
if (model.getMode().isChooser()) {
return true;
}
// Also show on local
if (model.getSelected().getValue() != null) {
// return model.getSelected().getValue().isLocal();
}
return false;
}, model.getOpenFileSystems(), model.getSelected())));
return false;
},
model.getOpenFileSystems(),
model.getSelected())));
localDownloadStage.prefHeight(200);
localDownloadStage.maxHeight(200);
var vertical = new VerticalComp(List.of(bookmarksList, localDownloadStage));
var splitPane = new SideSplitPaneComp(vertical, createTabs()).withInitialWidth(
AppLayoutModel.get().getSavedState().getBrowserConnectionsWidth()).withOnDividerChange(
AppLayoutModel.get().getSavedState()::setBrowserConnectionsWidth).apply(struc -> {
struc.getLeft().setMinWidth(200);
struc.getLeft().setMaxWidth(500);
});
var splitPane = new SideSplitPaneComp(vertical, createTabs())
.withInitialWidth(AppLayoutModel.get().getSavedState().getBrowserConnectionsWidth())
.withOnDividerChange(AppLayoutModel.get().getSavedState()::setBrowserConnectionsWidth)
.apply(struc -> {
struc.getLeft().setMinWidth(200);
struc.getLeft().setMaxWidth(500);
});
var r = addBottomBar(splitPane.createRegion());
r.getStyleClass().add("browser");
// AppFont.small(r);
@ -104,12 +103,16 @@ public class BrowserComp extends SimpleComp {
selected.setSpacing(10);
model.getSelection().addListener((ListChangeListener<? super BrowserEntry>) c -> {
PlatformThread.runLaterIfNeeded(() -> {
selected.getChildren().setAll(c.getList().stream().map(s -> {
var field = new TextField(s.getRawFileEntry().getPath());
field.setEditable(false);
field.setPrefWidth(500);
return field;
}).toList());
selected.getChildren()
.setAll(c.getList().stream()
.map(s -> {
var field =
new TextField(s.getRawFileEntry().getPath());
field.setEditable(false);
field.setPrefWidth(500);
return field;
})
.toList());
});
});
var spacer = new Spacer(Orientation.HORIZONTAL);
@ -128,12 +131,16 @@ public class BrowserComp extends SimpleComp {
}
private Comp<?> createTabs() {
var multi = new MultiContentComp(Map.<Comp<?>, ObservableValue<Boolean>>of(Comp.of(() -> createTabPane()),
var multi = new MultiContentComp(Map.<Comp<?>, ObservableValue<Boolean>>of(
Comp.of(() -> createTabPane()),
BindingsHelper.persist(Bindings.isNotEmpty(model.getOpenFileSystems())),
new BrowserWelcomeComp(model).apply(struc -> StackPane.setAlignment(struc.get(), Pos.CENTER_LEFT)),
Bindings.createBooleanBinding(() -> {
return model.getOpenFileSystems().size() == 0 && !model.getMode().isChooser();
}, model.getOpenFileSystems())));
Bindings.createBooleanBinding(
() -> {
return model.getOpenFileSystems().size() == 0
&& !model.getMode().isChooser();
},
model.getOpenFileSystems())));
return multi;
}
@ -154,7 +161,8 @@ public class BrowserComp extends SimpleComp {
map.put(v, t);
tabs.getTabs().add(t);
});
tabs.getSelectionModel().select(model.getOpenFileSystems().indexOf(model.getSelected().getValue()));
tabs.getSelectionModel()
.select(model.getOpenFileSystems().indexOf(model.getSelected().getValue()));
// Used for ignoring changes by the tabpane when new tabs are added. We want to perform the selections manually!
var modifying = new SimpleBooleanProperty();
@ -170,9 +178,9 @@ public class BrowserComp extends SimpleComp {
return;
}
var source = map.entrySet()
.stream()
.filter(openFileSystemModelTabEntry -> openFileSystemModelTabEntry.getValue().equals(newValue))
var source = map.entrySet().stream()
.filter(openFileSystemModelTabEntry ->
openFileSystemModelTabEntry.getValue().equals(newValue))
.findAny()
.map(Map.Entry::getKey)
.orElse(null);
@ -187,9 +195,9 @@ public class BrowserComp extends SimpleComp {
return;
}
var toSelect = map.entrySet()
.stream()
.filter(openFileSystemModelTabEntry -> openFileSystemModelTabEntry.getKey().equals(newValue))
var toSelect = map.entrySet().stream()
.filter(openFileSystemModelTabEntry ->
openFileSystemModelTabEntry.getKey().equals(newValue))
.findAny()
.map(Map.Entry::getValue)
.orElse(null);
@ -228,9 +236,9 @@ public class BrowserComp extends SimpleComp {
tabs.getTabs().addListener((ListChangeListener<? super Tab>) c -> {
while (c.next()) {
for (var r : c.getRemoved()) {
var source = map.entrySet()
.stream()
.filter(openFileSystemModelTabEntry -> openFileSystemModelTabEntry.getValue().equals(r))
var source = map.entrySet().stream()
.filter(openFileSystemModelTabEntry ->
openFileSystemModelTabEntry.getValue().equals(r))
.findAny()
.orElse(null);
@ -253,14 +261,22 @@ public class BrowserComp extends SimpleComp {
ring.setMinSize(16, 16);
ring.setPrefSize(16, 16);
ring.setMaxSize(16, 16);
ring.progressProperty().bind(Bindings.createDoubleBinding(() -> model.getBusy().get() ? -1d : 0, PlatformThread.sync(model.getBusy())));
ring.progressProperty()
.bind(Bindings.createDoubleBinding(
() -> model.getBusy().get() ? -1d : 0, PlatformThread.sync(model.getBusy())));
var image = model.getEntry().get().getProvider().getDisplayIconFileName(model.getEntry().getStore());
var image = model.getEntry()
.get()
.getProvider()
.getDisplayIconFileName(model.getEntry().getStore());
var logo = PrettyImageHelper.ofFixedSquare(image, 16).createRegion();
tab.graphicProperty().bind(Bindings.createObjectBinding(() -> {
return model.getBusy().get() ? ring : logo;
}, PlatformThread.sync(model.getBusy())));
tab.graphicProperty()
.bind(Bindings.createObjectBinding(
() -> {
return model.getBusy().get() ? ring : logo;
},
PlatformThread.sync(model.getBusy())));
tab.setText(model.getName());
tab.setContent(new OpenFileSystemComp(model).createSimple());
@ -281,12 +297,17 @@ public class BrowserComp extends SimpleComp {
StackPane c = (StackPane) tabs.lookup("#" + id + " .tab-container");
c.getStyleClass().add("color-box");
var color = DataStorage.get().getRootForEntry(model.getEntry().get()).getColor();
var color = DataStorage.get()
.getRootForEntry(model.getEntry().get())
.getColor();
if (color != null) {
c.getStyleClass().add(color.getId());
}
new FancyTooltipAugment<>(new SimpleStringProperty(model.getTooltip())).augment(c);
c.addEventHandler(DragEvent.DRAG_ENTERED, mouseEvent -> Platform.runLater(() -> tabs.getSelectionModel().select(tab)));
c.addEventHandler(
DragEvent.DRAG_ENTERED,
mouseEvent -> Platform.runLater(
() -> tabs.getSelectionModel().select(tab)));
});
}
});

View file

@ -24,6 +24,17 @@ final class BrowserContextMenu extends ContextMenu {
createMenu();
}
private static List<BrowserEntry> resolveIfNeeded(BrowserAction action, List<BrowserEntry> selected) {
return action.automaticallyResolveLinks()
? selected.stream()
.map(browserEntry -> new BrowserEntry(
browserEntry.getRawFileEntry().resolved(),
browserEntry.getModel(),
browserEntry.isSynthetic()))
.toList()
: selected;
}
private void createMenu() {
AppFont.normal(this.getStyleableNode());
@ -81,7 +92,10 @@ final class BrowserContextMenu extends ContextMenu {
}
m.setDisable(!a.isActive(model, used));
if (la.getProFeatureId() != null && !LicenseProvider.get().getFeature(la.getProFeatureId()).isSupported()) {
if (la.getProFeatureId() != null
&& !LicenseProvider.get()
.getFeature(la.getProFeatureId())
.isSupported()) {
m.setDisable(true);
m.setGraphic(new FontIcon("mdi2p-professional-hexagon"));
}
@ -91,15 +105,4 @@ final class BrowserContextMenu extends ContextMenu {
}
}
}
private static List<BrowserEntry> resolveIfNeeded(BrowserAction action, List<BrowserEntry> selected) {
return action.automaticallyResolveLinks()
? selected.stream()
.map(browserEntry -> new BrowserEntry(
browserEntry.getRawFileEntry().resolved(),
browserEntry.getModel(),
browserEntry.isSynthetic()))
.toList()
: selected;
}
}

View file

@ -2,8 +2,8 @@ package io.xpipe.app.browser;
import io.xpipe.app.browser.icon.DirectoryType;
import io.xpipe.app.browser.icon.FileType;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileSystem;
import lombok.Getter;

View file

@ -12,9 +12,9 @@ import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.HumanReadableFormat;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.process.OsType;
import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileSystem;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
@ -81,18 +81,18 @@ final class BrowserFileListComp extends SimpleComp {
filenameCol.setCellFactory(col -> new FilenameCell(fileList.getEditing()));
var sizeCol = new TableColumn<BrowserEntry, Number>("Size");
sizeCol.setCellValueFactory(param ->
new SimpleLongProperty(param.getValue().getRawFileEntry().resolved().getSize()));
sizeCol.setCellValueFactory(param -> new SimpleLongProperty(
param.getValue().getRawFileEntry().resolved().getSize()));
sizeCol.setCellFactory(col -> new FileSizeCell());
var mtimeCol = new TableColumn<BrowserEntry, Instant>("Modified");
mtimeCol.setCellValueFactory(param ->
new SimpleObjectProperty<>(param.getValue().getRawFileEntry().resolved().getDate()));
mtimeCol.setCellValueFactory(param -> new SimpleObjectProperty<>(
param.getValue().getRawFileEntry().resolved().getDate()));
mtimeCol.setCellFactory(col -> new FileTimeCell());
var modeCol = new TableColumn<BrowserEntry, String>("Attributes");
modeCol.setCellValueFactory(param ->
new SimpleObjectProperty<>(param.getValue().getRawFileEntry().resolved().getMode()));
modeCol.setCellValueFactory(param -> new SimpleObjectProperty<>(
param.getValue().getRawFileEntry().resolved().getMode()));
modeCol.setCellFactory(col -> new FileModeCell());
modeCol.setSortable(false);
@ -171,7 +171,7 @@ final class BrowserFileListComp extends SimpleComp {
.mapToInt(entry -> table.getItems().indexOf(entry))
.toArray();
table.getSelectionModel()
.selectIndices(table.getItems().indexOf(c.getList().get(0)), indices);
.selectIndices(table.getItems().indexOf(c.getList().getFirst()), indices);
});
});
}
@ -247,12 +247,20 @@ final class BrowserFileListComp extends SimpleComp {
}
if (row.getItem() != null
&& row.getItem().getRawFileEntry().resolved().getKind() == FileKind.DIRECTORY) {
&& row.getItem()
.getRawFileEntry()
.resolved()
.getKind()
== FileKind.DIRECTORY) {
return event.getButton() == MouseButton.SECONDARY;
}
if (row.getItem() != null
&& row.getItem().getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY) {
&& row.getItem()
.getRawFileEntry()
.resolved()
.getKind()
!= FileKind.DIRECTORY) {
return event.getButton() == MouseButton.SECONDARY
|| event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2;
}
@ -409,9 +417,10 @@ final class BrowserFileListComp extends SimpleComp {
}
double proximity = 100;
Bounds tableBounds = tableView.localToScene(tableView.getBoundsInParent());
Bounds tableBounds = tableView.localToScene(tableView.getBoundsInLocal());
double dragY = event.getSceneY();
double topYProximity = tableBounds.getMinY() + proximity;
// Include table header as well in calculations
double topYProximity = tableBounds.getMinY() + proximity + 20;
double bottomYProximity = tableBounds.getMaxY() - proximity;
// clamp new values between 0 and 1 to prevent scrollbar flicking around at the edges
@ -424,15 +433,60 @@ final class BrowserFileListComp extends SimpleComp {
}
}
private static class FileSizeCell extends TableCell<BrowserEntry, Number> {
@Override
protected void updateItem(Number fileSize, boolean empty) {
super.updateItem(fileSize, empty);
if (empty || getTableRow() == null || getTableRow().getItem() == null) {
setText(null);
} else {
var path = getTableRow().getItem();
if (path.getRawFileEntry().resolved().getKind() == FileKind.DIRECTORY) {
setText("");
} else {
setText(byteCount(fileSize.longValue()));
}
}
}
}
private static class FileModeCell extends TableCell<BrowserEntry, String> {
@Override
protected void updateItem(String mode, boolean empty) {
super.updateItem(mode, empty);
if (empty || getTableRow() == null || getTableRow().getItem() == null) {
setText(null);
} else {
setText(mode);
}
}
}
private static class FileTimeCell extends TableCell<BrowserEntry, Instant> {
@Override
protected void updateItem(Instant fileTime, boolean empty) {
super.updateItem(fileTime, empty);
if (empty) {
setText(null);
} else {
setText(
fileTime != null
? HumanReadableFormat.date(
fileTime.atZone(ZoneId.systemDefault()).toLocalDateTime())
: "");
}
}
}
private class FilenameCell extends TableCell<BrowserEntry, String> {
private final StringProperty img = new SimpleStringProperty();
private final StringProperty text = new SimpleStringProperty();
private final Node imageView = new PrettySvgComp(img, 24, 24)
.createRegion();
private final StackPane textField =
new LazyTextFieldComp(text).createStructure().get();
private final HBox graphic;
private final BooleanProperty updating = new SimpleBooleanProperty();
@ -463,7 +517,8 @@ final class BrowserFileListComp extends SimpleComp {
};
text.addListener(listener);
graphic = new HBox(imageView, textField);
Node imageView = new PrettySvgComp(img, 24, 24).createRegion();
HBox graphic = new HBox(imageView, textField);
graphic.setSpacing(10);
graphic.setAlignment(Pos.CENTER_LEFT);
HBox.setHgrow(textField, Priority.ALWAYS);
@ -520,52 +575,4 @@ final class BrowserFileListComp extends SimpleComp {
}
}
}
private static class FileSizeCell extends TableCell<BrowserEntry, Number> {
@Override
protected void updateItem(Number fileSize, boolean empty) {
super.updateItem(fileSize, empty);
if (empty || getTableRow() == null || getTableRow().getItem() == null) {
setText(null);
} else {
var path = getTableRow().getItem();
if (path.getRawFileEntry().resolved().getKind() == FileKind.DIRECTORY) {
setText("");
} else {
setText(byteCount(fileSize.longValue()));
}
}
}
}
private static class FileModeCell extends TableCell<BrowserEntry, String> {
@Override
protected void updateItem(String mode, boolean empty) {
super.updateItem(mode, empty);
if (empty || getTableRow() == null || getTableRow().getItem() == null) {
setText(null);
} else {
setText(mode);
}
}
}
private static class FileTimeCell extends TableCell<BrowserEntry, Instant> {
@Override
protected void updateItem(Instant fileTime, boolean empty) {
super.updateItem(fileTime, empty);
if (empty) {
setText(null);
} else {
setText(
fileTime != null
? HumanReadableFormat.date(
fileTime.atZone(ZoneId.systemDefault()).toLocalDateTime())
: "");
}
}
}
}

View file

@ -27,7 +27,8 @@ public class BrowserFileListCompEntry {
private Point2D lastOver = new Point2D(-1, -1);
private TimerTask activeTask;
public BrowserFileListCompEntry(TableView<BrowserEntry> tv, Node row, BrowserEntry item, BrowserFileListModel model) {
public BrowserFileListCompEntry(
TableView<BrowserEntry> tv, Node row, BrowserEntry item, BrowserFileListModel model) {
this.tv = tv;
this.row = row;
this.item = item;
@ -59,14 +60,18 @@ public class BrowserFileListCompEntry {
var all = tv.getItems();
var index = item != null ? all.indexOf(item) : all.size() - 1;
var min = Math.min(index, tv.getSelectionModel().getSelectedIndices().stream()
.mapToInt(value -> value)
.min()
.orElse(1));
var max = Math.max(index, tv.getSelectionModel().getSelectedIndices().stream()
.mapToInt(value -> value)
.max()
.orElse(all.indexOf(item)));
var min = Math.min(
index,
tv.getSelectionModel().getSelectedIndices().stream()
.mapToInt(value -> value)
.min()
.orElse(1));
var max = Math.max(
index,
tv.getSelectionModel().getSelectedIndices().stream()
.mapToInt(value -> value)
.max()
.orElse(all.indexOf(item)));
var toSelect = new ArrayList<BrowserEntry>();
for (int i = min; i <= max; i++) {
@ -98,13 +103,15 @@ public class BrowserFileListCompEntry {
return false;
}
if (!Objects.equals(model.getFileSystemModel().getFileSystem(), cb.getEntries().get(0).getFileSystem())) {
if (!Objects.equals(
model.getFileSystemModel().getFileSystem(),
cb.getEntries().getFirst().getFileSystem())) {
return true;
}
// Prevent drag and drops of files into the current directory
if (cb.getBaseDirectory() != null && cb
.getBaseDirectory()
if (cb.getBaseDirectory() != null
&& cb.getBaseDirectory()
.getPath()
.equals(model.getFileSystemModel().getCurrentDirectory().getPath())
&& (item == null || item.getRawFileEntry().getKind() != FileKind.DIRECTORY)) {

View file

@ -2,8 +2,8 @@ package io.xpipe.app.browser;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileSystem;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
@ -99,8 +99,9 @@ public final class BrowserFileListModel {
path -> path.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY);
var comp = comparatorProperty.getValue();
Comparator<? super BrowserEntry> us =
comp != null ? syntheticFirst.thenComparing(dirsFirst).thenComparing(comp) : syntheticFirst.thenComparing(dirsFirst);
Comparator<? super BrowserEntry> us = comp != null
? syntheticFirst.thenComparing(dirsFirst).thenComparing(comp)
: syntheticFirst.thenComparing(dirsFirst);
l.sort(us);
}
@ -110,14 +111,17 @@ public final class BrowserFileListModel {
boolean exists;
try {
exists = fileSystemModel.getFileSystem().fileExists(newFullPath) || fileSystemModel.getFileSystem().directoryExists(newFullPath);
exists = fileSystemModel.getFileSystem().fileExists(newFullPath)
|| fileSystemModel.getFileSystem().directoryExists(newFullPath);
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
return false;
}
if (exists) {
ErrorEvent.fromMessage("Target " + newFullPath + " does already exist").expected().handle();
ErrorEvent.fromMessage("Target " + newFullPath + " does already exist")
.expected()
.handle();
fileSystemModel.refresh();
return false;
}

View file

@ -16,6 +16,14 @@ import org.kordamp.ikonli.javafx.FontIcon;
public class BrowserFilterComp extends Comp<BrowserFilterComp.Structure> {
private final OpenFileSystemModel model;
private final Property<String> filterString;
public BrowserFilterComp(OpenFileSystemModel model, Property<String> filterString) {
this.model = model;
this.filterString = filterString;
}
@Override
public Structure createBase() {
var expanded = new SimpleBooleanProperty();
@ -98,12 +106,4 @@ public class BrowserFilterComp extends Comp<BrowserFilterComp.Structure> {
return box;
}
}
private final OpenFileSystemModel model;
private final Property<String> filterString;
public BrowserFilterComp(OpenFileSystemModel model, Property<String> filterString) {
this.model = model;
this.filterString = filterString;
}
}

View file

@ -1,6 +1,8 @@
package io.xpipe.app.browser;
import atlantafx.base.theme.Styles;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.fxcomps.SimpleComp;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
@ -11,6 +13,16 @@ public class BrowserGreetingComp extends SimpleComp {
@Override
protected Region createSimple() {
var r = new Label(getText());
AppLayoutModel.get().getSelected().addListener((observableValue, entry, t1) -> {
r.setText(getText());
});
AppFont.setSize(r, 7);
r.getStyleClass().add(Styles.TEXT_BOLD);
return r;
}
private String getText() {
var ldt = LocalDateTime.now();
var hour = ldt.getHour();
String text;
@ -21,8 +33,6 @@ public class BrowserGreetingComp extends SimpleComp {
} else {
text = "Good afternoon";
}
var r = new Label(text);
AppFont.setSize(r, 7);
return r;
return text;
}
}

View file

@ -33,6 +33,7 @@ public class BrowserModel {
private final BrowserTransferModel localTransfersStage = new BrowserTransferModel(this);
private final ObservableList<BrowserEntry> selection = FXCollections.observableArrayList();
private final BrowserSavedState savedState;
@Setter
private Consumer<List<FileReference>> onFinish;
@ -70,12 +71,21 @@ public class BrowserModel {
public void reset() {
synchronized (BrowserModel.this) {
for (OpenFileSystemModel o : new ArrayList<>(openFileSystems)) {
// Don't close busy connections gracefully
// as we otherwise might lock up
if (o.isBusy()) {
continue;
}
closeFileSystemSync(o);
}
if (savedState != null) {
savedState.save();
}
}
// Delete all files
localTransfersStage.clear();
}
public void finishChooser() {
@ -95,8 +105,10 @@ public class BrowserModel {
return;
}
var stores = chosen.stream().map(
entry -> new FileReference(selected.getValue().getEntry(), entry.getRawFileEntry().getPath())).toList();
var stores = chosen.stream()
.map(entry -> new FileReference(
selected.getValue().getEntry(), entry.getRawFileEntry().getPath()))
.toList();
onFinish.accept(stores);
}
@ -107,8 +119,11 @@ public class BrowserModel {
}
private void closeFileSystemSync(OpenFileSystemModel open) {
if (DataStorage.get().getStoreEntries().contains(open.getEntry().get()) && savedState != null && open.getCurrentPath().get() != null) {
savedState.add(new BrowserSavedState.Entry(open.getEntry().get().getUuid(), open.getCurrentPath().get()));
if (DataStorage.get().getStoreEntries().contains(open.getEntry().get())
&& savedState != null
&& open.getCurrentPath().get() != null) {
savedState.add(new BrowserSavedState.Entry(
open.getEntry().get().getUuid(), open.getCurrentPath().get()));
}
open.closeSync();
synchronized (BrowserModel.this) {
@ -116,7 +131,10 @@ public class BrowserModel {
}
}
public void openFileSystemAsync(DataStoreEntryRef<? extends FileSystemStore> store, FailableFunction<OpenFileSystemModel, String, Exception> path, BooleanProperty externalBusy) {
public void openFileSystemAsync(
DataStoreEntryRef<? extends FileSystemStore> store,
FailableFunction<OpenFileSystemModel, String, Exception> path,
BooleanProperty externalBusy) {
if (store == null) {
return;
}

View file

@ -106,7 +106,6 @@ public class BrowserNavBar extends SimpleComp {
})
.augment(new SimpleCompStructure<>(homeButton));
var historyButton = new Button(null, new FontIcon("mdi2h-history"));
historyButton.setAccessibleText("History");
historyButton.getStyleClass().add(Styles.RIGHT_PILL);
@ -146,7 +145,6 @@ public class BrowserNavBar extends SimpleComp {
.maxHeightProperty()
.bind(((Region) struc.get().getChildren().get(1)).heightProperty());
((Region) struc.get().getChildren().get(2))
.minHeightProperty()
.bind(((Region) struc.get().getChildren().get(1)).heightProperty());
@ -197,7 +195,8 @@ public class BrowserNavBar extends SimpleComp {
cm.getItems().add(current);
}
var b = model.getHistory().getBackwardHistory(Integer.MAX_VALUE).stream().toList();
var b = model.getHistory().getBackwardHistory(Integer.MAX_VALUE).stream()
.toList();
if (!b.isEmpty()) {
cm.getItems().add(new SeparatorMenuItem());
}

View file

@ -9,7 +9,7 @@ import java.util.UUID;
public interface BrowserSavedState {
public void add(Entry entry);
void add(Entry entry);
void save();
@ -18,7 +18,7 @@ public interface BrowserSavedState {
@Value
@Jacksonized
@Builder
public static class Entry {
class Entry {
UUID uuid;
String path;

View file

@ -20,12 +20,6 @@ import java.util.List;
@JsonDeserialize(using = BrowserSavedStateImpl.Deserializer.class)
public class BrowserSavedStateImpl implements BrowserSavedState {
static BrowserSavedStateImpl load() {
return AppCache.get("browser-state", BrowserSavedStateImpl.class, () -> {
return new BrowserSavedStateImpl(FXCollections.observableArrayList());
});
}
@JsonSerialize(as = List.class)
ObservableList<Entry> lastSystems;
@ -33,25 +27,10 @@ public class BrowserSavedStateImpl implements BrowserSavedState {
this.lastSystems = FXCollections.observableArrayList(lastSystems);
}
public static class Deserializer extends StdDeserializer<BrowserSavedStateImpl> {
protected Deserializer() {
super(BrowserSavedStateImpl.class);
}
@Override
@SneakyThrows
public BrowserSavedStateImpl deserialize(JsonParser p, DeserializationContext ctxt) {
var tree = (ObjectNode) JacksonMapper.getDefault().readTree(p);
JavaType javaType = JacksonMapper.getDefault()
.getTypeFactory()
.constructCollectionLikeType(List.class, Entry.class);
List<Entry> ls = JacksonMapper.getDefault().treeToValue(tree.remove("lastSystems"), javaType);
if (ls == null) {
ls = List.of();
}
return new BrowserSavedStateImpl(ls);
}
static BrowserSavedStateImpl load() {
return AppCache.get("browser-state", BrowserSavedStateImpl.class, () -> {
return new BrowserSavedStateImpl(FXCollections.observableArrayList());
});
}
@Override
@ -72,4 +51,24 @@ public class BrowserSavedStateImpl implements BrowserSavedState {
public ObservableList<Entry> getEntries() {
return lastSystems;
}
public static class Deserializer extends StdDeserializer<BrowserSavedStateImpl> {
protected Deserializer() {
super(BrowserSavedStateImpl.class);
}
@Override
@SneakyThrows
public BrowserSavedStateImpl deserialize(JsonParser p, DeserializationContext ctxt) {
var tree = (ObjectNode) JacksonMapper.getDefault().readTree(p);
JavaType javaType =
JacksonMapper.getDefault().getTypeFactory().constructCollectionLikeType(List.class, Entry.class);
List<Entry> ls = JacksonMapper.getDefault().treeToValue(tree.remove("lastSystems"), javaType);
if (ls == null) {
ls = List.of();
}
return new BrowserSavedStateImpl(ls);
}
}
}

View file

@ -31,6 +31,13 @@ import java.util.function.Function;
@AllArgsConstructor
public class BrowserSelectionListComp extends SimpleComp {
ObservableList<FileSystem.FileEntry> list;
Function<FileSystem.FileEntry, ObservableValue<String>> nameTransformation;
public BrowserSelectionListComp(ObservableList<FileSystem.FileEntry> list) {
this(list, entry -> new SimpleStringProperty(FileNames.getFileName(entry.getPath())));
}
public static Image snapshot(ObservableList<FileSystem.FileEntry> list) {
var r = new BrowserSelectionListComp(list).styleClass("drag").createRegion();
var scene = new Scene(r);
@ -41,13 +48,6 @@ public class BrowserSelectionListComp extends SimpleComp {
return r.snapshot(parameters, null);
}
ObservableList<FileSystem.FileEntry> list;
Function<FileSystem.FileEntry, ObservableValue<String>> nameTransformation;
public BrowserSelectionListComp(ObservableList<FileSystem.FileEntry> list) {
this(list, entry -> new SimpleStringProperty(FileNames.getFileName(entry.getPath())));
}
@Override
protected Region createSimple() {
var c = new ListBoxViewComp<>(list, list, entry -> {

View file

@ -2,11 +2,13 @@ package io.xpipe.app.browser;
import atlantafx.base.controls.Spacer;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
import io.xpipe.app.fxcomps.impl.LabelComp;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.util.HumanReadableFormat;
import javafx.beans.binding.Bindings;
import javafx.scene.control.ToolBar;
import javafx.scene.layout.Region;
@ -21,6 +23,57 @@ public class BrowserStatusBarComp extends SimpleComp {
@Override
protected Region createSimple() {
var bar = new ToolBar();
bar.getItems()
.setAll(
createClipboardStatus().createRegion(),
createProgressStatus().createRegion(),
new Spacer(),
createSelectionStatus().createRegion());
bar.getStyleClass().add("status-bar");
bar.setOnDragDetected(event -> {
event.consume();
bar.startFullDrag();
});
AppFont.small(bar);
simulateEmptyCell(bar);
return bar;
}
private Comp<?> createProgressStatus() {
var transferredCount = PlatformThread.sync(Bindings.createStringBinding(
() -> {
return HumanReadableFormat.byteCount(
model.getProgress().getValue().getTransferred());
},
model.getProgress()));
var allCount = PlatformThread.sync(Bindings.createStringBinding(
() -> {
return HumanReadableFormat.byteCount(
model.getProgress().getValue().getTotal());
},
model.getProgress()));
var progressComp = new LabelComp(Bindings.createStringBinding(
() -> {
if (model.getProgress().getValue() == null
|| model.getProgress().getValue().done()) {
return null;
} else {
var name = (model.getProgress().getValue().getName() != null
? " @ " + model.getProgress().getValue().getName() + " "
: "");
return transferredCount.getValue() + " / " + allCount.getValue() + name;
}
},
transferredCount,
allCount,
model.getProgress()));
return progressComp;
}
private Comp<?> createClipboardStatus() {
var cc = PlatformThread.sync(BrowserClipboard.currentCopyClipboard);
var ccCount = Bindings.createStringBinding(
() -> {
@ -32,7 +85,10 @@ public class BrowserStatusBarComp extends SimpleComp {
}
},
cc);
return new LabelComp(ccCount);
}
private Comp<?> createSelectionStatus() {
var selectedCount = PlatformThread.sync(Bindings.createIntegerBinding(
() -> {
return model.getFileList().getSelection().size();
@ -46,7 +102,6 @@ public class BrowserStatusBarComp extends SimpleComp {
.count();
},
model.getFileList().getAll()));
var selectedComp = new LabelComp(Bindings.createStringBinding(
() -> {
if (selectedCount.getValue().intValue() == 0) {
@ -57,19 +112,7 @@ public class BrowserStatusBarComp extends SimpleComp {
},
selectedCount,
allCount));
var bar = new ToolBar();
bar.getItems().setAll(new LabelComp(ccCount).createRegion(), new Spacer(), selectedComp.createRegion());
bar.getStyleClass().add("status-bar");
bar.setOnDragDetected(event -> {
event.consume();
bar.startFullDrag();
});
AppFont.small(bar);
simulateEmptyCell(bar);
return bar;
return selectedComp;
}
private void simulateEmptyCell(Region r) {

View file

@ -9,12 +9,12 @@ import io.xpipe.app.fxcomps.impl.*;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.core.process.OsType;
import io.xpipe.core.store.FileNames;
import javafx.beans.binding.Bindings;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.scene.image.Image;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.AnchorPane;
@ -29,48 +29,61 @@ import java.util.Optional;
public class BrowserTransferComp extends SimpleComp {
private final BrowserTransferModel stage;
private final BrowserTransferModel model;
public BrowserTransferComp(BrowserTransferModel stage) {
this.stage = stage;
public BrowserTransferComp(BrowserTransferModel model) {
this.model = model;
}
@Override
protected Region createSimple() {
var background = new LabelComp(AppI18n.observable("transferDescription"))
.apply(struc -> struc.get().setGraphic(new FontIcon("mdi2d-download-outline")))
.visible(BindingsHelper.persist(Bindings.isEmpty(stage.getItems())));
.visible(BindingsHelper.persist(Bindings.isEmpty(model.getItems())));
var backgroundStack =
new StackComp(List.of(background)).grow(true, true).styleClass("download-background");
var binding = BindingsHelper.mappedContentBinding(stage.getItems(), item -> item.getFileEntry());
var list = new BrowserSelectionListComp(binding, entry -> Bindings.createStringBinding(() -> {
var sourceItem = stage.getItems().stream().filter(item -> item.getFileEntry() == entry).findAny();
if (sourceItem.isEmpty()) {
return "?";
}
var name = sourceItem.get().getFinishedDownload().get() ? "Local" : DataStorage.get().getStoreDisplayName(entry.getFileSystem().getStore()).orElse("?");
return FileNames.getFileName(entry.getPath()) + " (" + name + ")";
}, stage.getAllDownloaded()))
var binding = BindingsHelper.mappedContentBinding(model.getItems(), item -> item.getFileEntry());
var list = new BrowserSelectionListComp(
binding,
entry -> Bindings.createStringBinding(
() -> {
var sourceItem = model.getItems().stream()
.filter(item -> item.getFileEntry() == entry)
.findAny();
if (sourceItem.isEmpty()) {
return "?";
}
var name =
sourceItem.get().downloadFinished().get()
? "Local"
: DataStorage.get()
.getStoreDisplayName(entry.getFileSystem()
.getStore())
.orElse("?");
return FileNames.getFileName(entry.getPath()) + " (" + name + ")";
},
model.getAllDownloaded()))
.apply(struc -> struc.get().setMinHeight(150))
.grow(false, true);
var dragNotice = new LabelComp(stage.getAllDownloaded().flatMap(aBoolean -> aBoolean ? AppI18n.observable("dragLocalFiles") : AppI18n.observable("dragFiles")))
var dragNotice = new LabelComp(model.getAllDownloaded()
.flatMap(aBoolean ->
aBoolean ? AppI18n.observable("dragLocalFiles") : AppI18n.observable("dragFiles")))
.apply(struc -> struc.get().setGraphic(new FontIcon("mdi2e-export")))
.hide(PlatformThread.sync(
BindingsHelper.persist(Bindings.isEmpty(stage.getItems()))))
.hide(PlatformThread.sync(BindingsHelper.persist(Bindings.isEmpty(model.getItems()))))
.grow(true, false)
.apply(struc -> struc.get().setPadding(new Insets(8)));
var downloadButton = new IconButtonComp("mdi2d-download", () -> {
stage.download();
model.download();
})
.hide(BindingsHelper.persist(Bindings.isEmpty(stage.getItems())))
.disable(PlatformThread.sync(stage.getAllDownloaded()))
.hide(BindingsHelper.persist(Bindings.isEmpty(model.getItems())))
.disable(PlatformThread.sync(model.getAllDownloaded()))
.apply(new FancyTooltipAugment<>("downloadStageDescription"));
var clearButton = new IconButtonComp("mdi2c-close", () -> {
stage.clear();
model.clear();
})
.hide(BindingsHelper.persist(Bindings.isEmpty(stage.getItems())));
.hide(BindingsHelper.persist(Bindings.isEmpty(model.getItems())));
var clearPane = Comp.derive(
new HorizontalComp(List.of(downloadButton, clearButton))
.apply(struc -> struc.get().setSpacing(10)),
@ -93,37 +106,56 @@ public class BrowserTransferComp extends SimpleComp {
event.acceptTransferModes(TransferMode.ANY);
event.consume();
}
// Accept drops from outside the app window
if (event.getGestureSource() == null
&& !event.getDragboard().getFiles().isEmpty()) {
event.acceptTransferModes(TransferMode.ANY);
event.consume();
}
});
struc.get().setOnDragDropped(event -> {
// Accept drops from inside the app window
if (event.getGestureSource() != null) {
var files = BrowserClipboard.retrieveDrag(event.getDragboard())
.getEntries();
stage.drop(files);
var drag = BrowserClipboard.retrieveDrag(event.getDragboard());
if (drag == null) {
return;
}
var files = drag.getEntries();
model.drop(
model.getBrowserModel()
.getSelected()
.getValue(),
files);
event.setDropCompleted(true);
event.consume();
}
// Accept drops from outside the app window
if (event.getGestureSource() == null) {
model.dropLocal(event.getDragboard().getFiles());
event.setDropCompleted(true);
event.consume();
}
});
struc.get().setOnDragDetected(event -> {
if (stage.getDownloading().get()) {
if (model.getDownloading().get()) {
return;
}
// Drag within browser
if (!stage.getAllDownloaded().get()) {
var selected = stage.getItems().stream().map(item -> item.getFileEntry()).toList();
Dragboard db = struc.get().startDragAndDrop(TransferMode.COPY);
db.setContent(BrowserClipboard.startDrag(null, selected));
var selected = model.getItems().stream()
.map(BrowserTransferModel.Item::getFileEntry)
.toList();
Dragboard db = struc.get().startDragAndDrop(TransferMode.COPY);
Image image = BrowserSelectionListComp.snapshot(FXCollections.observableList(selected));
db.setDragView(image, -20, 15);
event.setDragDetect(true);
event.consume();
var cc = BrowserClipboard.startDrag(null, selected);
if (cc == null) {
return;
}
// Drag outside browser
var files = stage.getItems().stream()
var files = model.getItems().stream()
.filter(item -> item.downloadFinished().get())
.map(item -> {
try {
var file = item.getLocalFile();
@ -131,40 +163,35 @@ public class BrowserTransferComp extends SimpleComp {
return Optional.<File>empty();
}
return Optional.of(file
.toRealPath()
.toFile());
return Optional.of(
file.toRealPath().toFile());
} catch (IOException e) {
throw new RuntimeException(e);
}
})
.flatMap(Optional::stream)
.toList();
Dragboard db = struc.get().startDragAndDrop(TransferMode.MOVE);
var cc = new ClipboardContent();
cc.putFiles(files);
db.setContent(cc);
var image = BrowserSelectionListComp.snapshot(
FXCollections.observableList(stage.getItems().stream()
.map(item -> item.getFileEntry())
.toList()));
Image image = BrowserSelectionListComp.snapshot(FXCollections.observableList(selected));
db.setDragView(image, -20, 15);
event.setDragDetect(true);
event.consume();
});
struc.get().setOnDragDone(event -> {
// macOS does always report false here
if (!event.isAccepted()) {
// macOS does always report false here, which is unfortunate
if (!event.isAccepted() && !OsType.getLocal().equals(OsType.MACOS)) {
return;
}
stage.getItems().clear();
// Don't clear, it might be more convenient to keep the contents
// model.clear();
event.consume();
});
}),
PlatformThread.sync(stage.getDownloading()));
PlatformThread.sync(model.getDownloading()));
return stack.styleClass("transfer").createRegion();
}
}

View file

@ -2,16 +2,23 @@ package io.xpipe.app.browser;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.ShellTemp;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileSystem;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableBooleanValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import lombok.Value;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
@ -21,8 +28,7 @@ import java.util.concurrent.Executors;
@Value
public class BrowserTransferModel {
private static final Path TEMP =
FileUtils.getTempDirectory().toPath().resolve("xpipe").resolve("download");
private static final Path TEMP = ShellTemp.getLocalTempDataDirectory("download");
ExecutorService executor = Executors.newSingleThreadExecutor(r -> {
Thread t = Executors.defaultThreadFactory().newThread(r);
@ -30,30 +36,32 @@ public class BrowserTransferModel {
t.setName("file downloader");
return t;
});
@Value
public static class Item {
String name;
FileSystem.FileEntry fileEntry;
Path localFile;
BooleanProperty finishedDownload = new SimpleBooleanProperty();
}
BrowserModel browserModel;
ObservableList<Item> items = FXCollections.observableArrayList();
BooleanProperty downloading = new SimpleBooleanProperty();
BooleanProperty allDownloaded = new SimpleBooleanProperty();
public void clear() {
try {
FileUtils.deleteDirectory(TEMP.toFile());
private void cleanDirectory() {
if (!Files.isDirectory(TEMP)) {
return;
}
try (var ls = Files.list(TEMP)) {
var list = ls.toList();
for (Path path : list) {
FileUtils.forceDelete(path.toFile());
}
} catch (IOException e) {
ErrorEvent.fromThrowable(e).handle();
}
}
public void clear() {
cleanDirectory();
items.clear();
}
public void drop(List<FileSystem.FileEntry> entries) {
public void drop(OpenFileSystemModel model, List<FileSystem.FileEntry> entries) {
entries.forEach(entry -> {
var name = FileNames.getFileName(entry.getPath());
if (items.stream().anyMatch(item -> item.getName().equals(name))) {
@ -61,12 +69,39 @@ public class BrowserTransferModel {
}
Path file = TEMP.resolve(name);
var item = new Item(name, entry, file);
var item = new Item(model, name, entry, file);
items.add(item);
allDownloaded.set(false);
});
}
public void dropLocal(List<File> entries) {
if (entries.isEmpty()) {
return;
}
var empty = items.isEmpty();
try {
var paths = entries.stream().map(File::toPath).filter(Files::exists).toList();
for (Path path : paths) {
var entry = FileSystemHelper.getLocal(path);
var name = entry.getName();
if (items.stream().anyMatch(item -> item.getName().equals(name))) {
return;
}
var item = new Item(null, name, entry, path);
item.progress.setValue(BrowserTransferProgress.finished(entry.getName(), entry.getSize()));
items.add(item);
}
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
}
if (empty) {
allDownloaded.set(true);
}
}
public void download() {
executor.submit(() -> {
try {
@ -77,18 +112,23 @@ public class BrowserTransferModel {
}
for (Item item : new ArrayList<>(items)) {
if (item.getFinishedDownload().get()) {
if (item.downloadFinished().get()) {
continue;
}
if (item.getOpenFileSystemModel() != null
&& item.getOpenFileSystemModel().isClosed()) {
continue;
}
try {
try (var b = new BooleanScope(downloading).start()) {
FileSystemHelper.dropFilesInto(
FileSystemHelper.getLocal(TEMP),
List.of(item.getFileEntry()),
true);
FileSystemHelper.getLocal(TEMP), List.of(item.getFileEntry()), true, false, progress -> {
item.getProgress().setValue(progress);
item.getOpenFileSystemModel().getProgress().setValue(progress);
});
}
item.finishedDownload.set(true);
} catch (Throwable t) {
ErrorEvent.fromThrowable(t).handle();
items.remove(item);
@ -97,4 +137,31 @@ public class BrowserTransferModel {
allDownloaded.set(true);
});
}
@Value
public static class Item {
OpenFileSystemModel openFileSystemModel;
String name;
FileSystem.FileEntry fileEntry;
Path localFile;
Property<BrowserTransferProgress> progress;
public Item(
OpenFileSystemModel openFileSystemModel, String name, FileSystem.FileEntry fileEntry, Path localFile) {
this.openFileSystemModel = openFileSystemModel;
this.name = name;
this.fileEntry = fileEntry;
this.localFile = localFile;
this.progress =
new SimpleObjectProperty<>(BrowserTransferProgress.empty(fileEntry.getName(), fileEntry.getSize()));
}
public ObservableBooleanValue downloadFinished() {
return Bindings.createBooleanBinding(
() -> {
return progress.getValue().done();
},
progress);
}
}
}

View file

@ -0,0 +1,27 @@
package io.xpipe.app.browser;
import lombok.Value;
@Value
public class BrowserTransferProgress {
String name;
long transferred;
long total;
static BrowserTransferProgress empty() {
return new BrowserTransferProgress(null, 0, 0);
}
static BrowserTransferProgress empty(String name, long size) {
return new BrowserTransferProgress(name, 0, size);
}
static BrowserTransferProgress finished(String name, long size) {
return new BrowserTransferProgress(name, size, size);
}
public boolean done() {
return transferred >= total;
}
}

View file

@ -1,10 +1,10 @@
package io.xpipe.app.browser;
import atlantafx.base.controls.Spacer;
import atlantafx.base.theme.Styles;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.comp.base.ListBoxViewComp;
import io.xpipe.app.comp.base.TileButtonComp;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.LabelComp;
@ -42,7 +42,9 @@ public class BrowserWelcomeComp extends SimpleComp {
var vbox = new VBox(welcome, new Spacer(4, Orientation.VERTICAL));
vbox.setAlignment(Pos.CENTER_LEFT);
var img = PrettyImageHelper.ofSvg(new SimpleStringProperty("Hips.svg"), 50, 75).padding(new Insets(5, 0, 0, 0)).createRegion();
var img = PrettyImageHelper.ofSvg(new SimpleStringProperty("Hips.svg"), 50, 75)
.padding(new Insets(5, 0, 0, 0))
.createRegion();
var hbox = new HBox(img, vbox);
hbox.setAlignment(Pos.CENTER_LEFT);
hbox.setSpacing(15);
@ -68,33 +70,47 @@ public class BrowserWelcomeComp extends SimpleComp {
});
var empty = Bindings.createBooleanBinding(() -> list.isEmpty(), list);
var header = new LabelComp(Bindings.createStringBinding(() -> {
return !empty.get() ? "You were recently connected to the following systems:" :
"Here you will be able to see where you left off last time.";
}, empty)).createRegion();
header.getStyleClass().add(Styles.TEXT_MUTED);
var header = new LabelComp(Bindings.createStringBinding(
() -> {
return !empty.get()
? "You were recently connected to the following systems:"
: "Here you will be able to see where you left off last time.";
},
empty))
.createRegion();
AppFont.setSize(header, 1);
vbox.getChildren().add(header);
var storeList = new VBox();
storeList.setSpacing(8);
var listBox = new ListBoxViewComp<>(list, list, e -> {
var entry = DataStorage.get().getStoreEntryIfPresent(e.getUuid());
var graphic = entry.get().getProvider().getDisplayIconFileName(entry.get().getStore());
var view = PrettyImageHelper.ofFixedSize(graphic, 50, 40);
view.padding(new Insets(2, 8, 2, 8));
var content =
JfxHelper.createNamedEntry(DataStorage.get().getStoreDisplayName(entry.get()), e.getPath(), graphic);
var disable = new SimpleBooleanProperty();
return new ButtonComp(null, content, () -> {
ThreadHelper.runAsync(() -> {
model.restoreStateAsync(e, disable);
});
}).accessibleText(DataStorage.get().getStoreDisplayName(entry.get())).disable(disable).styleClass("color-listBox").apply(struc -> struc.get().setMaxWidth(2000)).grow(true, false);
}).apply(struc -> {
VBox vBox = (VBox) struc.get().getContent();
vBox.setSpacing(10);
}).hide(empty).createRegion();
var entry = DataStorage.get().getStoreEntryIfPresent(e.getUuid());
var graphic = entry.get()
.getProvider()
.getDisplayIconFileName(entry.get().getStore());
var view = PrettyImageHelper.ofFixedSize(graphic, 50, 40);
view.padding(new Insets(2, 8, 2, 8));
var content = JfxHelper.createNamedEntry(
DataStorage.get().getStoreDisplayName(entry.get()), e.getPath(), graphic);
var disable = new SimpleBooleanProperty();
return new ButtonComp(null, content, () -> {
ThreadHelper.runAsync(() -> {
model.restoreStateAsync(e, disable);
});
})
.accessibleText(DataStorage.get().getStoreDisplayName(entry.get()))
.disable(disable)
.styleClass("color-listBox")
.apply(struc -> struc.get().setMaxWidth(2000))
.grow(true, false);
})
.apply(struc -> {
VBox vBox = (VBox) struc.get().getContent();
vBox.setSpacing(10);
})
.hide(empty)
.createRegion();
var layout = new VBox();
layout.getStyleClass().add("welcome");
@ -107,9 +123,12 @@ public class BrowserWelcomeComp extends SimpleComp {
layout.getChildren().add(Comp.separator().hide(empty).createRegion());
var tile = new TileButtonComp("restore", "restoreAllSessions", "mdmz-restore", actionEvent -> {
model.restoreState(state);
actionEvent.consume();
}).grow(true, false).hide(empty).accessibleTextKey("restoreAllSessions");
model.restoreState(state);
actionEvent.consume();
})
.grow(true, false)
.hide(empty)
.accessibleTextKey("restoreAllSessions");
layout.getChildren().add(tile.createRegion());
return layout;

View file

@ -7,13 +7,22 @@ import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileSystem;
import io.xpipe.core.store.LocalStore;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
public class FileSystemHelper {
private static final int DEFAULT_BUFFER_SIZE = 16384;
private static FileSystem localFileSystem;
public static String adjustPath(OpenFileSystemModel model, String path) {
if (path == null) {
return null;
@ -114,7 +123,8 @@ public class FileSystemHelper {
}
if (!model.getFileSystem().directoryExists(path)) {
throw ErrorEvent.unreportable(new IllegalArgumentException(String.format("Directory %s does not exist", path)));
throw ErrorEvent.unreportable(
new IllegalArgumentException(String.format("Directory %s does not exist", path)));
}
try {
@ -125,8 +135,6 @@ public class FileSystemHelper {
}
}
private static FileSystem localFileSystem;
public static FileSystem.FileEntry getLocal(Path file) throws Exception {
if (localFileSystem == null) {
localFileSystem = new LocalStore().createFileSystem();
@ -144,25 +152,22 @@ public class FileSystemHelper {
Files.isDirectory(file) ? FileKind.DIRECTORY : FileKind.FILE);
}
public static void dropLocalFilesInto(FileSystem.FileEntry entry, List<Path> files) {
try {
var entries = files.stream()
.map(path -> {
try {
return getLocal(path);
} catch (Exception e) {
throw new RuntimeException(e);
}
})
.toList();
dropFilesInto(entry, entries, false);
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
}
public static void dropLocalFilesInto(
FileSystem.FileEntry entry, List<Path> files, Consumer<BrowserTransferProgress> progress, boolean checkConflicts) throws Exception {
var entries = files.stream()
.map(path -> {
try {
return getLocal(path);
} catch (Exception e) {
throw new RuntimeException(e);
}
})
.toList();
dropFilesInto(entry, entries, false, checkConflicts, progress);
}
public static void delete(List<FileSystem.FileEntry> files) {
if (files.size() == 0) {
if (files.isEmpty()) {
return;
}
@ -176,22 +181,43 @@ public class FileSystemHelper {
}
public static void dropFilesInto(
FileSystem.FileEntry target, List<FileSystem.FileEntry> files, boolean explicitCopy) throws Exception {
if (files.size() == 0) {
FileSystem.FileEntry target,
List<FileSystem.FileEntry> files,
boolean explicitCopy,
boolean checkConflicts,
Consumer<BrowserTransferProgress> progress)
throws Exception {
if (files.isEmpty()) {
progress.accept(BrowserTransferProgress.empty());
return;
}
var same = files.getFirst().getFileSystem().equals(target.getFileSystem());
if (same && !explicitCopy) {
if (!BrowserAlerts.showMoveAlert(files, target)) {
return;
}
}
AtomicReference<BrowserAlerts.FileConflictChoice> lastConflictChoice = new AtomicReference<>();
for (var file : files) {
if (file.getFileSystem().equals(target.getFileSystem())) {
dropFileAcrossSameFileSystem(target, file, explicitCopy);
dropFileAcrossSameFileSystem(target, file, explicitCopy, lastConflictChoice, files.size() > 1, checkConflicts);
progress.accept(BrowserTransferProgress.finished(file.getName(), file.getSize()));
} else {
dropFileAcrossFileSystems(target, file);
dropFileAcrossFileSystems(target, file, progress, lastConflictChoice, files.size() > 1, checkConflicts);
}
}
}
private static void dropFileAcrossSameFileSystem(
FileSystem.FileEntry target, FileSystem.FileEntry source, boolean explicitCopy) throws Exception {
FileSystem.FileEntry target,
FileSystem.FileEntry source,
boolean explicitCopy,
AtomicReference<BrowserAlerts.FileConflictChoice> lastConflictChoice,
boolean multiple,
boolean checkConflicts)
throws Exception {
// Prevent dropping directory into itself
if (source.getPath().equals(target.getPath())) {
return;
@ -205,7 +231,12 @@ public class FileSystemHelper {
}
if (source.getKind() == FileKind.DIRECTORY && target.getFileSystem().directoryExists(targetFile)) {
throw ErrorEvent.unreportable(new IllegalArgumentException("Target directory " + targetFile + " does already exist"));
throw ErrorEvent.unreportable(
new IllegalArgumentException("Target directory " + targetFile + " does already exist"));
}
if (checkConflicts && !handleChoice(lastConflictChoice, target.getFileSystem(), targetFile, multiple)) {
return;
}
if (explicitCopy) {
@ -215,7 +246,13 @@ public class FileSystemHelper {
}
}
private static void dropFileAcrossFileSystems(FileSystem.FileEntry target, FileSystem.FileEntry source)
private static void dropFileAcrossFileSystems(
FileSystem.FileEntry target,
FileSystem.FileEntry source,
Consumer<BrowserTransferProgress> progress,
AtomicReference<BrowserAlerts.FileConflictChoice> lastConflictChoice,
boolean multiple,
boolean checkConflicts)
throws Exception {
if (target.getKind() != FileKind.DIRECTORY) {
throw new IllegalStateException("Target " + target.getPath() + " is not a directory");
@ -229,19 +266,27 @@ public class FileSystemHelper {
return;
}
AtomicLong totalSize = new AtomicLong();
if (source.getKind() == FileKind.DIRECTORY) {
var directoryName = FileNames.getFileName(source.getPath());
flatFiles.put(source, directoryName);
var baseRelative = FileNames.toDirectory(FileNames.getParent(source.getPath()));
List<FileSystem.FileEntry> list = source.getFileSystem().listFilesRecursively(source.getPath());
list.forEach(fileEntry -> {
for (FileSystem.FileEntry fileEntry : list) {
flatFiles.put(fileEntry, FileNames.toUnix(FileNames.relativize(baseRelative, fileEntry.getPath())));
});
if (fileEntry.getKind() == FileKind.FILE) {
// This one is up-to-date and does not need to be recalculated
totalSize.addAndGet(fileEntry.getSize());
}
}
} else {
flatFiles.put(source, FileNames.getFileName(source.getPath()));
// Recalculate as it could have been changed meanwhile
totalSize.addAndGet(source.getFileSystem().getFileSize(source.getPath()));
}
AtomicLong transferred = new AtomicLong();
for (var e : flatFiles.entrySet()) {
var sourceFile = e.getKey();
var targetFile = FileNames.join(target.getPath(), e.getValue());
@ -252,11 +297,127 @@ public class FileSystemHelper {
if (sourceFile.getKind() == FileKind.DIRECTORY) {
target.getFileSystem().mkdirs(targetFile);
} else if (sourceFile.getKind() == FileKind.FILE) {
try (var in = sourceFile.getFileSystem().openInput(sourceFile.getPath());
var out = target.getFileSystem().openOutput(targetFile)) {
in.transferTo(out);
if (checkConflicts && !handleChoice(
lastConflictChoice, target.getFileSystem(), targetFile, multiple || flatFiles.size() > 1)) {
continue;
}
InputStream inputStream = null;
OutputStream outputStream = null;
try {
var fileSize = sourceFile.getFileSystem().getFileSize(sourceFile.getPath());
inputStream = sourceFile.getFileSystem().openInput(sourceFile.getPath());
outputStream = target.getFileSystem().openOutput(targetFile, fileSize);
transferFile(sourceFile, inputStream, outputStream, transferred, totalSize, progress);
inputStream.transferTo(OutputStream.nullOutputStream());
} catch (Exception ex) {
// Mark progress as finished to reset any progress display
progress.accept(BrowserTransferProgress.finished(sourceFile.getName(), transferred.get()));
if (inputStream != null) {
try {
inputStream.close();
} catch (Exception om) {
// This is expected as the process control has to be killed
// When calling close, it will throw an exception when it has to kill
// ErrorEvent.fromThrowable(om).handle();
}
}
if (outputStream != null) {
try {
outputStream.close();
} catch (Exception om) {
// This is expected as the process control has to be killed
// When calling close, it will throw an exception when it has to kill
// ErrorEvent.fromThrowable(om).handle();
}
}
throw ex;
}
Exception exception = null;
try {
inputStream.close();
} catch (Exception om) {
exception = om;
}
try {
outputStream.close();
} catch (Exception om) {
if (exception != null) {
ErrorEvent.fromThrowable(om).handle();
} else {
exception = om;
}
}
if (exception != null) {
throw exception;
}
}
}
progress.accept(BrowserTransferProgress.finished(source.getName(), totalSize.get()));
}
private static boolean handleChoice(
AtomicReference<BrowserAlerts.FileConflictChoice> previous,
FileSystem fileSystem,
String target,
boolean multiple)
throws Exception {
if (previous.get() == BrowserAlerts.FileConflictChoice.CANCEL) {
return false;
}
if (previous.get() == BrowserAlerts.FileConflictChoice.REPLACE_ALL) {
return true;
}
if (fileSystem.fileExists(target)) {
if (previous.get() == BrowserAlerts.FileConflictChoice.SKIP_ALL) {
return false;
}
var choice = BrowserAlerts.showFileConflictAlert(target, multiple);
if (choice == BrowserAlerts.FileConflictChoice.CANCEL) {
previous.set(BrowserAlerts.FileConflictChoice.CANCEL);
return false;
}
if (choice == BrowserAlerts.FileConflictChoice.SKIP) {
return false;
}
if (choice == BrowserAlerts.FileConflictChoice.SKIP_ALL) {
previous.set(BrowserAlerts.FileConflictChoice.SKIP_ALL);
return false;
}
if (choice == BrowserAlerts.FileConflictChoice.REPLACE_ALL) {
previous.set(BrowserAlerts.FileConflictChoice.REPLACE_ALL);
return true;
}
}
return true;
}
private static void transferFile(
FileSystem.FileEntry sourceFile,
InputStream inputStream,
OutputStream outputStream,
AtomicLong transferred,
AtomicLong total,
Consumer<BrowserTransferProgress> progress)
throws IOException {
// Initialize progress immediately prior to reading anything
progress.accept(new BrowserTransferProgress(sourceFile.getName(), transferred.get(), total.get()));
var bs = (int) Math.min(DEFAULT_BUFFER_SIZE, sourceFile.getSize());
byte[] buffer = new byte[bs];
int read;
while ((read = inputStream.read(buffer, 0, bs)) > 0) {
outputStream.write(buffer, 0, read);
transferred.addAndGet(read);
progress.accept(new BrowserTransferProgress(sourceFile.getName(), transferred.get(), total.get()));
}
}
}

View file

@ -6,9 +6,12 @@ import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.TerminalHelper;
import io.xpipe.app.util.TerminalLauncher;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.process.*;
import io.xpipe.core.process.ProcessControlProvider;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.process.ShellDialects;
import io.xpipe.core.process.ShellOpenFunction;
import io.xpipe.core.store.*;
import io.xpipe.core.util.FailableConsumer;
import javafx.beans.binding.Bindings;
@ -27,20 +30,21 @@ import java.util.stream.Stream;
public final class OpenFileSystemModel {
private final DataStoreEntryRef<? extends FileSystemStore> entry;
private FileSystem fileSystem;
private final Property<String> filter = new SimpleStringProperty();
private final BrowserFileListModel fileList;
private final ReadOnlyObjectWrapper<String> currentPath = new ReadOnlyObjectWrapper<>();
private final OpenFileSystemHistory history = new OpenFileSystemHistory();
private final BooleanProperty busy = new SimpleBooleanProperty();
private final BrowserModel browserModel;
private OpenFileSystemSavedState savedState;
private OpenFileSystemCache cache;
private final Property<ModalOverlayComp.OverlayContent> overlay = new SimpleObjectProperty<>();
private final BooleanProperty inOverview = new SimpleBooleanProperty();
private final String name;
private final String tooltip;
private boolean local;
private final Property<BrowserTransferProgress> progress =
new SimpleObjectProperty<>(BrowserTransferProgress.empty());
private FileSystem fileSystem;
private OpenFileSystemSavedState savedState;
private OpenFileSystemCache cache;
private int customScriptsStartIndex;
public OpenFileSystemModel(BrowserModel browserModel, DataStoreEntryRef<? extends FileSystemStore> entry) {
@ -56,6 +60,24 @@ public final class OpenFileSystemModel {
fileList = new BrowserFileListModel(this);
}
public boolean isBusy() {
return !progress.getValue().done()
|| (fileSystem != null
&& fileSystem.getShell().isPresent()
&& fileSystem.getShell().get().getLock().isLocked());
}
private void startIfNeeded() throws Exception {
if (fileSystem == null) {
return;
}
var s = fileSystem.getShell();
if (s.isPresent()) {
s.get().start();
}
}
public void withShell(FailableConsumer<ShellControl, Exception> c, boolean refresh) {
ThreadHelper.runFailableAsync(() -> {
if (fileSystem == null) {
@ -131,8 +153,13 @@ public final class OpenFileSystemModel {
return Optional.empty();
}
// Start shell in case we exited
getFileSystem().getShell().orElseThrow().start();
try {
// Start shell in case we exited
startIfNeeded();
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
return Optional.ofNullable(currentPath.get());
}
// Fix common issues with paths
var adjustedPath = FileSystemHelper.adjustPath(this, path);
@ -158,26 +185,19 @@ public final class OpenFileSystemModel {
var directory = currentPath.get();
var name = adjustedPath + " - " + entry.get().getName();
ThreadHelper.runFailableAsync(() -> {
if (ShellDialects.getStartableDialects().stream().anyMatch(dialect -> adjustedPath.startsWith(dialect.getOpenCommand()))) {
TerminalHelper.open(
if (ShellDialects.getStartableDialects().stream()
.anyMatch(dialect -> adjustedPath.startsWith(dialect.getOpenCommand(null)))) {
TerminalLauncher.open(
entry.getEntry(),
name,
fileSystem
.getShell()
.get()
.subShell(processControl -> adjustedPath, (sc) -> adjustedPath)
.withInitSnippet(new SimpleScriptSnippet(
fileSystem
.getShell()
.get()
.getShellDialect()
.getCdCommand(currentPath.get()),
ScriptSnippet.ExecutionType.BOTH)));
directory,
fileSystem.getShell().get().singularSubShell(ShellOpenFunction.of(adjustedPath)));
} else {
TerminalHelper.open(
TerminalLauncher.open(
entry.getEntry(),
name,
fileSystem.getShell().get().command(adjustedPath).withWorkingDirectory(directory));
directory,
fileSystem.getShell().get().command(adjustedPath));
}
});
return Optional.ofNullable(currentPath.get());
@ -227,6 +247,7 @@ public final class OpenFileSystemModel {
private boolean loadFilesSync(String dir) {
try {
if (dir != null) {
startIfNeeded();
var stream = getFileSystem().listFiles(dir);
fileList.setAll(stream);
} else {
@ -247,7 +268,8 @@ public final class OpenFileSystemModel {
return;
}
FileSystemHelper.dropLocalFilesInto(entry, files);
startIfNeeded();
FileSystemHelper.dropLocalFilesInto(entry, files, progress::setValue, true);
refreshSync();
});
});
@ -266,14 +288,10 @@ public final class OpenFileSystemModel {
return;
}
var same = files.get(0).getFileSystem().equals(target.getFileSystem());
if (same && !explicitCopy) {
if (!BrowserAlerts.showMoveAlert(files, target)) {
return;
}
}
FileSystemHelper.dropFilesInto(target, files, explicitCopy);
startIfNeeded();
FileSystemHelper.dropFilesInto(target, files, explicitCopy, true, browserTransferProgress -> {
progress.setValue(browserTransferProgress);
});
refreshSync();
});
});
@ -294,9 +312,11 @@ public final class OpenFileSystemModel {
return;
}
startIfNeeded();
var abs = FileNames.join(getCurrentDirectory().getPath(), name);
if (fileSystem.directoryExists(abs)) {
throw ErrorEvent.unreportable(new IllegalStateException(String.format("Directory %s already exists", abs)));
throw ErrorEvent.unreportable(
new IllegalStateException(String.format("Directory %s already exists", abs)));
}
fileSystem.mkdirs(abs);
@ -320,6 +340,7 @@ public final class OpenFileSystemModel {
return;
}
startIfNeeded();
var abs = FileNames.join(getCurrentDirectory().getPath(), linkName);
fileSystem.symbolicLink(abs, targetFile);
refreshSync();
@ -370,14 +391,12 @@ public final class OpenFileSystemModel {
BooleanScope.execute(busy, () -> {
var fs = entry.getStore().createFileSystem();
if (fs.getShell().isPresent()) {
this.customScriptsStartIndex = fs.getShell().get().getInitCommands().size();
this.customScriptsStartIndex =
fs.getShell().get().getInitCommands().size();
ProcessControlProvider.get().withDefaultScripts(fs.getShell().get());
}
fs.open();
this.fileSystem = fs;
this.local = fs.getShell()
.map(shellControl -> shellControl.hasLocalSystemAccess())
.orElse(false);
this.cache = new OpenFileSystemCache(this);
for (BrowserAction b : BrowserAction.ALL) {
@ -408,21 +427,12 @@ public final class OpenFileSystemModel {
BooleanScope.execute(busy, () -> {
if (fileSystem.getShell().isPresent()) {
var connection = fileSystem.getShell().get();
var snippet = directory != null ? new SimpleScriptSnippet(connection.getShellDialect().getCdCommand(directory),
ScriptSnippet.ExecutionType.BOTH) : null;
if (snippet != null) {
connection.getInitCommands().add(customScriptsStartIndex,snippet);
}
var name = (directory != null ? directory + " - " : "")
+ entry.get().getName();
TerminalLauncher.open(entry.getEntry(), name, directory, connection);
try {
var name = (directory != null ? directory + " - " : "") + entry.get().getName();
TerminalHelper.open(entry.getEntry(), name, connection);
// Restart connection as we will have to start it anyway, so we speed it up by doing it preemptively
connection.start();
} finally {
connection.getInitCommands().remove(snippet);
}
// Restart connection as we will have to start it anyway, so we speed it up by doing it preemptively
startIfNeeded();
}
});
});

View file

@ -33,6 +33,86 @@ import java.util.stream.Collectors;
@JsonDeserialize(using = OpenFileSystemSavedState.Deserializer.class)
public class OpenFileSystemSavedState {
private static final Timer TIMEOUT_TIMER = new Timer(true);
private static final int STORED = 10;
@Setter
private OpenFileSystemModel model;
private String lastDirectory;
@NonNull
private ObservableList<RecentEntry> recentDirectories;
public OpenFileSystemSavedState(String lastDirectory, @NonNull ObservableList<RecentEntry> recentDirectories) {
this.lastDirectory = lastDirectory;
this.recentDirectories = recentDirectories;
}
public OpenFileSystemSavedState() {
lastDirectory = null;
recentDirectories = FXCollections.observableList(new ArrayList<>(STORED));
}
static OpenFileSystemSavedState loadForStore(OpenFileSystemModel model) {
var state = AppCache.get("fs-state-" + model.getEntry().get().getUuid(), OpenFileSystemSavedState.class, () -> {
return new OpenFileSystemSavedState();
});
state.setModel(model);
return state;
}
public void save() {
if (model == null) {
return;
}
AppCache.update("fs-state-" + model.getEntry().get().getUuid(), this);
}
public void cd(String dir) {
if (dir == null) {
lastDirectory = null;
return;
}
lastDirectory = dir;
// After 10 seconds
TIMEOUT_TIMER.schedule(
new TimerTask() {
@Override
public void run() {
// Synchronize with platform thread
Platform.runLater(() -> {
if (model.isClosed()) {
return;
}
if (Objects.equals(lastDirectory, dir)) {
updateRecent(dir);
save();
}
});
}
},
10000);
}
private void updateRecent(String dir) {
var without = FileNames.removeTrailingSlash(dir);
var with = FileNames.toDirectory(dir);
recentDirectories.removeIf(recentEntry ->
Objects.equals(recentEntry.directory, without) || Objects.equals(recentEntry.directory, with));
var o = new RecentEntry(with, Instant.now());
if (recentDirectories.size() < STORED) {
recentDirectories.addFirst(o);
} else {
recentDirectories.removeLast();
recentDirectories.addFirst(o);
}
}
public static class Serializer extends StdSerializer<OpenFileSystemSavedState> {
protected Serializer() {
@ -79,14 +159,6 @@ public class OpenFileSystemSavedState {
}
}
static OpenFileSystemSavedState loadForStore(OpenFileSystemModel model) {
var state = AppCache.get("fs-state-" + model.getEntry().get().getUuid(), OpenFileSystemSavedState.class, () -> {
return new OpenFileSystemSavedState();
});
state.setModel(model);
return state;
}
@Value
@Jacksonized
@Builder
@ -95,76 +167,4 @@ public class OpenFileSystemSavedState {
String directory;
Instant time;
}
@Setter
private OpenFileSystemModel model;
private String lastDirectory;
@NonNull
private ObservableList<RecentEntry> recentDirectories;
public OpenFileSystemSavedState(String lastDirectory, @NonNull ObservableList<RecentEntry> recentDirectories) {
this.lastDirectory = lastDirectory;
this.recentDirectories = recentDirectories;
}
private static final Timer TIMEOUT_TIMER = new Timer(true);
private static final int STORED = 10;
public OpenFileSystemSavedState() {
lastDirectory = null;
recentDirectories = FXCollections.observableList(new ArrayList<>(STORED));
}
public void save() {
if (model == null) {
return;
}
AppCache.update("fs-state-" + model.getEntry().get().getUuid(), this);
}
public void cd(String dir) {
if (dir == null) {
lastDirectory = null;
return;
}
lastDirectory = dir;
// After 10 seconds
TIMEOUT_TIMER.schedule(
new TimerTask() {
@Override
public void run() {
// Synchronize with platform thread
Platform.runLater(() -> {
if (model.isClosed()) {
return;
}
if (Objects.equals(lastDirectory, dir)) {
updateRecent(dir);
save();
}
});
}
},
10000);
}
private void updateRecent(String dir) {
var without = FileNames.removeTrailingSlash(dir);
var with = FileNames.toDirectory(dir);
recentDirectories.removeIf(recentEntry ->
Objects.equals(recentEntry.directory, without) || Objects.equals(recentEntry.directory, with));
var o = new RecentEntry(with, Instant.now());
if (recentDirectories.size() < STORED) {
recentDirectories.add(0, o);
} else {
recentDirectories.remove(recentDirectories.size() - 1);
recentDirectories.add(0, o);
}
}
}

View file

@ -39,7 +39,8 @@ public class StandaloneFileBrowser {
});
}
public static void openSingleFile(Supplier<DataStoreEntryRef<? extends FileSystemStore>> store, Consumer<FileReference> file) {
public static void openSingleFile(
Supplier<DataStoreEntryRef<? extends FileSystemStore>> store, Consumer<FileReference> file) {
PlatformThread.runLaterIfNeeded(() -> {
var model = new BrowserModel(BrowserModel.Mode.SINGLE_FILE_CHOOSER, null);
var comp = new BrowserComp(model)
@ -47,7 +48,7 @@ public class StandaloneFileBrowser {
.apply(struc -> AppFont.normal(struc.get()));
var window = AppWindowHelper.sideWindow(AppI18n.get("openFileTitle"), stage -> comp, false, null);
model.setOnFinish(fileStores -> {
file.accept(fileStores.size() > 0 ? fileStores.get(0) : null);
file.accept(fileStores.size() > 0 ? fileStores.getFirst() : null);
window.close();
});
window.show();
@ -63,7 +64,7 @@ public class StandaloneFileBrowser {
.apply(struc -> AppFont.normal(struc.get()));
var window = AppWindowHelper.sideWindow(AppI18n.get("saveFileTitle"), stage -> comp, true, null);
model.setOnFinish(fileStores -> {
file.setValue(fileStores.size() > 0 ? fileStores.get(0) : null);
file.setValue(fileStores.size() > 0 ? fileStores.getFirst() : null);
window.close();
});
window.show();

View file

@ -13,14 +13,6 @@ import java.util.ServiceLoader;
public interface BrowserAction {
enum Category {
CUSTOM,
OPEN,
NATIVE,
COPY_PASTE,
MUTATION
}
List<BrowserAction> ALL = new ArrayList<>();
static List<LeafAction> getFlattened(OpenFileSystemModel model, List<BrowserEntry> entries) {
@ -39,7 +31,7 @@ public interface BrowserAction {
.orElseThrow();
}
default void init(OpenFileSystemModel model) throws Exception {}
default void init(OpenFileSystemModel model) {}
default String getProFeatureId() {
return null;
@ -75,6 +67,14 @@ public interface BrowserAction {
return true;
}
enum Category {
CUSTOM,
OPEN,
NATIVE,
COPY_PASTE,
MUTATION
}
class Loader implements ModuleLayerLoader {
@Override

View file

@ -7,7 +7,7 @@ import java.util.List;
public class BrowserActionFormatter {
public static String filesArgument(List<BrowserEntry> entries) {
return entries.size() == 1 ? entries.get(0).getOptionallyQuotedFileName() : "(" + entries.size() + ")";
return entries.size() == 1 ? entries.getFirst().getOptionallyQuotedFileName() : "(" + entries.size() + ")";
}
public static String centerEllipsis(String input, int length) {

View file

@ -52,7 +52,8 @@ public interface LeafAction extends BrowserAction {
b.setDisable(!isActive(model, selected));
});
if (getProFeatureId() != null && !LicenseProvider.get().getFeature(getProFeatureId()).isSupported()) {
if (getProFeatureId() != null
&& !LicenseProvider.get().getFeature(getProFeatureId()).isSupported()) {
b.setDisable(true);
b.setGraphic(new FontIcon("mdi2p-professional-hexagon"));
}
@ -83,7 +84,8 @@ public interface LeafAction extends BrowserAction {
mi.setMnemonicParsing(false);
mi.setDisable(!isActive(model, selected));
if (getProFeatureId() != null && !LicenseProvider.get().getFeature(getProFeatureId()).isSupported()) {
if (getProFeatureId() != null
&& !LicenseProvider.get().getFeature(getProFeatureId()).isSupported()) {
mi.setDisable(true);
mi.setText(mi.getText() + " (Pro)");
}

View file

@ -4,7 +4,7 @@ import io.xpipe.app.browser.BrowserEntry;
import io.xpipe.app.browser.OpenFileSystemModel;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.ApplicationHelper;
import io.xpipe.app.util.TerminalHelper;
import io.xpipe.app.util.TerminalLauncher;
import io.xpipe.core.process.ShellControl;
import org.apache.commons.io.FilenameUtils;
@ -24,25 +24,30 @@ public abstract class MultiExecuteAction implements BranchAction {
model.withShell(
pc -> {
for (BrowserEntry entry : entries) {
TerminalHelper.open(model.getEntry().getEntry(), FilenameUtils.getBaseName(
entry.getRawFileEntry().getPath()), pc.command(createCommand(pc, model, entry))
.withWorkingDirectory(model.getCurrentDirectory()
.getPath()));
TerminalLauncher.open(
model.getEntry().getEntry(),
FilenameUtils.getBaseName(
entry.getRawFileEntry().getPath()),
model.getCurrentDirectory() != null
? model.getCurrentDirectory()
.getPath()
: null,
pc.command(createCommand(pc, model, entry)));
}
},
false);
}
@Override
public boolean isApplicable(OpenFileSystemModel model, List<BrowserEntry> entries) {
return AppPrefs.get().terminalType().getValue() != null;
}
@Override
public String getName(OpenFileSystemModel model, List<BrowserEntry> entries) {
var t = AppPrefs.get().terminalType().getValue();
return "in " + (t != null ? t.toTranslatedString() : "?");
}
@Override
public boolean isApplicable(OpenFileSystemModel model, List<BrowserEntry> entries) {
return AppPrefs.get().terminalType().getValue() != null;
}
},
new LeafAction() {
@ -51,7 +56,8 @@ public abstract class MultiExecuteAction implements BranchAction {
model.withShell(
pc -> {
for (BrowserEntry entry : entries) {
var cmd = ApplicationHelper.createDetachCommand(pc, createCommand(pc, model, entry));
var cmd = ApplicationHelper.createDetachCommand(
pc, createCommand(pc, model, entry));
pc.command(cmd)
.withWorkingDirectory(model.getCurrentDirectory()
.getPath())

View file

@ -9,7 +9,10 @@ import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public interface DirectoryType {
@ -71,6 +74,12 @@ public interface DirectoryType {
});
}
String getId();
boolean matches(FileSystem.FileEntry entry);
String getIcon(FileSystem.FileEntry entry, boolean open);
class Simple implements DirectoryType {
@Getter
@ -101,10 +110,4 @@ public interface DirectoryType {
return open ? this.open.getIcon() : this.closed.getIcon();
}
}
String getId();
boolean matches(FileSystem.FileEntry entry);
String getIcon(FileSystem.FileEntry entry, boolean open);
}

View file

@ -53,6 +53,12 @@ public interface FileType {
});
}
String getId();
boolean matches(FileSystem.FileEntry entry);
String getIcon();
@Getter
class Simple implements FileType {
@ -72,7 +78,9 @@ public interface FileType {
return false;
}
return (entry.getExtension() != null && endings.contains("." + entry.getExtension().toLowerCase(Locale.ROOT))) || endings.contains(entry.getName());
return (entry.getExtension() != null
&& endings.contains("." + entry.getExtension().toLowerCase(Locale.ROOT)))
|| endings.contains(entry.getName());
}
@Override
@ -80,10 +88,4 @@ public interface FileType {
return icon.getIcon();
}
}
String getId();
boolean matches(FileSystem.FileEntry entry);
String getIcon();
}

View file

@ -9,6 +9,7 @@ import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
import javafx.beans.binding.Bindings;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
@ -31,13 +32,14 @@ public class AppLayoutComp extends Comp<CompStructure<Pane>> {
model.getSelected())))));
var pane = new BorderPane();
var sidebar = new SideMenuBarComp(model.getSelected(), model.getEntries());
var sidebar = new SideMenuBarComp(model.getSelectedInternal(), model.getEntries());
pane.setCenter(multi.createRegion());
pane.setRight(sidebar.createRegion());
pane.getStyleClass().add("background");
model.getSelected().addListener((c, o, n) -> {
if (o != null && o.equals(model.getEntries().get(2))) {
AppPrefs.get().save();
DataStorage.get().saveAsync();
}
});
AppFont.normal(pane);

View file

@ -0,0 +1,103 @@
package io.xpipe.app.comp.base;
import atlantafx.base.controls.Spacer;
import atlantafx.base.theme.Styles;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppWindowHelper;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import javafx.application.Platform;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Pos;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import java.util.List;
import java.util.function.Function;
public abstract class DialogComp extends Comp<CompStructure<Region>> {
public static void showWindow(String titleKey, Function<Stage, DialogComp> f) {
var loading = new SimpleBooleanProperty();
Platform.runLater(() -> {
var stage = AppWindowHelper.sideWindow(
AppI18n.get(titleKey),
window -> {
var c = f.apply(window);
loading.bind(c.busy());
return c;
},
false,
loading);
stage.show();
});
}
protected Region createStepNavigation() {
HBox buttons = new HBox();
buttons.setFillHeight(true);
var customButton = bottom();
if (customButton != null) {
buttons.getChildren().add(customButton.createRegion());
}
buttons.getChildren().add(new Spacer());
buttons.getStyleClass().add("buttons");
buttons.setSpacing(5);
buttons.setAlignment(Pos.CENTER_RIGHT);
buttons.getChildren()
.addAll(customButtons().stream()
.map(buttonComp -> buttonComp.createRegion())
.toList());
var nextButton = new ButtonComp(AppI18n.observable("finishStep"), null, this::finish)
.apply(struc -> struc.get().setDefaultButton(true))
.styleClass(Styles.ACCENT)
.styleClass("next");
buttons.getChildren().add(nextButton.createRegion());
return buttons;
}
protected List<Comp<?>> customButtons() {
return List.of();
}
@Override
public CompStructure<Region> createBase() {
var sp = scrollPane(content()).createRegion();
VBox vbox = new VBox();
vbox.getChildren().addAll(sp, createStepNavigation());
vbox.getStyleClass().add("dialog-comp");
vbox.setFillWidth(true);
VBox.setVgrow(sp, Priority.ALWAYS);
return new SimpleCompStructure<>(vbox);
}
protected ObservableValue<Boolean> busy() {
return new SimpleBooleanProperty(false);
}
protected abstract void finish();
public abstract Comp<?> content();
protected Comp<?> scrollPane(Comp<?> content) {
var entry = content.styleClass("dialog-content");
return Comp.of(() -> {
var entryR = entry.createRegion();
var sp = new ScrollPane(entryR);
sp.setFitToWidth(true);
entryR.minHeightProperty().bind(sp.heightProperty());
return sp;
});
}
public Comp<?> bottom() {
return null;
}
}

View file

@ -45,7 +45,7 @@ public class IntegratedTextAreaComp extends SimpleComp {
c.getChildren().addAll(textArea, pane);
return c;
}),
paths -> value.setValue(Files.readString(paths.get(0))));
paths -> value.setValue(Files.readString(paths.getFirst())));
return fileDrop.createRegion();
}

View file

@ -1,6 +1,5 @@
package io.xpipe.app.comp.base;
import com.jfoenix.controls.JFXTextField;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.util.PlatformThread;
@ -28,7 +27,7 @@ public class LazyTextFieldComp extends Comp<LazyTextFieldComp.Structure> {
@Override
public LazyTextFieldComp.Structure createBase() {
var sp = new StackPane();
var r = new JFXTextField();
var r = new TextField();
r.setOnKeyPressed(ke -> {
if (ke.getCode().equals(KeyCode.ESCAPE)) {
@ -69,8 +68,7 @@ public class LazyTextFieldComp extends Comp<LazyTextFieldComp.Structure> {
SimpleChangeListener.apply(currentValue, n -> {
PlatformThread.runLaterIfNeeded(() -> {
// Check if control value is the same. Then don't set it as that might cause bugs
if (Objects.equals(r.getText(), n)
|| (n == null && r.getText().isEmpty())) {
if (Objects.equals(r.getText(), n) || (n == null && r.getText().isEmpty())) {
return;
}

View file

@ -66,7 +66,8 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
return new SimpleCompStructure<>(scroll);
}
private void refresh(VBox listView, List<? extends T> shown, List<? extends T> all, Map<T, Region> cache, boolean asynchronous) {
private void refresh(
VBox listView, List<? extends T> shown, List<? extends T> all, Map<T, Region> cache, boolean asynchronous) {
Runnable update = () -> {
// Clear cache of unused values
cache.keySet().removeIf(t -> !all.contains(t));

View file

@ -65,7 +65,8 @@ public class ListSelectorComp<T> extends SimpleComp {
if (showAllSelector) {
var allSelector = new CheckBox(null);
allSelector.setSelected(values.stream().filter(t -> !disable.test(t)).count() == selected.size());
allSelector.setSelected(
values.stream().filter(t -> !disable.test(t)).count() == selected.size());
allSelector.selectedProperty().addListener((observable, oldValue, newValue) -> {
cbs.forEach(checkBox -> {
if (checkBox.isDisabled()) {

View file

@ -16,10 +16,8 @@ import javafx.scene.layout.StackPane;
public class LoadingOverlayComp extends Comp<CompStructure<StackPane>> {
public static LoadingOverlayComp noProgress(Comp<?> comp, ObservableValue<Boolean> loading) {
return new LoadingOverlayComp(comp, loading, new SimpleDoubleProperty(-1));
}
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;
@ -30,6 +28,10 @@ public class LoadingOverlayComp extends Comp<CompStructure<StackPane>> {
this.progress = PlatformThread.sync(progress);
}
public static LoadingOverlayComp noProgress(Comp<?> comp, ObservableValue<Boolean> loading) {
return new LoadingOverlayComp(comp, loading, new SimpleDoubleProperty(-1));
}
@Override
public CompStructure<StackPane> createBase() {
var compStruc = comp.createStructure();
@ -39,6 +41,11 @@ 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());

View file

@ -46,11 +46,14 @@ public class MarkdownComp extends Comp<CompStructure<StackPane>> {
@SneakyThrows
private WebView createWebView() {
var wv = new WebView();
wv.getEngine().setUserDataDirectory(AppProperties.get().getDataDir().resolve("webview").toFile());
wv.getEngine()
.setUserDataDirectory(
AppProperties.get().getDataDir().resolve("webview").toFile());
wv.setPageFill(Color.TRANSPARENT);
var theme = AppPrefs.get() != null && AppPrefs.get().theme.getValue().isDark() ? "web/github-markdown-dark.css" : "web/github-markdown-light.css";
var url = AppResources.getResourceURL(AppResources.XPIPE_MODULE, theme)
.orElseThrow();
var theme = AppPrefs.get() != null && AppPrefs.get().theme.getValue().isDark()
? "web/github-markdown-dark.css"
: "web/github-markdown-light.css";
var url = AppResources.getResourceURL(AppResources.XPIPE_MODULE, theme).orElseThrow();
wv.getEngine().setUserStyleSheetLocation(url.toString());
SimpleChangeListener.apply(PlatformThread.sync(markdown), val -> {

View file

@ -7,15 +7,12 @@ import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.Shortcuts;
import javafx.application.Platform;
import javafx.beans.property.Property;
import javafx.geometry.Insets;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonBar;
import javafx.scene.control.Label;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
@ -23,23 +20,14 @@ import lombok.Value;
public class ModalOverlayComp extends SimpleComp {
private final Comp<?> background;
private final Property<OverlayContent> overlayContent;
public ModalOverlayComp(Comp<?> background, Property<OverlayContent> overlayContent) {
this.background = background;
this.overlayContent = overlayContent;
}
@Value
public static class OverlayContent {
String titleKey;
Comp<?> content;
String finishKey;
Runnable onFinish;
}
private final Comp<?> background;
private final Property<OverlayContent> overlayContent;
@Override
protected Region createSimple() {
var bgRegion = background.createRegion();
@ -62,7 +50,7 @@ public class ModalOverlayComp extends SimpleComp {
if (newValue.finishKey != null) {
var finishButton = new Button(AppI18n.get(newValue.finishKey));
Shortcuts.addShortcut(finishButton, new KeyCodeCombination(KeyCode.ENTER));
finishButton.setDefaultButton(true);
Styles.toggleStyleClass(finishButton, Styles.FLAT);
finishButton.setOnAction(event -> {
newValue.onFinish.run();
@ -96,4 +84,13 @@ public class ModalOverlayComp extends SimpleComp {
});
return pane;
}
@Value
public static class OverlayContent {
String titleKey;
Comp<?> content;
String finishKey;
Runnable onFinish;
}
}

View file

@ -20,6 +20,9 @@ import java.util.Map;
public class OsLogoComp extends SimpleComp {
private static final Map<String, String> ICONS = new HashMap<>();
private static final String LINUX_DEFAULT = "linux-24.png";
private static final String LINUX_DEFAULT_SVG = "linux.svg";
private final StoreEntryWrapper wrapper;
private final ObservableValue<SystemStateComp.State> state;
@ -47,7 +50,8 @@ public class OsLogoComp extends SimpleComp {
return getImage(ons.getOsName());
},
wrapper.getPersistentState(), state));
wrapper.getPersistentState(),
state));
var hide = BindingsHelper.map(img, s -> s != null);
return new StackComp(List.of(
new SystemStateComp(state).hide(hide),
@ -55,9 +59,6 @@ public class OsLogoComp extends SimpleComp {
.createRegion();
}
private static final Map<String, String> ICONS = new HashMap<>();
private static final String LINUX_DEFAULT = "linux-24.png";
private String getImage(String name) {
if (name == null) {
return null;
@ -66,15 +67,21 @@ public class OsLogoComp extends SimpleComp {
if (ICONS.isEmpty()) {
AppResources.with(AppResources.XPIPE_MODULE, "img/os", file -> {
try (var list = Files.list(file)) {
list.filter(path -> path.toString().endsWith(".svg") && !path.toString().endsWith(LINUX_DEFAULT))
.map(path -> FileNames.getFileName(path.toString())).forEach(path -> {
var base = FileNames.getBaseName(path).replace("-dark", "") + "-24.png";
ICONS.put(FileNames.getBaseName(base).split("-")[0], "os/" + base);
});
list.filter(path -> path.toString().endsWith(".svg")
&& !path.toString().endsWith(LINUX_DEFAULT_SVG))
.map(path -> FileNames.getFileName(path.toString()))
.forEach(path -> {
var base = FileNames.getBaseName(path).replace("-dark", "") + "-24.png";
ICONS.put(FileNames.getBaseName(base).split("-")[0], "os/" + base);
});
}
});
}
return ICONS.entrySet().stream().filter(e->name.toLowerCase().contains(e.getKey())).findAny().map(e->e.getValue()).orElse("os/" + LINUX_DEFAULT);
return ICONS.entrySet().stream()
.filter(e -> name.toLowerCase().contains(e.getKey()))
.findAny()
.map(e -> e.getValue())
.orElse("os/" + LINUX_DEFAULT);
}
}

View file

@ -6,6 +6,7 @@ import io.xpipe.app.core.AppLogs;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.augment.Augment;
import io.xpipe.app.fxcomps.impl.FancyTooltipAugment;
import io.xpipe.app.fxcomps.impl.IconButtonComp;
import io.xpipe.app.fxcomps.util.PlatformThread;
@ -14,13 +15,13 @@ import io.xpipe.app.issue.UserReportComp;
import io.xpipe.app.update.UpdateAvailableAlert;
import io.xpipe.app.update.XPipeDistributionType;
import io.xpipe.app.util.Hyperlinks;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.css.PseudoClass;
import javafx.scene.control.Button;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import java.util.List;
@ -39,6 +40,32 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
var vbox = new VBox();
vbox.setFillWidth(true);
var selectedBorder = Bindings.createObjectBinding(
() -> {
var c = Platform.getPreferences().getAccentColor();
return new Border(new BorderStroke(
c, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, new BorderWidths(0, 3, 0, 0)));
},
Platform.getPreferences().accentColorProperty());
var hoverBorder = Bindings.createObjectBinding(
() -> {
var c = Platform.getPreferences().getAccentColor().darker();
return new Border(new BorderStroke(
c, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, new BorderWidths(0, 3, 0, 0)));
},
Platform.getPreferences().accentColorProperty());
var noneBorder = Bindings.createObjectBinding(
() -> {
return new Border(new BorderStroke(
Color.TRANSPARENT,
BorderStrokeStyle.SOLID,
CornerRadii.EMPTY,
new BorderWidths(0, 3, 0, 0)));
},
Platform.getPreferences().accentColorProperty());
var selected = PseudoClass.getPseudoClass("selected");
entries.forEach(e -> {
var b = new IconButtonComp(e.icon(), () -> value.setValue(e)).apply(new FancyTooltipAugment<>(e.name()));
@ -50,22 +77,59 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
struc.get().pseudoClassStateChanged(selected, n.equals(e));
});
});
struc.get()
.borderProperty()
.bind(Bindings.createObjectBinding(
() -> {
if (value.getValue().equals(e)) {
return selectedBorder.get();
}
if (struc.get().isHover()) {
return hoverBorder.get();
}
return noneBorder.get();
},
struc.get().hoverProperty(),
value,
hoverBorder,
selectedBorder,
noneBorder));
});
b.accessibleText(e.name());
vbox.getChildren().add(b.createRegion());
});
Augment<CompStructure<Button>> simpleBorders = struc -> {
struc.get()
.borderProperty()
.bind(Bindings.createObjectBinding(
() -> {
if (struc.get().isHover()) {
return hoverBorder.get();
}
return noneBorder.get();
},
struc.get().hoverProperty(),
value,
hoverBorder,
selectedBorder,
noneBorder));
};
{
var b = new IconButtonComp(
"mdal-bug_report",
() -> {
var b = new IconButtonComp("mdal-bug_report", () -> {
var event = ErrorEvent.fromMessage("User Report");
if (AppLogs.get().isWriteToFile()) {
event.attachment(AppLogs.get().getSessionLogsDirectory());
}
UserReportComp.show(event.build());
})
.apply(new FancyTooltipAugment<>("reportIssue")).accessibleTextKey("reportIssue");
.apply(new FancyTooltipAugment<>("reportIssue"))
.apply(simpleBorders)
.accessibleTextKey("reportIssue");
b.apply(struc -> {
AppFont.setSize(struc.get(), 2);
});
@ -74,26 +138,20 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
{
var b = new IconButtonComp("mdi2g-github", () -> Hyperlinks.open(Hyperlinks.GITHUB))
.apply(new FancyTooltipAugment<>("visitGithubRepository")).accessibleTextKey("visitGithubRepository");
.apply(new FancyTooltipAugment<>("visitGithubRepository"))
.apply(simpleBorders)
.accessibleTextKey("visitGithubRepository");
b.apply(struc -> {
AppFont.setSize(struc.get(), 2);
});
vbox.getChildren().add(b.createRegion());
}
// {
// var b = new IconButtonComp("mdi2c-comment-processing-outline", () -> Hyperlinks.open(Hyperlinks.ROADMAP))
// .apply(new FancyTooltipAugment<>("roadmap"));
// b.apply(struc -> {
// AppFont.setSize(struc.get(), 2);
// });
// vbox.getChildren().add(b.createRegion());
// }
{
var b = new IconButtonComp("mdi2d-discord", () -> Hyperlinks.open(Hyperlinks.DISCORD))
.apply(new FancyTooltipAugment<>("discord")).accessibleTextKey("discord");
.apply(new FancyTooltipAugment<>("discord"))
.apply(simpleBorders)
.accessibleTextKey("discord");
b.apply(struc -> {
AppFont.setSize(struc.get(), 2);
});
@ -102,7 +160,8 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
{
var b = new IconButtonComp("mdi2u-update", () -> UpdateAvailableAlert.showIfNeeded())
.apply(new FancyTooltipAugment<>("updateAvailableTooltip")).accessibleTextKey("updateAvailableTooltip");
.apply(new FancyTooltipAugment<>("updateAvailableTooltip"))
.accessibleTextKey("updateAvailableTooltip");
b.apply(struc -> {
AppFont.setSize(struc.get(), 2);
});
@ -123,7 +182,7 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
filler.setMaxHeight(3000);
vbox.getChildren().add(filler);
VBox.setVgrow(filler, Priority.ALWAYS);
filler.prefWidthProperty().bind(((Region) vbox.getChildren().get(0)).widthProperty());
filler.prefWidthProperty().bind(((Region) vbox.getChildren().getFirst()).widthProperty());
vbox.getStyleClass().add("sidebar-comp");
return new SimpleCompStructure<>(vbox);

View file

@ -15,6 +15,7 @@ public class SideSplitPaneComp extends Comp<SideSplitPaneComp.Structure> {
private final Comp<?> center;
private Double initialWidth;
private Consumer<Double> onDividerChange;
public SideSplitPaneComp(Comp<?> left, Comp<?> center) {
this.left = left;
this.center = center;
@ -36,13 +37,13 @@ public class SideSplitPaneComp extends Comp<SideSplitPaneComp.Structure> {
}
if (!setInitial.get() && initialWidth != null) {
r.getDividers().get(0).setPosition(initialWidth / newValue.doubleValue());
r.getDividers().getFirst().setPosition(initialWidth / newValue.doubleValue());
setInitial.set(true);
}
});
SplitPane.setResizableWithParent(sidebar, false);
r.getDividers().get(0).positionProperty().addListener((observable, oldValue, newValue) -> {
r.getDividers().getFirst().positionProperty().addListener((observable, oldValue, newValue) -> {
if (r.getWidth() <= 0) {
return;
}
@ -52,7 +53,7 @@ public class SideSplitPaneComp extends Comp<SideSplitPaneComp.Structure> {
}
});
r.getStyleClass().add("side-split-pane-comp");
return new Structure(sidebar, c, r, r.getDividers().get(0));
return new Structure(sidebar, c, r, r.getDividers().getFirst());
}
public SideSplitPaneComp withInitialWidth(double val) {

View file

@ -39,7 +39,7 @@ public class StoreToggleComp extends SimpleComp {
},
section.getWrapper().getValidity(),
section.getShowDetails()));
var t = new NamedToggleComp(value, AppI18n.observable(nameKey))
var t = new ToggleSwitchComp(value, AppI18n.observable(nameKey))
.visible(visible)
.disable(disable);
value.addListener((observable, oldValue, newValue) -> {

View file

@ -18,29 +18,12 @@ import org.kordamp.ikonli.javafx.StackedFontIcon;
@Getter
public class SystemStateComp extends SimpleComp {
private final ObservableValue<State> state;
public SystemStateComp(ObservableValue<State> state) {
this.state = state;
}
public enum State {
FAILURE,
SUCCESS,
OTHER;
public static ObservableValue<State> shellState(StoreEntryWrapper w) {
return BindingsHelper.map(w.getPersistentState(),o -> {
if (o instanceof ShellStoreState shellStoreState) {
return shellStoreState.getRunning() != null ? shellStoreState.getRunning() ? SUCCESS : FAILURE : OTHER;
}
return OTHER;
});
}
}
private final ObservableValue<State> state;
@Override
protected Region createSimple() {
var icon = PlatformThread.sync(Bindings.createStringBinding(
@ -58,15 +41,19 @@ public class SystemStateComp extends SimpleComp {
border.getStyleClass().add("outer-icon");
border.setOpacity(0.5);
var success = Styles.toDataURI(".stacked-ikonli-font-icon > .outer-icon { -fx-icon-color: -color-success-emphasis; }");
var failure = Styles.toDataURI(".stacked-ikonli-font-icon > .outer-icon { -fx-icon-color: -color-danger-emphasis; }");
var other = Styles.toDataURI(".stacked-ikonli-font-icon > .outer-icon { -fx-icon-color: -color-accent-emphasis; }");
var success = Styles.toDataURI(
".stacked-ikonli-font-icon > .outer-icon { -fx-icon-color: -color-success-emphasis; }");
var failure =
Styles.toDataURI(".stacked-ikonli-font-icon > .outer-icon { -fx-icon-color: -color-danger-emphasis; }");
var other =
Styles.toDataURI(".stacked-ikonli-font-icon > .outer-icon { -fx-icon-color: -color-accent-emphasis; }");
var pane = new StackedFontIcon();
pane.getChildren().addAll(fi, border);
pane.setAlignment(Pos.CENTER);
var dataClass1 = """
var dataClass1 =
"""
.stacked-ikonli-font-icon > .outer-icon {
-fx-icon-size: 22px;
}
@ -78,9 +65,31 @@ public class SystemStateComp extends SimpleComp {
SimpleChangeListener.apply(PlatformThread.sync(state), val -> {
pane.getStylesheets().removeAll(success, failure, other);
pane.getStylesheets().add(val == State.SUCCESS ? success : val == State.FAILURE ? failure: other);
pane.getStylesheets().add(val == State.SUCCESS ? success : val == State.FAILURE ? failure : other);
});
return pane;
}
public enum State {
FAILURE,
SUCCESS,
OTHER;
public static ObservableValue<State> shellState(StoreEntryWrapper w) {
return BindingsHelper.map(w.getPersistentState(), o -> {
if (o instanceof ShellStoreState s) {
if (s.getShellDialect() != null && !s.getShellDialect().getDumbMode().supportsAnyPossibleInteraction()) {
return SUCCESS;
}
return s.getRunning() != null
? s.getRunning() ? SUCCESS : FAILURE
: OTHER;
}
return OTHER;
});
}
}
}

View file

@ -28,6 +28,18 @@ import java.util.function.Consumer;
@Getter
public class TileButtonComp extends Comp<TileButtonComp.Structure> {
private final ObservableValue<String> name;
private final ObservableValue<String> description;
private final ObservableValue<String> icon;
private final Consumer<ActionEvent> action;
public TileButtonComp(String nameKey, String descriptionKey, String icon, Consumer<ActionEvent> action) {
this.name = AppI18n.observable(nameKey);
this.description = AppI18n.observable(descriptionKey);
this.icon = new SimpleStringProperty(icon);
this.action = action;
}
@Override
public Structure createBase() {
var bt = new Button();
@ -68,7 +80,13 @@ public class TileButtonComp extends Comp<TileButtonComp.Structure> {
fi.setIconSize((int) (size * 0.55));
});
bt.setGraphic(hbox);
return Structure.builder().graphic(fi).button(bt).content(hbox).name(header).description(desc).build();
return Structure.builder()
.graphic(fi)
.button(bt)
.content(hbox)
.name(header)
.description(desc)
.build();
}
@Value
@ -85,16 +103,4 @@ public class TileButtonComp extends Comp<TileButtonComp.Structure> {
return button;
}
}
private final ObservableValue<String> name;
private final ObservableValue<String> description;
private final ObservableValue<String> icon;
private final Consumer<ActionEvent> action;
public TileButtonComp(String nameKey, String descriptionKey, String icon, Consumer<ActionEvent> action) {
this.name = AppI18n.observable(nameKey);
this.description = AppI18n.observable(descriptionKey);
this.icon = new SimpleStringProperty(icon);
this.action = action;
}
}

View file

@ -0,0 +1,37 @@
package io.xpipe.app.comp.base;
import atlantafx.base.controls.ToggleSwitch;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.util.PlatformThread;
import javafx.beans.property.Property;
import javafx.beans.value.ObservableValue;
import javafx.scene.layout.Region;
public class ToggleSwitchComp extends SimpleComp {
private final Property<Boolean> selected;
private final ObservableValue<String> name;
public ToggleSwitchComp(Property<Boolean> selected, ObservableValue<String> name) {
this.selected = selected;
this.name = name;
}
@Override
protected Region createSimple() {
var s = new ToggleSwitch();
s.setSelected(selected.getValue());
s.selectedProperty().addListener((observable, oldValue, newValue) -> {
selected.setValue(newValue);
});
selected.addListener((observable, oldValue, newValue) -> {
PlatformThread.runLaterIfNeeded(() -> {
s.setSelected(newValue);
});
});
if (name != null) {
s.textProperty().bind(PlatformThread.sync(name));
}
return s;
}
}

View file

@ -4,7 +4,6 @@ import io.xpipe.app.core.AppFont;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.augment.GrowAugment;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import javafx.beans.binding.Bindings;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
@ -32,16 +31,26 @@ public class DenseStoreEntryComp extends StoreEntryComp {
: Comp.empty();
information.setGraphic(state.createRegion());
var summary = wrapper.getSummary();
var info = wrapper.getEntry().getProvider().informationString(wrapper);
SimpleChangeListener.apply(grid.hoverProperty(), val -> {
if (val && summary.getValue() != null && wrapper.getEntry().getProvider().alwaysShowSummary()) {
information.textProperty().bind(PlatformThread.sync(summary));
} else {
information.textProperty().bind(PlatformThread.sync(info));
}
});
var summary = wrapper.getSummary();
if (wrapper.getEntry().getProvider() != null) {
information
.textProperty()
.bind(PlatformThread.sync(Bindings.createStringBinding(
() -> {
var val = summary.getValue();
if (val != null
&& grid.isHover()
&& wrapper.getEntry().getProvider().alwaysShowSummary()) {
return val;
} else {
return info.getValue();
}
},
grid.hoverProperty(),
info,
summary)));
}
return information;
}
@ -51,9 +60,12 @@ public class DenseStoreEntryComp extends StoreEntryComp {
grid.setHgap(8);
var name = createName().createRegion();
name.maxWidthProperty().bind(Bindings.createDoubleBinding(() -> {
return grid.getWidth() / 2.5;
}, grid.widthProperty()));
name.maxWidthProperty()
.bind(Bindings.createDoubleBinding(
() -> {
return grid.getWidth() / 2.5;
},
grid.widthProperty()));
if (showIcon) {
var storeIcon = createIcon(30, 24);

View file

@ -12,7 +12,6 @@ public class StandardStoreEntryComp extends StoreEntryComp {
super(entry, content);
}
protected Region createContent() {
var name = createName().createRegion();

View file

@ -1,9 +1,9 @@
package io.xpipe.app.comp.store;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreCategory;
import io.xpipe.app.storage.DataStoreEntry;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
@ -59,13 +59,13 @@ public class StoreCategoryWrapper {
public StoreCategoryWrapper getParent() {
return StoreViewState.get().getCategories().stream()
.filter(storeCategoryWrapper ->
storeCategoryWrapper.getCategory().getUuid().equals(category.getParentCategory()))
.findAny().orElse(null);
storeCategoryWrapper.getCategory().getUuid().equals(category.getParentCategory()))
.findAny()
.orElse(null);
}
public boolean contains(DataStoreEntry entry) {
return entry.getCategoryUuid().equals(category.getUuid())
|| children.stream().anyMatch(storeCategoryWrapper -> storeCategoryWrapper.contains(entry));
public boolean contains(StoreEntryWrapper entry) {
return entry.getEntry().getCategoryUuid().equals(category.getUuid()) || containedEntries.contains(entry);
}
public void select() {
@ -87,6 +87,10 @@ public class StoreCategoryWrapper {
update();
}));
AppPrefs.get().showChildCategoriesInParentCategory().addListener((observable, oldValue, newValue) -> {
update();
});
sortMode.addListener((observable, oldValue, newValue) -> {
category.setSortMode(newValue);
});
@ -97,8 +101,8 @@ public class StoreCategoryWrapper {
DataStoreCategory p = category;
if (newValue) {
while ((p = DataStorage.get()
.getStoreCategoryIfPresent(p.getParentCategory())
.orElse(null))
.getStoreCategoryIfPresent(p.getParentCategory())
.orElse(null))
!= null) {
p.setShare(true);
}
@ -117,17 +121,23 @@ public class StoreCategoryWrapper {
share.setValue(category.isShare());
containedEntries.setAll(StoreViewState.get().getAllEntries().stream()
.filter(entry -> contains(entry.getEntry()))
.filter(entry -> {
return entry.getEntry().getCategoryUuid().equals(category.getUuid())
|| (AppPrefs.get()
.showChildCategoriesInParentCategory()
.get()
&& children.stream()
.anyMatch(storeCategoryWrapper -> storeCategoryWrapper.contains(entry)));
})
.toList());
children.setAll(StoreViewState.get().getCategories().stream()
.filter(storeCategoryWrapper -> getCategory()
.getUuid()
.equals(storeCategoryWrapper.getCategory().getParentCategory()))
.toList());
Optional.ofNullable(getParent())
.ifPresent(storeCategoryWrapper -> {
storeCategoryWrapper.update();
});
Optional.ofNullable(getParent()).ifPresent(storeCategoryWrapper -> {
storeCategoryWrapper.update();
});
}
public String getName() {

View file

@ -1,13 +1,15 @@
package io.xpipe.app.comp.store;
import atlantafx.base.controls.Spacer;
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.MultiStepComp;
import io.xpipe.app.comp.base.PopupMenuButtonComp;
import io.xpipe.app.core.*;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppWindowHelper;
import io.xpipe.app.ext.DataStoreProvider;
import io.xpipe.app.ext.DataStoreProviders;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.augment.GrowAugment;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
@ -25,12 +27,12 @@ import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Insets;
import javafx.scene.control.Alert;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Separator;
import javafx.geometry.Orientation;
import javafx.scene.control.*;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import lombok.AccessLevel;
import lombok.experimental.FieldDefaults;
@ -41,9 +43,10 @@ import java.util.function.Consumer;
import java.util.function.Predicate;
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
public class StoreCreationComp extends MultiStepComp.Step<CompStructure<?>> {
public class StoreCreationComp extends DialogComp {
MultiStepComp parent;
Stage window;
Consumer<DataStoreEntry> consumer;
Property<DataStoreProvider> provider;
Property<DataStore> store;
Predicate<DataStoreProvider> filter;
@ -53,19 +56,22 @@ public class StoreCreationComp extends MultiStepComp.Step<CompStructure<?>> {
BooleanProperty finished = new SimpleBooleanProperty();
ObservableValue<DataStoreEntry> entry;
BooleanProperty changedSinceError = new SimpleBooleanProperty();
BooleanProperty skippable = new SimpleBooleanProperty();
StringProperty name;
DataStoreEntry existingEntry;
boolean staticDisplay;
public StoreCreationComp(
MultiStepComp parent,
Stage window,
Consumer<DataStoreEntry> consumer,
Property<DataStoreProvider> provider,
Property<DataStore> store,
Predicate<DataStoreProvider> filter,
String initialName,
DataStoreEntry existingEntry,
boolean staticDisplay) {
this.parent = parent;
this.window = window;
this.consumer = consumer;
this.provider = provider;
this.store = store;
this.filter = filter;
@ -97,36 +103,42 @@ public class StoreCreationComp extends MultiStepComp.Step<CompStructure<?>> {
newValue.validate();
});
});
this.entry = Bindings.createObjectBinding(() -> {
if (name.getValue() == null || store.getValue() == null) {
return null;
}
this.entry = Bindings.createObjectBinding(
() -> {
if (name.getValue() == null || store.getValue() == null) {
return null;
}
var testE = DataStoreEntry.createNew(
UUID.randomUUID(),
DataStorage.get().getSelectedCategory().getUuid(),
name.getValue(),
store.getValue());
var p = provider.getValue().getDisplayParent(testE);
var testE = DataStoreEntry.createNew(
UUID.randomUUID(),
DataStorage.get().getSelectedCategory().getUuid(),
name.getValue(),
store.getValue());
var p = provider.getValue().getDisplayParent(testE);
var targetCategory = p != null
? p.getCategoryUuid()
: DataStorage.get()
.getSelectedCategory()
.getUuid();
var rootCategory = DataStorage.get().getRootCategory(DataStorage.get().getStoreCategoryIfPresent(targetCategory).orElseThrow());
// Don't put connections in the scripts category ever
if ((provider.getValue().getCreationCategory() == null || !provider.getValue().getCreationCategory().equals(DataStoreProvider.CreationCategory.SCRIPT)) &&
rootCategory.equals(DataStorage.get().getAllScriptsCategory())) {
targetCategory = DataStorage.get().getDefaultCategory().getUuid();
}
var targetCategory = p != null
? p.getCategoryUuid()
: DataStorage.get().getSelectedCategory().getUuid();
var rootCategory = DataStorage.get()
.getRootCategory(DataStorage.get()
.getStoreCategoryIfPresent(targetCategory)
.orElseThrow());
// Don't put connections in the scripts category ever
if ((provider.getValue().getCreationCategory() == null
|| !provider.getValue()
.getCreationCategory()
.equals(DataStoreProvider.CreationCategory.SCRIPT))
&& rootCategory.equals(DataStorage.get().getAllScriptsCategory())) {
targetCategory = DataStorage.get()
.getDefaultConnectionsCategory()
.getUuid();
}
return DataStoreEntry.createNew(
UUID.randomUUID(),
targetCategory,
name.getValue(),
store.getValue());
}, name, store);
return DataStoreEntry.createNew(
UUID.randomUUID(), targetCategory, name.getValue(), store.getValue());
},
name,
store);
}
public static void showEdit(DataStoreEntry e) {
@ -148,16 +160,23 @@ public class StoreCreationComp extends MultiStepComp.Step<CompStructure<?>> {
e);
}
public static void showCreation(DataStoreProvider selected, Predicate<DataStoreProvider> filter) {
public static void showCreation(DataStoreProvider selected, DataStoreProvider.CreationCategory category) {
showCreation(selected != null ? selected.defaultStore() : null, category);
}
public static void showCreation(DataStore base, DataStoreProvider.CreationCategory category) {
show(
null,
selected,
selected != null ? selected.defaultStore() : null,
filter,
base != null ? DataStoreProviders.byStore(base) : null,
base,
dataStoreProvider -> category.equals(dataStoreProvider.getCreationCategory()),
e -> {
try {
DataStorage.get().addStoreEntryIfNotPresent(e);
if (e.getProvider().shouldHaveChildren()) {
if (e.getProvider().shouldHaveChildren()
&& AppPrefs.get()
.openConnectionSearchWindowOnConnectionCreation()
.get()) {
ScanAlert.showAsync(e);
}
} catch (Exception ex) {
@ -178,38 +197,117 @@ public class StoreCreationComp extends MultiStepComp.Step<CompStructure<?>> {
DataStoreEntry existingEntry) {
var prop = new SimpleObjectProperty<>(provider);
var store = new SimpleObjectProperty<>(s);
var loading = new SimpleBooleanProperty();
var name = "addConnection";
Platform.runLater(() -> {
var stage = AppWindowHelper.sideWindow(
AppI18n.get(name),
window -> {
return new MultiStepComp() {
DialogComp.showWindow(
"addConnection",
stage -> new StoreCreationComp(
stage, con, prop, store, filter, initialName, existingEntry, staticDisplay));
}
private final StoreCreationComp creator = new StoreCreationComp(
this, prop, store, filter, initialName, existingEntry, staticDisplay);
private static boolean showInvalidConfirmAlert() {
return AppWindowHelper.showBlockingAlert(alert -> {
alert.setTitle(AppI18n.get("confirmInvalidStoreTitle"));
alert.setHeaderText(AppI18n.get("confirmInvalidStoreHeader"));
alert.getDialogPane()
.setContent(AppWindowHelper.alertContentText(AppI18n.get("confirmInvalidStoreContent")));
alert.setAlertType(Alert.AlertType.CONFIRMATION);
alert.getButtonTypes().clear();
alert.getButtonTypes().add(new ButtonType("Retry", ButtonBar.ButtonData.CANCEL_CLOSE));
alert.getButtonTypes().add(new ButtonType("Skip", ButtonBar.ButtonData.OK_DONE));
})
.map(b -> b.getButtonData().isDefaultButton())
.orElse(false);
}
@Override
protected List<Entry> setup() {
loading.bind(creator.busy);
return List.of(new Entry(AppI18n.observable("a"), creator));
}
@Override
protected List<Comp<?>> customButtons() {
return List.of(new ButtonComp(AppI18n.observable("skip"), null, () -> {
if (showInvalidConfirmAlert()) {
commit();
} else {
finish();
}
})
.visible(skippable));
}
@Override
protected void finish() {
window.close();
if (creator.entry.getValue() != null) {
con.accept(creator.entry.getValue());
}
}
};
},
false,
loading);
stage.show();
@Override
protected ObservableValue<Boolean> busy() {
return busy;
}
@Override
protected void finish() {
if (finished.get()) {
return;
}
if (store.getValue() == null) {
return;
}
// We didn't change anything
if (existingEntry != null && existingEntry.getStore().equals(store.getValue())) {
commit();
return;
}
if (!validator.getValue().validate()) {
var msg = validator
.getValue()
.getValidationResult()
.getMessages()
.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);
changedSinceError.setValue(false);
return;
}
ThreadHelper.runAsync(() -> {
try (var b = new BooleanScope(busy).start()) {
DataStorage.get().addStoreEntryInProgress(entry.getValue());
entry.getValue().validateOrThrow();
commit();
} catch (Throwable ex) {
if (ex instanceof ValidationException) {
ErrorEvent.unreportable(ex);
skippable.set(false);
} else {
skippable.set(true);
}
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);
changedSinceError.setValue(false);
ErrorEvent.fromThrowable(ex).omit().handle();
} finally {
DataStorage.get().removeStoreEntryInProgress(entry.getValue());
}
});
}
@Override
public Comp<?> content() {
return Comp.of(this::createLayout);
}
@Override
protected Comp<?> scrollPane(Comp<?> content) {
var back = super.scrollPane(content);
return new ErrorOverlayComp(back, messageProp);
}
@Override
public Comp<?> bottom() {
var disable = Bindings.createBooleanBinding(
@ -219,7 +317,9 @@ public class StoreCreationComp extends MultiStepComp.Step<CompStructure<?>> {
|| !store.getValue().isComplete()
// When switching providers, both observables change one after another.
// So temporarily there might be a store class mismatch
|| provider.getValue().getStoreClasses().stream().noneMatch(aClass -> aClass.isAssignableFrom(store.getValue().getClass()))
|| provider.getValue().getStoreClasses().stream()
.noneMatch(aClass -> aClass.isAssignableFrom(
store.getValue().getClass()))
|| provider.getValue().createInsightsMarkdown(store.getValue()) == null;
},
provider,
@ -233,22 +333,11 @@ public class StoreCreationComp extends MultiStepComp.Step<CompStructure<?>> {
.createRegion()
: null;
}),
true)
true)
.hide(disable)
.styleClass("button-comp");
}
private static boolean showInvalidConfirmAlert() {
return AppWindowHelper.showBlockingAlert(alert -> {
alert.setTitle(AppI18n.get("confirmInvalidStoreTitle"));
alert.setHeaderText(AppI18n.get("confirmInvalidStoreHeader"));
alert.setContentText(AppI18n.get("confirmInvalidStoreContent"));
alert.setAlertType(Alert.AlertType.CONFIRMATION);
})
.map(b -> b.getButtonData().isDefaultButton())
.orElse(false);
}
private Region createStoreProperties(Comp<?> comp, Validator propVal) {
return new OptionsBuilder()
.addComp(comp, store)
@ -259,18 +348,26 @@ public class StoreCreationComp extends MultiStepComp.Step<CompStructure<?>> {
.build();
}
@Override
public CompStructure<? extends Region> createBase() {
var back = Comp.of(this::createLayout);
var message = new ErrorOverlayComp(back, messageProp);
return message.createStructure();
private void commit() {
if (finished.get()) {
return;
}
finished.setValue(true);
if (entry.getValue() != null) {
consumer.accept(entry.getValue());
}
PlatformThread.runLaterIfNeeded(() -> {
window.close();
});
}
private Region createLayout() {
var layout = new BorderPane();
layout.getStyleClass().add("store-creator");
layout.setPadding(new Insets(20));
var providerChoice = new DataStoreProviderChoiceComp(filter, provider, staticDisplay);
var providerChoice = new StoreProviderChoiceComp(filter, provider, staticDisplay);
if (staticDisplay) {
providerChoice.apply(struc -> struc.get().setDisable(true));
}
@ -297,93 +394,9 @@ public class StoreCreationComp extends MultiStepComp.Step<CompStructure<?>> {
var sep = new Separator();
sep.getStyleClass().add("spacer");
var top = new VBox(providerChoice.createRegion(), sep);
var top = new VBox(providerChoice.createRegion(), new Spacer(7, Orientation.VERTICAL), sep);
top.getStyleClass().add("top");
layout.setTop(top);
return layout;
}
@Override
public boolean canContinue() {
if (provider.getValue() != null) {
var install = provider.getValue().getRequiredAdditionalInstallation();
if (install != null && !AppExtensionManager.getInstance().isInstalled(install)) {
ThreadHelper.runAsync(() -> {
try (var ignored = new BooleanScope(busy).start()) {
AppExtensionManager.getInstance().installIfNeeded(install);
/*
TODO: Use reload
*/
finished.setValue(true);
OperationMode.shutdown(false, false);
PlatformThread.runLaterIfNeeded(parent::next);
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
}
});
return false;
}
}
if (finished.get()) {
return true;
}
if (store.getValue() == null) {
return false;
}
// We didn't change anything
if (existingEntry != null && existingEntry.getStore().equals(store.getValue())) {
return true;
}
if (messageProp.getValue() != null && !changedSinceError.get()) {
if (AppPrefs.get().developerMode().getValue() && showInvalidConfirmAlert()) {
return true;
}
}
if (!validator.getValue().validate()) {
var msg = validator
.getValue()
.getValidationResult()
.getMessages()
.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);
changedSinceError.setValue(false);
return false;
}
ThreadHelper.runAsync(() -> {
try (var b = new BooleanScope(busy).start()) {
DataStorage.get().addStoreEntryInProgress(entry.getValue());
entry.getValue().validateOrThrow();
finished.setValue(true);
PlatformThread.runLaterIfNeeded(parent::next);
} catch (Throwable ex) {
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);
changedSinceError.setValue(false);
if (ex instanceof ValidationException) {
ErrorEvent.unreportable(ex);
}
ErrorEvent.fromThrowable(ex).omit().handle();
} finally {
DataStorage.get().removeStoreEntryInProgress(entry.getValue());
}
});
return false;
}
}

View file

@ -24,37 +24,42 @@ public class StoreCreationMenu {
menu.getItems().add(automatically);
menu.getItems().add(new SeparatorMenuItem());
menu.getItems().add(category("addHost", "mdi2h-home-plus",
DataStoreProvider.CreationCategory.HOST, "ssh"));
menu.getItems().add(category("addHost", "mdi2h-home-plus", DataStoreProvider.CreationCategory.HOST, "ssh"));
menu.getItems().add(category("addShell", "mdi2t-text-box-multiple",
DataStoreProvider.CreationCategory.SHELL, null));
menu.getItems()
.add(category("addShell", "mdi2t-text-box-multiple", DataStoreProvider.CreationCategory.SHELL, null));
menu.getItems().add(category("addScript", "mdi2s-script-text-outline",
DataStoreProvider.CreationCategory.SCRIPT, "script"));
menu.getItems()
.add(category(
"addScript", "mdi2s-script-text-outline", DataStoreProvider.CreationCategory.SCRIPT, "script"));
menu.getItems().add(category("addCommand", "mdi2c-code-greater-than",
DataStoreProvider.CreationCategory.COMMAND, "cmd"));
menu.getItems()
.add(category(
"addCommand", "mdi2c-code-greater-than", DataStoreProvider.CreationCategory.COMMAND, "cmd"));
menu.getItems().add(category("addTunnel", "mdi2v-vector-polyline-plus",
DataStoreProvider.CreationCategory.TUNNEL, null));
menu.getItems()
.add(category(
"addTunnel", "mdi2v-vector-polyline-plus", DataStoreProvider.CreationCategory.TUNNEL, null));
menu.getItems().add(category("addCluster", "mdi2d-domain-plus",
DataStoreProvider.CreationCategory.CLUSTER, null));
menu.getItems().add(category("addDatabase", "mdi2d-database-plus",
DataStoreProvider.CreationCategory.DATABASE, null));
menu.getItems()
.add(category("addDatabase", "mdi2d-database-plus", DataStoreProvider.CreationCategory.DATABASE, null));
}
private static MenuItem category(String name, String graphic, DataStoreProvider.CreationCategory category, String defaultProvider) {
var sub = DataStoreProviders.getAll().stream().filter(dataStoreProvider -> category.equals(dataStoreProvider.getCreationCategory())).toList();
private static MenuItem category(
String name, String graphic, DataStoreProvider.CreationCategory category, String defaultProvider) {
var sub = DataStoreProviders.getAll().stream()
.filter(dataStoreProvider -> category.equals(dataStoreProvider.getCreationCategory()))
.toList();
if (sub.size() < 2) {
var item = new MenuItem();
item.setGraphic(new FontIcon(graphic));
item.textProperty().bind(AppI18n.observable(name));
item.setOnAction(event -> {
StoreCreationComp.showCreation(defaultProvider != null ? DataStoreProviders.byName(defaultProvider).orElseThrow() : null,
v -> category.equals(v.getCreationCategory()));
StoreCreationComp.showCreation(
defaultProvider != null
? DataStoreProviders.byName(defaultProvider).orElseThrow()
: null,
category);
event.consume();
});
return item;
@ -68,16 +73,19 @@ public class StoreCreationMenu {
return;
}
StoreCreationComp.showCreation(defaultProvider != null ? DataStoreProviders.byName(defaultProvider).orElseThrow() : null,
v -> category.equals(v.getCreationCategory()));
StoreCreationComp.showCreation(
defaultProvider != null
? DataStoreProviders.byName(defaultProvider).orElseThrow()
: null,
category);
event.consume();
});
sub.forEach(dataStoreProvider -> {
var item = new MenuItem(dataStoreProvider.getDisplayName());
item.setGraphic(PrettyImageHelper.ofFixedSmallSquare(dataStoreProvider.getDisplayIconFileName(null)).createRegion());
item.setGraphic(PrettyImageHelper.ofFixedSmallSquare(dataStoreProvider.getDisplayIconFileName(null))
.createRegion());
item.setOnAction(event -> {
StoreCreationComp.showCreation(dataStoreProvider,
v -> category.equals(v.getCreationCategory()));
StoreCreationComp.showCreation(dataStoreProvider, category);
event.consume();
});
menu.getItems().add(item);

View file

@ -40,24 +40,6 @@ import java.util.Arrays;
public abstract class StoreEntryComp extends SimpleComp {
public static StoreEntryComp create(
StoreEntryWrapper entry, Comp<?> content, boolean preferLarge) {
if (!preferLarge) {
return new DenseStoreEntryComp(entry, true, content);
} else {
return new StandardStoreEntryComp(entry, content);
}
}
public static Comp<?> customSection(StoreSection e, boolean topLevel) {
var prov = e.getWrapper().getEntry().getProvider();
if (prov != null) {
return prov.customEntryComp(e, topLevel);
} else {
return new StandardStoreEntryComp(e.getWrapper(), null);
}
}
public static final PseudoClass FAILED = PseudoClass.getPseudoClass("failed");
public static final PseudoClass INCOMPLETE = PseudoClass.getPseudoClass("incomplete");
public static final ObservableDoubleValue INFO_NO_CONTENT_WIDTH =
@ -72,6 +54,29 @@ public abstract class StoreEntryComp extends SimpleComp {
this.content = content;
}
public static StoreEntryComp create(StoreEntryWrapper entry, Comp<?> content, boolean preferLarge) {
var forceCondensed = AppPrefs.get() != null
&& AppPrefs.get().condenseConnectionDisplay().get();
if (!preferLarge || forceCondensed) {
return new DenseStoreEntryComp(entry, true, content);
} else {
return new StandardStoreEntryComp(entry, content);
}
}
public static Comp<?> customSection(StoreSection e, boolean topLevel) {
var prov = e.getWrapper().getEntry().getProvider();
if (prov != null) {
return prov.customEntryComp(e, topLevel);
} else {
var forceCondensed = AppPrefs.get() != null
&& AppPrefs.get().condenseConnectionDisplay().get();
return forceCondensed
? new DenseStoreEntryComp(e.getWrapper(), true, null)
: new StandardStoreEntryComp(e.getWrapper(), null);
}
}
@Override
protected final Region createSimple() {
var r = createContent();
@ -83,8 +88,7 @@ public abstract class StoreEntryComp extends SimpleComp {
button.setPadding(Insets.EMPTY);
button.setMaxWidth(5000);
button.setFocusTraversable(true);
button.accessibleTextProperty()
.bind(wrapper.nameProperty());
button.accessibleTextProperty().bind(wrapper.nameProperty());
button.setOnAction(event -> {
event.consume();
ThreadHelper.runFailableAsync(() -> {
@ -105,8 +109,13 @@ public abstract class StoreEntryComp extends SimpleComp {
protected Label createInformation() {
var information = new Label();
information.setGraphicTextGap(7);
information.textProperty().bind(wrapper.getEntry().getProvider() != null ?
PlatformThread.sync(wrapper.getEntry().getProvider().informationString(wrapper)) : new SimpleStringProperty());
information
.textProperty()
.bind(
wrapper.getEntry().getProvider() != null
? PlatformThread.sync(
wrapper.getEntry().getProvider().informationString(wrapper))
: new SimpleStringProperty());
information.getStyleClass().add("information");
AppFont.header(information);
@ -191,15 +200,16 @@ public abstract class StoreEntryComp extends SimpleComp {
continue;
}
var button = new IconButtonComp(
actionProvider.getIcon(wrapper.getEntry().ref()), () -> {
var button =
new IconButtonComp(actionProvider.getIcon(wrapper.getEntry().ref()), () -> {
ThreadHelper.runFailableAsync(() -> {
var action = actionProvider.createAction(
wrapper.getEntry().ref());
action.execute();
});
});
button.accessibleText(actionProvider.getName(wrapper.getEntry().ref()).getValue());
button.accessibleText(
actionProvider.getName(wrapper.getEntry().ref()).getValue());
button.apply(new FancyTooltipAugment<>(
actionProvider.getName(wrapper.getEntry().ref())));
if (actionProvider.activeType() == ActionProvider.DataStoreCallSite.ActiveType.ONLY_SHOW_IF_ENABLED) {
@ -213,11 +223,11 @@ public abstract class StoreEntryComp extends SimpleComp {
var settingsButton = createSettingsButton();
list.add(settingsButton);
if (list.size() > 1) {
list.get(0).styleClass(Styles.LEFT_PILL);
list.getFirst().styleClass(Styles.LEFT_PILL);
for (int i = 1; i < list.size() - 1; i++) {
list.get(i).styleClass(Styles.CENTER_PILL);
}
list.get(list.size() - 1).styleClass(Styles.RIGHT_PILL);
list.getLast().styleClass(Styles.RIGHT_PILL);
}
list.forEach(comp -> {
comp.apply(struc -> struc.get().getStyleClass().remove(Styles.FLAT));
@ -264,11 +274,13 @@ public abstract class StoreEntryComp extends SimpleComp {
? new Menu(null, new FontIcon(icon))
: new MenuItem(null, new FontIcon(icon));
var proRequired = p.getKey().getProFeatureId() != null &&
!LicenseProvider.get().getFeature(p.getKey().getProFeatureId()).isSupported();
var proRequired = p.getKey().getProFeatureId() != null
&& !LicenseProvider.get()
.getFeature(p.getKey().getProFeatureId())
.isSupported();
if (proRequired) {
item.setDisable(true);
item.textProperty().bind(Bindings.createStringBinding(() -> name.getValue() + " (Pro)",name));
item.textProperty().bind(Bindings.createStringBinding(() -> name.getValue() + " (Pro)", name));
} else {
item.textProperty().bind(name);
}
@ -285,8 +297,7 @@ public abstract class StoreEntryComp extends SimpleComp {
contextMenu.hide();
ThreadHelper.runFailableAsync(() -> {
var action = actionProvider.createAction(
wrapper.getEntry().ref());
var action = actionProvider.createAction(wrapper.getEntry().ref());
action.execute();
});
});
@ -302,20 +313,27 @@ public abstract class StoreEntryComp extends SimpleComp {
run.textProperty().bind(AppI18n.observable("base.execute"));
run.setOnAction(event -> {
ThreadHelper.runFailableAsync(() -> {
p.getKey().getDataStoreCallSite().createAction(wrapper.getEntry().ref()).execute();
p.getKey()
.getDataStoreCallSite()
.createAction(wrapper.getEntry().ref())
.execute();
});
});
menu.getItems().add(run);
var sc = new MenuItem(null, new FontIcon("mdi2c-code-greater-than"));
var url = "xpipe://action/" + p.getKey().getId() + "/"
+ wrapper.getEntry().getUuid();
sc.textProperty().bind(AppI18n.observable("base.createShortcut"));
sc.setOnAction(event -> {
ThreadHelper.runFailableAsync(() -> {
DesktopShortcuts.create(url,
wrapper.nameProperty().getValue() + " (" + p.getKey().getDataStoreCallSite().getName(wrapper.getEntry().ref()).getValue() + ")");
DesktopShortcuts.create(
url,
wrapper.nameProperty().getValue() + " ("
+ p.getKey()
.getDataStoreCallSite()
.getName(wrapper.getEntry().ref())
.getValue() + ")");
});
});
menu.getItems().add(sc);
@ -345,20 +363,23 @@ public abstract class StoreEntryComp extends SimpleComp {
contextMenu.getItems().add(browse);
}
if (wrapper.getEntry().getProvider() != null && wrapper.getEntry().getProvider().canMoveCategories()) {
if (wrapper.getEntry().getProvider() != null
&& wrapper.getEntry().getProvider().canMoveCategories()) {
var move = new Menu(AppI18n.get("moveTo"), new FontIcon("mdi2f-folder-move-outline"));
StoreViewState.get().getSortedCategories(wrapper.getCategory().getValue().getRoot()).forEach(storeCategoryWrapper -> {
MenuItem m = new MenuItem(storeCategoryWrapper.getName());
m.setOnAction(event -> {
wrapper.moveTo(storeCategoryWrapper.getCategory());
event.consume();
});
if (storeCategoryWrapper.getParent() == null) {
m.setDisable(true);
}
StoreViewState.get()
.getSortedCategories(wrapper.getCategory().getValue().getRoot())
.forEach(storeCategoryWrapper -> {
MenuItem m = new MenuItem(storeCategoryWrapper.getName());
m.setOnAction(event -> {
wrapper.moveTo(storeCategoryWrapper.getCategory());
event.consume();
});
if (storeCategoryWrapper.getParent() == null) {
m.setDisable(true);
}
move.getItems().add(m);
});
move.getItems().add(m);
});
contextMenu.getItems().add(move);
}
@ -382,9 +403,16 @@ public abstract class StoreEntryComp extends SimpleComp {
}
var del = new MenuItem(AppI18n.get("remove"), new FontIcon("mdal-delete_outline"));
del.disableProperty().bind(Bindings.createBooleanBinding(() -> {
return !wrapper.getDeletable().get() && !AppPrefs.get().developerDisableGuiRestrictions().get();
}, wrapper.getDeletable(), AppPrefs.get().developerDisableGuiRestrictions()));
del.disableProperty()
.bind(Bindings.createBooleanBinding(
() -> {
return !wrapper.getDeletable().get()
&& !AppPrefs.get()
.developerDisableGuiRestrictions()
.get();
},
wrapper.getDeletable(),
AppPrefs.get().developerDisableGuiRestrictions()));
del.setOnAction(event -> wrapper.delete());
contextMenu.getItems().add(del);

View file

@ -35,10 +35,18 @@ public class StoreEntryListComp extends SimpleComp {
var showIntro = Bindings.createBooleanBinding(
() -> {
var all = StoreViewState.get().getAllConnectionsCategory();
var connections = StoreViewState.get().getAllEntries().stream().filter(wrapper -> all.contains(wrapper.getEntry())).toList();
return initialCount == connections.size() && StoreViewState.get().getActiveCategory().getValue().getRoot().equals(StoreViewState.get().getAllConnectionsCategory());
var connections = StoreViewState.get().getAllEntries().stream()
.filter(wrapper -> all.contains(wrapper))
.toList();
return initialCount == connections.size()
&& StoreViewState.get()
.getActiveCategory()
.getValue()
.getRoot()
.equals(StoreViewState.get().getAllConnectionsCategory());
},
StoreViewState.get().getAllEntries(), StoreViewState.get().getActiveCategory());
StoreViewState.get().getAllEntries(),
StoreViewState.get().getActiveCategory());
var map = new LinkedHashMap<Comp<?>, ObservableValue<Boolean>>();
map.put(
createList(),

View file

@ -37,23 +37,43 @@ public class StoreEntryListStatusComp extends SimpleComp {
public StoreEntryListStatusComp() {
this.sortMode = new SimpleObjectProperty<>();
SimpleChangeListener.apply(StoreViewState.get().getActiveCategory(), val -> {
sortMode.unbind();
sortMode.bindBidirectional(val.getSortMode());
sortMode.setValue(val.getSortMode().getValue());
});
sortMode.addListener((observable, oldValue, newValue) -> {
var cat = StoreViewState.get().getActiveCategory().getValue();
if (cat == null) {
return;
}
cat.getSortMode().setValue(newValue);
});
}
private Region createGroupListHeader() {
var label = new Label();
label.textProperty().bind(Bindings.createStringBinding(() -> {
return StoreViewState.get().getActiveCategory().getValue().getRoot().equals(StoreViewState.get().getAllConnectionsCategory()) ? "Connections" : "Scripts";
}, StoreViewState.get().getActiveCategory()));
label.textProperty()
.bind(Bindings.createStringBinding(
() -> {
return StoreViewState.get()
.getActiveCategory()
.getValue()
.getRoot()
.equals(StoreViewState.get().getAllConnectionsCategory())
? "Connections"
: "Scripts";
},
StoreViewState.get().getActiveCategory()));
label.getStyleClass().add("name");
var all = BindingsHelper.filteredContentBinding(
StoreViewState.get().getAllEntries(),
storeEntryWrapper -> {
var storeRoot = storeEntryWrapper.getCategory().getValue().getRoot();
return StoreViewState.get().getActiveCategory().getValue().getRoot().equals(storeRoot);
return StoreViewState.get()
.getActiveCategory()
.getValue()
.getRoot()
.equals(storeRoot);
},
StoreViewState.get().getActiveCategory());
var shownList = BindingsHelper.filteredContentBinding(
@ -66,7 +86,13 @@ public class StoreEntryListStatusComp extends SimpleComp {
var count = new CountComp<>(shownList, all);
var c = count.createRegion();
var topBar = new HBox(label, c, Comp.hspacer().createRegion(), createDateSortButton().createRegion(), Comp.hspacer(2).createRegion(), createAlphabeticalSortButton().createRegion());
var topBar = new HBox(
label,
c,
Comp.hspacer().createRegion(),
createDateSortButton().createRegion(),
Comp.hspacer(2).createRegion(),
createAlphabeticalSortButton().createRegion());
AppFont.setSize(label, 3);
AppFont.setSize(c, 3);
topBar.setAlignment(Pos.CENTER);
@ -87,7 +113,7 @@ public class StoreEntryListStatusComp extends SimpleComp {
});
filter.apply(struc -> struc.get().sceneProperty().addListener((observable, oldValue, newValue) -> {
if (newValue != null) {
struc.getText().requestFocus();
struc.getText().requestFocus();
}
}));
@ -104,7 +130,6 @@ public class StoreEntryListStatusComp extends SimpleComp {
f.setPadding(new Insets(-3, 0, -3, 0));
}
AppFont.medium(hbox);
return hbox;
}

View file

@ -121,7 +121,10 @@ public class StoreEntryWrapper {
deletable.setValue(entry.getConfiguration().isDeletable()
|| AppPrefs.get().developerDisableGuiRestrictions().getValue());
category.setValue(StoreViewState.get().getCategoryWrapper(DataStorage.get().getStoreCategoryIfPresent(entry.getCategoryUuid()).orElseThrow()));
category.setValue(StoreViewState.get()
.getCategoryWrapper(DataStorage.get()
.getStoreCategoryIfPresent(entry.getCategoryUuid())
.orElseThrow()));
if (!entry.getValidity().isUsable()) {
summary.setValue(null);
@ -155,8 +158,7 @@ public class StoreEntryWrapper {
&& e.getDefaultDataStoreCallSite()
.getApplicableClass()
.isAssignableFrom(entry.getStore().getClass())
&& e.getDefaultDataStoreCallSite()
.isApplicable(entry.ref()))
&& e.getDefaultDataStoreCallSite().isApplicable(entry.ref()))
.findFirst()
.map(ActionProvider::getDefaultDataStoreCallSite)
.orElse(null);

View file

@ -1,5 +1,6 @@
package io.xpipe.app.comp.store;
import atlantafx.base.theme.Styles;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.SimpleComp;
@ -7,11 +8,9 @@ import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.ScanAlert;
import javafx.beans.property.SimpleStringProperty;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.Separator;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
@ -23,23 +22,23 @@ public class StoreIntroComp extends SimpleComp {
@Override
public Region createSimple() {
var title = new Label(AppI18n.get("storeIntroTitle"));
title.getStyleClass().add(Styles.TEXT_BOLD);
AppFont.setSize(title, 7);
var introDesc = new Label(AppI18n.get("storeIntroDescription"));
var mfi = new FontIcon("mdi2p-playlist-plus");
var machine = new Label(AppI18n.get("storeMachineDescription"));
machine.heightProperty().addListener((c, o, n) -> {
mfi.iconSizeProperty().set(n.intValue());
});
introDesc.setWrapText(true);
introDesc.setMaxWidth(470);
var scanButton = new Button(AppI18n.get("detectConnections"), new FontIcon("mdi2m-magnify"));
scanButton.setOnAction(event -> ScanAlert.showAsync(DataStorage.get().local()));
scanButton.setDefaultButton(true);
var scanPane = new StackPane(scanButton);
scanPane.setAlignment(Pos.CENTER);
var img = PrettyImageHelper.ofSvg(new SimpleStringProperty("Wave.svg"), 80, 150).createRegion();
var text = new VBox(title, introDesc, new Separator(Orientation.HORIZONTAL), machine);
var img = PrettyImageHelper.ofSvg(new SimpleStringProperty("Wave.svg"), 80, 150)
.createRegion();
var text = new VBox(title, introDesc);
text.setSpacing(5);
text.setAlignment(Pos.CENTER_LEFT);
var hbox = new HBox(img, text);
hbox.setSpacing(35);

View file

@ -19,10 +19,12 @@ public class StoreLayoutComp extends SimpleComp {
@Override
protected Region createSimple() {
var struc = new SideSplitPaneComp(new StoreSidebarComp(), new StoreEntryListComp()).withInitialWidth(
AppLayoutModel.get().getSavedState().getSidebarWidth()).withOnDividerChange(aDouble -> {
AppLayoutModel.get().getSavedState().setSidebarWidth(aDouble);
}).createStructure();
var struc = new SideSplitPaneComp(new StoreSidebarComp(), new StoreEntryListComp())
.withInitialWidth(AppLayoutModel.get().getSavedState().getSidebarWidth())
.withOnDividerChange(aDouble -> {
AppLayoutModel.get().getSavedState().setSidebarWidth(aDouble);
})
.createStructure();
struc.getLeft().setMinWidth(260);
struc.getLeft().setMaxWidth(500);
struc.get().getStyleClass().add("store-layout");

View file

@ -0,0 +1,82 @@
package io.xpipe.app.comp.store;
import io.xpipe.app.ext.DataStoreProvider;
import io.xpipe.app.ext.DataStoreProviders;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.util.JfxHelper;
import javafx.beans.property.Property;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ListCell;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.Region;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.experimental.FieldDefaults;
import java.util.List;
import java.util.function.Predicate;
import java.util.function.Supplier;
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
@AllArgsConstructor
public class StoreProviderChoiceComp extends Comp<CompStructure<ComboBox<DataStoreProvider>>> {
Predicate<DataStoreProvider> filter;
Property<DataStoreProvider> provider;
boolean staticDisplay;
private List<DataStoreProvider> getProviders() {
return DataStoreProviders.getAll().stream()
.filter(val -> filter == null || filter.test(val))
.toList();
}
private Region createGraphic(DataStoreProvider provider) {
if (provider == null) {
return null;
}
var graphic = provider.getDisplayIconFileName(null);
return JfxHelper.createNamedEntry(provider.getDisplayName(), provider.getDisplayDescription(), graphic);
}
@Override
public CompStructure<ComboBox<DataStoreProvider>> createBase() {
Supplier<ListCell<DataStoreProvider>> cellFactory = () -> new ListCell<>() {
@Override
protected void updateItem(DataStoreProvider item, boolean empty) {
super.updateItem(item, empty);
setGraphic(createGraphic(item));
setAccessibleText(item != null ? item.getDisplayName() : null);
setAccessibleHelp(item != null ? item.getDisplayDescription() : null);
}
};
var cb = new ComboBox<DataStoreProvider>();
cb.setCellFactory(param -> {
return cellFactory.get();
});
cb.setButtonCell(cellFactory.get());
var l = getProviders().stream()
.filter(p -> p.getCreationCategory() != null || staticDisplay)
.toList();
l.forEach(dataStoreProvider -> cb.getItems().add(dataStoreProvider));
if (provider.getValue() == null) {
provider.setValue(l.getFirst());
}
cb.setValue(provider.getValue());
provider.bind(cb.valueProperty());
cb.getStyleClass().add("choice-comp");
cb.setAccessibleText("Choose connection type");
cb.setOnKeyPressed(event -> {
if (!event.getCode().equals(KeyCode.ENTER)) {
return;
}
cb.show();
event.consume();
});
return new SimpleCompStructure<>(cb);
}
}

View file

@ -19,15 +19,6 @@ import java.util.function.Predicate;
@Value
public class StoreSection {
public static Comp<?> customSection(StoreSection e, boolean topLevel) {
var prov = e.getWrapper().getEntry().getProvider();
if (prov != null) {
return prov.customSectionComp(e, topLevel);
} else {
return new StoreSectionComp(e, topLevel);
}
}
StoreEntryWrapper wrapper;
ObservableList<StoreSection> allChildren;
ObservableList<StoreSection> shownChildren;
@ -55,6 +46,15 @@ public class StoreSection {
}
}
public static Comp<?> customSection(StoreSection e, boolean topLevel) {
var prov = e.getWrapper().getEntry().getProvider();
if (prov != null) {
return prov.customSectionComp(e, topLevel);
} else {
return new StoreSectionComp(e, topLevel);
}
}
private static ObservableList<StoreSection> sorted(
ObservableList<StoreSection> list, ObservableValue<StoreCategoryWrapper> category) {
if (category == null) {
@ -63,14 +63,16 @@ public class StoreSection {
var c = Comparator.<StoreSection>comparingInt(
value -> value.getWrapper().getEntry().getValidity().isUsable() ? -1 : 1);
var mappedSortMode = BindingsHelper.mappedBinding(category, storeCategoryWrapper -> storeCategoryWrapper != null ? storeCategoryWrapper.getSortMode() : null);
var mappedSortMode = BindingsHelper.mappedBinding(
category,
storeCategoryWrapper -> storeCategoryWrapper != null ? storeCategoryWrapper.getSortMode() : null);
return BindingsHelper.orderedContentBinding(
list,
(o1, o2) -> {
var current = mappedSortMode.getValue();
if (current != null) {
return c.thenComparing(current.comparator())
.compare(o1, o2);
.compare(current.representative(o1), current.representative(o2));
} else {
return c.compare(o1, o2);
}
@ -97,7 +99,9 @@ public class StoreSection {
section -> {
var showFilter = filterString == null || section.shouldShow(filterString.get());
var matchesSelector = section.anyMatches(entryFilter);
var sameCategory = category == null || category.getValue() == null || category.getValue().contains(section.getWrapper().getEntry());
var sameCategory = category == null
|| category.getValue() == null
|| category.getValue().contains(section.getWrapper());
return showFilter && matchesSelector && sameCategory;
},
category,
@ -117,11 +121,11 @@ public class StoreSection {
}
var allChildren = BindingsHelper.filteredContentBinding(all, other -> {
// Legacy implementation that does not use caches. Use for testing
// if (true) return DataStorage.get()
// .getDisplayParent(other.getEntry())
// .map(found -> found.equals(e.getEntry()))
// .orElse(false);
// Legacy implementation that does not use children caches. Use for testing
// if (true) return DataStorage.get()
// .getDisplayParent(other.getEntry())
// .map(found -> found.equals(e.getEntry()))
// .orElse(false);
// This check is fast as the children are cached in the storage
return DataStorage.get().getStoreChildren(e.getEntry()).contains(other.getEntry());
@ -134,9 +138,13 @@ public class StoreSection {
section -> {
var showFilter = filterString == null || section.shouldShow(filterString.get());
var matchesSelector = section.anyMatches(entryFilter);
var sameCategory = category == null || category.getValue() == null || category.getValue().contains(section.getWrapper().getEntry());
// If this entry is already shown as root due to a different category than parent, don't show it again here
var notRoot = !DataStorage.get().isRootEntry(section.getWrapper().getEntry());
var sameCategory = category == null
|| category.getValue() == null
|| category.getValue().contains(section.getWrapper());
// If this entry is already shown as root due to a different category than parent, don't show it
// again here
var notRoot =
!DataStorage.get().isRootEntry(section.getWrapper().getEntry());
return showFilter && matchesSelector && sameCategory && notRoot;
},
category,

View file

@ -22,12 +22,11 @@ import java.util.List;
public class StoreSectionComp extends Comp<CompStructure<VBox>> {
public static final PseudoClass EXPANDED = PseudoClass.getPseudoClass("expanded");
private static final PseudoClass ROOT = PseudoClass.getPseudoClass("root");
private static final PseudoClass SUB = PseudoClass.getPseudoClass("sub");
private static final PseudoClass ODD = PseudoClass.getPseudoClass("odd-depth");
private static final PseudoClass EVEN = PseudoClass.getPseudoClass("even-depth");
public static final PseudoClass EXPANDED = PseudoClass.getPseudoClass("expanded");
private final StoreSection section;
private final boolean topLevel;
@ -38,7 +37,7 @@ public class StoreSectionComp extends Comp<CompStructure<VBox>> {
@Override
public CompStructure<VBox> createBase() {
var root = StandardStoreEntryComp.customSection(section, topLevel)
var root = StoreEntryComp.customSection(section, topLevel)
.apply(struc -> HBox.setHgrow(struc.get(), Priority.ALWAYS));
var button = new IconButtonComp(
Bindings.createStringBinding(
@ -54,9 +53,11 @@ public class StoreSectionComp extends Comp<CompStructure<VBox>> {
.apply(struc -> struc.get().setMinWidth(30))
.apply(struc -> struc.get().setPrefWidth(30))
.focusTraversable()
.accessibleText(Bindings.createStringBinding(() -> {
return "Expand " + section.getWrapper().getName().getValue();
}, section.getWrapper().getName()))
.accessibleText(Bindings.createStringBinding(
() -> {
return "Expand " + section.getWrapper().getName().getValue();
},
section.getWrapper().getName()))
.disable(BindingsHelper.persist(
Bindings.size(section.getShownChildren()).isEqualTo(0)))
.grow(false, true)

View file

@ -28,19 +28,18 @@ import java.util.function.BiConsumer;
@Builder
public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
public static Comp<?> createList(StoreSection top, BiConsumer<StoreSection, Comp<CompStructure<Button>>> augment) {
return new StoreSectionMiniComp(top, augment);
}
public static final PseudoClass EXPANDED = PseudoClass.getPseudoClass("expanded");
private static final PseudoClass ODD = PseudoClass.getPseudoClass("odd-depth");
private static final PseudoClass EVEN = PseudoClass.getPseudoClass("even-depth");
public static final PseudoClass EXPANDED = PseudoClass.getPseudoClass("expanded");
private final StoreSection section;
@Builder.Default
private final BiConsumer<StoreSection, Comp<CompStructure<Button>>> augment = (section1, buttonComp) -> {};
public static Comp<?> createList(StoreSection top, BiConsumer<StoreSection, Comp<CompStructure<Button>>> augment) {
return new StoreSectionMiniComp(top, augment);
}
@Override
public CompStructure<VBox> createBase() {
var list = new ArrayList<Comp<?>>();
@ -48,14 +47,14 @@ public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
if (section.getWrapper() != null) {
var root = new ButtonComp(section.getWrapper().nameProperty(), () -> {})
.apply(struc -> {
var provider = section.getWrapper()
.getEntry()
.getProvider();
var provider = section.getWrapper().getEntry().getProvider();
struc.get()
.setGraphic(PrettyImageHelper.ofFixedSmallSquare(provider != null ? provider
.getDisplayIconFileName(section.getWrapper()
.getEntry()
.getStore()) : null)
.setGraphic(PrettyImageHelper.ofFixedSmallSquare(
provider != null
? provider.getDisplayIconFileName(section.getWrapper()
.getEntry()
.getStore())
: null)
.createRegion());
})
.apply(struc -> {
@ -79,38 +78,47 @@ public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
.apply(struc -> struc.get().setMinWidth(20))
.apply(struc -> struc.get().setPrefWidth(20))
.focusTraversable()
.accessibleText(Bindings.createStringBinding(() -> {
return "Expand " + section.getWrapper().getName().getValue();
}, section.getWrapper().getName()))
.accessibleText(Bindings.createStringBinding(
() -> {
return "Expand "
+ section.getWrapper().getName().getValue();
},
section.getWrapper().getName()))
.disable(BindingsHelper.persist(
Bindings.size(section.getAllChildren()).isEqualTo(0)))
.grow(false, true)
.styleClass("expand-button");
List<Comp<?>> topEntryList = List.of(button, root);
list.add(new HorizontalComp(topEntryList)
.apply(struc -> struc.get().setFillHeight(true)));
list.add(new HorizontalComp(topEntryList).apply(struc -> struc.get().setFillHeight(true)));
} else {
expanded = new SimpleBooleanProperty(true);
}
// Optimization for large sections. If there are more than 20 children, only add the nodes to the scene if the
// section is actually expanded
var listSections = section.getWrapper() != null ? BindingsHelper.filteredContentBinding(
section.getShownChildren(),
storeSection -> section.getAllChildren().size() <= 20
|| expanded.get(),
expanded,
section.getAllChildren()) : section.getShownChildren();
var listSections = section.getWrapper() != null
? BindingsHelper.filteredContentBinding(
section.getShownChildren(),
storeSection -> section.getAllChildren().size() <= 20 || expanded.get(),
expanded,
section.getAllChildren())
: section.getShownChildren();
var content = new ListBoxViewComp<>(listSections, section.getAllChildren(), (StoreSection e) -> {
return StoreSectionMiniComp.builder().section(e).augment(this.augment).build();
}).withLimit(100).minHeight(0).hgrow();
return StoreSectionMiniComp.builder()
.section(e)
.augment(this.augment)
.build();
})
.withLimit(100)
.minHeight(0)
.hgrow();
list.add(new HorizontalComp(List.of(content))
.styleClass("content")
.apply(struc -> struc.get().setFillHeight(true))
.hide(BindingsHelper.persist(Bindings.or(
Bindings.not(expanded),
Bindings.size(section.getAllChildren()).isEqualTo(0)))));
.styleClass("content")
.apply(struc -> struc.get().setFillHeight(true))
.hide(BindingsHelper.persist(Bindings.or(
Bindings.not(expanded),
Bindings.size(section.getAllChildren()).isEqualTo(0)))));
return new VerticalComp(list)
.styleClass("store-section-mini-comp")
@ -130,8 +138,9 @@ public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
return;
}
struc.get().getStyleClass().removeIf(
s -> Arrays.stream(DataStoreColor.values()).anyMatch(dataStoreColor -> dataStoreColor.getId().equals(s)));
struc.get().getStyleClass().removeIf(s -> Arrays.stream(DataStoreColor.values())
.anyMatch(dataStoreColor ->
dataStoreColor.getId().equals(s)));
struc.get().getStyleClass().remove("none");
struc.get().getStyleClass().add("color-box");
if (val != null) {
@ -141,7 +150,7 @@ public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
}
});
}
})
})
.createStructure();
}
}

View file

@ -14,9 +14,18 @@ public class StoreSidebarComp extends SimpleComp {
protected Region createSimple() {
var sideBar = new VerticalComp(List.of(
new StoreEntryListStatusComp().styleClass("color-box").styleClass("gray"),
new StoreCategoryListComp(StoreViewState.get().getAllConnectionsCategory()).styleClass("color-box").styleClass("gray"),
new StoreCategoryListComp(StoreViewState.get().getAllScriptsCategory()).styleClass("color-box").styleClass("gray"),
Comp.of(() -> new Region()).styleClass("bar").styleClass("color-box").styleClass("gray").styleClass("filler-bar").vgrow()));
new StoreCategoryListComp(StoreViewState.get().getAllConnectionsCategory())
.styleClass("color-box")
.styleClass("gray"),
new StoreCategoryListComp(StoreViewState.get().getAllScriptsCategory())
.styleClass("color-box")
.styleClass("gray"),
Comp.of(() -> new Region())
.styleClass("bar")
.styleClass("color-box")
.styleClass("gray")
.styleClass("filler-bar")
.vgrow()));
sideBar.apply(struc -> struc.get().setFillWidth(true));
sideBar.styleClass("sidebar");
sideBar.prefWidth(240);

View file

@ -12,6 +12,11 @@ import java.util.stream.Stream;
public interface StoreSortMode {
StoreSortMode ALPHABETICAL_DESC = new StoreSortMode() {
@Override
public StoreSection representative(StoreSection s) {
return s;
}
@Override
public String getId() {
return "alphabetical-desc";
@ -23,8 +28,12 @@ public interface StoreSortMode {
e -> e.getWrapper().nameProperty().getValue().toLowerCase(Locale.ROOT));
}
};
StoreSortMode ALPHABETICAL_ASC = new StoreSortMode() {
@Override
public StoreSection representative(StoreSection s) {
return s;
}
@Override
public String getId() {
return "alphabetical-asc";
@ -37,8 +46,21 @@ public interface StoreSortMode {
.reversed();
}
};
StoreSortMode DATE_DESC = new StoreSortMode() {
@Override
public StoreSection representative(StoreSection s) {
var c = comparator();
return Stream.of(
s.getShownChildren().stream()
.max((o1, o2) -> {
return c.compare(representative(o1), representative(o2));
})
.orElse(s),
s)
.max(c)
.orElseThrow();
}
@Override
public String getId() {
return "date-desc";
@ -54,8 +76,21 @@ public interface StoreSortMode {
});
}
};
StoreSortMode DATE_ASC = new StoreSortMode() {
@Override
public StoreSection representative(StoreSection s) {
var c = comparator();
return Stream.of(
s.getShownChildren().stream()
.min((o1, o2) -> {
return c.compare(representative(o1), representative(o2));
})
.orElse(s),
s)
.min(c)
.orElseThrow();
}
@Override
public String getId() {
return "date-asc";
@ -64,13 +99,15 @@ public interface StoreSortMode {
@Override
public Comparator<StoreSection> comparator() {
return Comparator.<StoreSection, Instant>comparing(e -> {
return flatten(e)
.map(entry -> entry.getLastAccess())
.max(Comparator.naturalOrder())
.orElseThrow();
}).reversed();
return flatten(e)
.map(entry -> entry.getLastAccess())
.max(Comparator.naturalOrder())
.orElseThrow();
})
.reversed();
}
};
List<StoreSortMode> ALL = List.of(ALPHABETICAL_DESC, ALPHABETICAL_ASC, DATE_DESC, DATE_ASC);
static Stream<DataStoreEntry> flatten(StoreSection section) {
return Stream.concat(
@ -78,14 +115,14 @@ public interface StoreSortMode {
section.getAllChildren().stream().flatMap(section1 -> flatten(section1)));
}
List<StoreSortMode> ALL = List.of(ALPHABETICAL_DESC, ALPHABETICAL_ASC, DATE_DESC, DATE_ASC);
static Optional<StoreSortMode> fromId(String id) {
return ALL.stream()
.filter(storeSortMode -> storeSortMode.getId().equals(id))
.findFirst();
}
StoreSection representative(StoreSection s);
String getId();
Comparator<StoreSection> comparator();

View file

@ -3,6 +3,7 @@ package io.xpipe.app.comp.store;
import io.xpipe.app.core.AppCache;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreCategory;
import io.xpipe.app.storage.DataStoreEntry;
@ -23,6 +24,26 @@ import java.util.stream.Collectors;
public class StoreViewState {
private static StoreViewState INSTANCE;
private final StringProperty filter = new SimpleStringProperty();
@Getter
private final ObservableList<StoreEntryWrapper> allEntries =
FXCollections.observableList(new CopyOnWriteArrayList<>());
@Getter
private final ObservableList<StoreCategoryWrapper> categories =
FXCollections.observableList(new CopyOnWriteArrayList<>());
@Getter
private final Property<StoreCategoryWrapper> activeCategory = new SimpleObjectProperty<>();
@Getter
private StoreSection currentTopLevelSection;
private StoreViewState() {
initContent();
addListeners();
}
public static void init() {
if (INSTANCE != null) {
@ -52,27 +73,6 @@ public class StoreViewState {
return INSTANCE;
}
private final StringProperty filter = new SimpleStringProperty();
@Getter
private final ObservableList<StoreEntryWrapper> allEntries =
FXCollections.observableList(new CopyOnWriteArrayList<>());
@Getter
private final ObservableList<StoreCategoryWrapper> categories =
FXCollections.observableList(new CopyOnWriteArrayList<>());
@Getter
private StoreSection currentTopLevelSection;
@Getter
private final Property<StoreCategoryWrapper> activeCategory = new SimpleObjectProperty<>();
private StoreViewState() {
initContent();
addStorageListeners();
}
private void updateContent() {
categories.forEach(c -> c.update());
allEntries.forEach(e -> e.update());
@ -112,12 +112,27 @@ public class StoreViewState {
.orElseThrow()));
}
private void addStorageListeners() {
private void addListeners() {
if (AppPrefs.get() != null) {
AppPrefs.get().condenseConnectionDisplay().addListener((observable, oldValue, newValue) -> {
Platform.runLater(() -> {
synchronized (this) {
var l = new ArrayList<>(allEntries);
allEntries.clear();
allEntries.setAll(l);
}
});
});
}
// Watch out for synchronizing all calls to the entries and categories list!
DataStorage.get().addListener(new StorageListener() {
@Override
public void onStoreAdd(DataStoreEntry... entry) {
var l = Arrays.stream(entry).map(StoreEntryWrapper::new).peek(storeEntryWrapper -> storeEntryWrapper.update()).toList();
var l = Arrays.stream(entry)
.map(StoreEntryWrapper::new)
.peek(storeEntryWrapper -> storeEntryWrapper.update())
.toList();
Platform.runLater(() -> {
// Don't update anything if we have already reset
if (INSTANCE == null) {

View file

@ -1,6 +1,5 @@
package io.xpipe.app.core;
import io.xpipe.app.Main;
import io.xpipe.app.comp.AppLayoutComp;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.issue.ErrorEvent;
@ -12,8 +11,8 @@ import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.stage.Stage;
import lombok.Getter;
import lombok.SneakyThrows;
import javax.imageio.ImageIO;
import java.awt.*;
@Getter
@ -27,26 +26,13 @@ public class App extends Application {
}
@Override
@SneakyThrows
public void start(Stage primaryStage) {
TrackEvent.info("Application launched");
APP = this;
stage = primaryStage;
stage.opacityProperty().bind(AppPrefs.get().windowOpacity());
// Set dock icon explicitly on mac
// This is necessary in case XPipe was started through a script as it will have no icon otherwise
if (OsType.getLocal().equals(OsType.MACOS) && AppProperties.get().isDeveloperMode() && AppLogs.get().isWriteToSysout()) {
try {
var iconUrl = Main.class.getResourceAsStream("resources/img/logo/logo_macos_128x128.png");
if (iconUrl != null) {
var awtIcon = ImageIO.read(iconUrl);
Taskbar.getTaskbar().setIconImage(awtIcon);
}
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).omitted(true).build().handle();
}
}
if (OsType.getLocal().equals(OsType.MACOS)) {
Desktop.getDesktop().setPreferencesHandler(e -> {
AppLayoutModel.get().selectSettings();
@ -56,7 +42,8 @@ public class App extends Application {
if (OsType.getLocal().equals(OsType.LINUX)) {
try {
Toolkit xToolkit = Toolkit.getDefaultToolkit();
java.lang.reflect.Field awtAppClassNameField = xToolkit.getClass().getDeclaredField("awtAppClassName");
java.lang.reflect.Field awtAppClassNameField =
xToolkit.getClass().getDeclaredField("awtAppClassName");
awtAppClassNameField.setAccessible(true);
awtAppClassNameField.set(xToolkit, "XPipe");
} catch (Exception e) {
@ -103,10 +90,7 @@ public class App extends Application {
public void focus() {
PlatformThread.runLaterIfNeeded(() -> {
stage.setAlwaysOnTop(true);
stage.setAlwaysOnTop(false);
stage.requestFocus();
});
}
}

View file

@ -16,7 +16,8 @@ public class AppBundledFonts {
return;
}
System.setProperty("prism.fontdir", XPipeInstallation.getBundledFontsPath().toString());
System.setProperty(
"prism.fontdir", XPipeInstallation.getBundledFontsPath().toString());
System.setProperty("prism.embeddedfonts", "true");
}

View file

@ -11,7 +11,8 @@ public class AppDebugModeNotice {
}
var out = AppLogs.get().getOriginalSysOut();
var msg = """
var msg =
"""
****************************************
* You are running XPipe in debug mode! *

View file

@ -2,7 +2,6 @@ package io.xpipe.app.core;
import io.xpipe.app.exchange.MessageExchangeImpls;
import io.xpipe.app.ext.ExtensionException;
import io.xpipe.app.ext.ModuleInstall;
import io.xpipe.app.ext.XPipeServiceProviders;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent;
@ -29,6 +28,7 @@ public class AppExtensionManager {
private final List<ModuleLayer> leafModuleLayers = new ArrayList<>();
private final List<Path> extensionBaseDirectories = new ArrayList<>();
private ModuleLayer baseLayer = ModuleLayer.boot();
@Getter
private ModuleLayer extendedLayer;
@ -52,11 +52,20 @@ public class AppExtensionManager {
XPipeServiceProviders.load(INSTANCE.extendedLayer);
MessageExchangeImpls.loadAll();
} catch (Throwable t) {
throw new ExtensionException("Service provider initialization failed. Is the installation data corrupt?", t);
throw new ExtensionException(
"Service provider initialization failed. Is the installation data corrupt?", t);
}
}
}
public static void reset() {
INSTANCE = null;
}
public static AppExtensionManager getInstance() {
return INSTANCE;
}
private void loadBaseExtension() {
var baseModule = findAndParseExtension("base", ModuleLayer.boot());
if (baseModule.isEmpty()) {
@ -95,14 +104,6 @@ public class AppExtensionManager {
extensionBaseDirectories.add(productionRoot);
}
public static void reset() {
INSTANCE = null;
}
public static AppExtensionManager getInstance() {
return INSTANCE;
}
public Set<Module> getContentModules() {
return Stream.concat(
Stream.of(ModuleLayer.boot().findModule("io.xpipe.app").orElseThrow()),
@ -110,87 +111,28 @@ public class AppExtensionManager {
.collect(Collectors.toSet());
}
public boolean isInstalled(ModuleInstall install) {
var target =
AppExtensionManager.getInstance().getGeneratedModulesDirectory(install.getModule(), install.getId());
return Files.exists(target) && Files.isRegularFile(target.resolve("finished"));
}
public void installIfNeeded(ModuleInstall install) throws Exception {
var target =
AppExtensionManager.getInstance().getGeneratedModulesDirectory(install.getModule(), install.getId());
if (Files.exists(target) && Files.isRegularFile(target.resolve("finished"))) {
return;
}
Files.createDirectories(target);
install.installInternal(target);
Files.createFile(target.resolve("finished"));
}
public Path getGeneratedModulesDirectory(String module, String ext) {
var base = AppProperties.get()
.getDataDir()
.resolve("generated_extensions")
.resolve(AppProperties.get().getVersion())
.resolve(module);
return ext != null ? base.resolve(ext) : base;
}
private void loadAllExtensions() {
for (Path extensionBaseDirectory : extensionBaseDirectories) {
loadExtensionRootDirectory(extensionBaseDirectory);
for (var ext : List.of("jdbc", "proc", "uacc")) {
var extension = findAndParseExtension(ext, baseLayer)
.orElseThrow(() -> ExtensionException.corrupt("Missing module " + ext));
loadedExtensions.add(extension);
leafModuleLayers.add(extension.getModule().getLayer());
}
if (leafModuleLayers.size() > 0) {
var scl = ClassLoader.getSystemClassLoader();
var cfs = leafModuleLayers.stream().map(ModuleLayer::configuration).toList();
var finder = ModuleFinder.ofSystem();
var cf = Configuration.resolve(finder, cfs, finder, List.of());
extendedLayer = ModuleLayer.defineModulesWithOneLoader(cf, leafModuleLayers, scl)
.layer();
} else {
extendedLayer = baseLayer;
}
}
private void loadExtensionRootDirectory(Path dir) {
if (!Files.exists(dir)) {
return;
}
// Order results as on unix systems the file list order is not deterministic
try (var s = Files.list(dir).sorted(Comparator.comparing(path -> path.toString()))) {
s.forEach(sub -> {
if (Files.isDirectory(sub)) {
// TODO: Better detection for x modules
if (sub.toString().endsWith("x")) {
return;
}
var extension = parseExtensionDirectory(sub, baseLayer);
if (extension.isEmpty()) {
return;
}
loadedExtensions.add(extension.get());
var xModule = findAndParseExtension(
extension.get().getId() + "x",
extension.get().getModule().getLayer());
if (xModule.isPresent()) {
loadedExtensions.add(xModule.get());
leafModuleLayers.add(xModule.get().getModule().getLayer());
} else {
leafModuleLayers.add(extension.get().getModule().getLayer());
}
}
});
} catch (IOException ex) {
ErrorEvent.fromThrowable(ex).handle();
}
var scl = ClassLoader.getSystemClassLoader();
var cfs = leafModuleLayers.stream().map(ModuleLayer::configuration).toList();
var finder = ModuleFinder.ofSystem();
var cf = Configuration.resolve(finder, cfs, finder, List.of());
extendedLayer = ModuleLayer.defineModulesWithOneLoader(cf, leafModuleLayers, scl)
.layer();
}
private Optional<Extension> findAndParseExtension(String name, ModuleLayer parent) {
var inModulePath = ModuleLayer.boot().findModule("io.xpipe.ext." + name);
if (inModulePath.isPresent()) {
return Optional.of(new Extension(null, inModulePath.get().getName(), name, inModulePath.get(), 0));
}
for (Path extensionBaseDirectory : extensionBaseDirectories) {
var found = parseExtensionDirectory(extensionBaseDirectory.resolve(name), parent);
if (found.isPresent()) {
@ -206,7 +148,7 @@ public class AppExtensionManager {
return Optional.empty();
}
if (loadedExtensions.stream().anyMatch(extension -> extension.dir.equals(dir))
if (loadedExtensions.stream().anyMatch(extension -> dir.equals(extension.dir))
|| loadedExtensions.stream()
.anyMatch(extension ->
extension.id.equals(dir.getFileName().toString()))) {

View file

@ -107,6 +107,7 @@ public class AppFileWatcher {
private class WatchedDirectory {
private final BiConsumer<Path, WatchEvent.Kind<Path>> listener;
@Getter
private final Path baseDir;
@ -114,9 +115,7 @@ public class AppFileWatcher {
this.baseDir = dir;
this.listener = listener;
createRecursiveWatchers(dir);
TrackEvent.withTrace("watcher", "Added watched directory")
.tag("location", dir)
.handle();
TrackEvent.withTrace("Added watched directory").tag("location", dir).handle();
}
private void createRecursiveWatchers(Path dir) {
@ -177,13 +176,12 @@ public class AppFileWatcher {
}
// Handle event
TrackEvent.withTrace("watcher", "Watch event")
TrackEvent.withTrace("Watch event")
.tag("baseDir", baseDir)
.tag("file", baseDir.relativize(file))
.tag("kind", event.kind().name())
.handle();
listener.accept(file, ev.kind());
}
}
}

View file

@ -54,7 +54,8 @@ public class AppFont {
try (var in = Files.newInputStream(file)) {
Font.loadFont(in, OsType.getLocal() == OsType.LINUX ? 11 : 12);
} catch (Throwable t) {
// Font loading can fail in rare cases. This is however not important, so we can just ignore it
// Font loading can fail in rare cases. This is however not important, so we can just ignore
// it
}
return FileVisitResult.CONTINUE;
}

View file

@ -1,6 +1,5 @@
package io.xpipe.app.core;
import com.jfoenix.controls.JFXCheckBox;
import io.xpipe.app.comp.base.MarkdownComp;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.fxcomps.Comp;
@ -52,7 +51,7 @@ public class AppGreetings {
public static void showIfNeeded() {
boolean set = AppCache.get("legalAccepted", Boolean.class, () -> false);
if (set || !AppState.get().isInitialLaunch()) {
if (set || AppProperties.get().isDevelopmentEnvironment()) {
return;
}
var read = new SimpleBooleanProperty();
@ -72,20 +71,21 @@ public class AppGreetings {
});
var acceptanceBox = Comp.of(() -> {
var cb = new JFXCheckBox();
var cb = new CheckBox();
cb.selectedProperty().bindBidirectional(accepted);
var label = new Label(AppI18n.get("legalAccept"));
label.setGraphic(cb);
AppFont.medium(label);
label.setPadding(new Insets(40, 0, 10, 0));
label.setPadding(new Insets(20, 0, 10, 0));
label.setOnMouseClicked(event -> accepted.set(!accepted.get()));
label.setGraphicTextGap(10);
return label;
})
.createRegion();
var layout = new BorderPane();
layout.getStyleClass().add("window-content");
layout.setPadding(new Insets(20));
layout.setCenter(accordion);
layout.setBottom(acceptanceBox);
layout.setPrefWidth(700);

View file

@ -37,10 +37,10 @@ import java.util.regex.Pattern;
public class AppI18n {
private static final Pattern VAR_PATTERN = Pattern.compile("\\$\\w+?\\$");
private static final AppI18n INSTANCE = new AppI18n();
private Map<String, String> translations;
private Map<String, String> markdownDocumentations;
private PrettyTime prettyTime;
private static final AppI18n INSTANCE = new AppI18n();
public static void init() {
var i = INSTANCE;
@ -51,7 +51,7 @@ public class AppI18n {
i.load();
if (AppPrefs.get() != null) {
AppPrefs.get().language.addListener((c, o, n) -> {
AppPrefs.get().language().addListener((c, o, n) -> {
i.clear();
i.load();
});
@ -98,8 +98,11 @@ public class AppI18n {
return "null";
}
return getInstance().prettyTime.formatDuration(
getInstance().prettyTime.approximateDuration(Instant.now().plus(duration.getValue())));
return getInstance()
.prettyTime
.formatDuration(getInstance()
.prettyTime
.approximateDuration(Instant.now().plus(duration.getValue())));
},
duration);
}
@ -136,20 +139,6 @@ public class AppI18n {
return s;
}
private void clear() {
translations.clear();
prettyTime = null;
}
@SuppressWarnings("removal")
public static class CallingClass extends SecurityManager {
public static final CallingClass INSTANCE = new CallingClass();
public Class<?>[] getCallingClasses() {
return getClassContext();
}
}
@SneakyThrows
private static String getCallerModuleName() {
var callers = CallingClass.INSTANCE.getCallingClasses();
@ -161,6 +150,7 @@ public class AppI18n {
|| caller.equals(FancyTooltipAugment.class)
|| caller.equals(PrefsChoiceValue.class)
|| caller.equals(Translatable.class)
|| caller.equals(AppWindowHelper.class)
|| caller.equals(OptionsBuilder.class)) {
continue;
}
@ -170,6 +160,11 @@ public class AppI18n {
return "";
}
private void clear() {
translations.clear();
prettyTime = null;
}
public String getKey(String s) {
var key = s;
if (!s.contains(".")) {
@ -210,7 +205,7 @@ public class AppI18n {
private boolean matchesLocale(Path f) {
var l = AppPrefs.get() != null
? AppPrefs.get().language.getValue().getLocale()
? AppPrefs.get().language().getValue().getLocale()
: SupportedLocale.ENGLISH.getLocale();
var name = FilenameUtils.getBaseName(f.getFileName().toString());
var ending = "_" + l.toLanguageTag();
@ -219,7 +214,8 @@ public class AppI18n {
public String getMarkdownDocumentation(String name) {
if (!markdownDocumentations.containsKey(name)) {
TrackEvent.withWarn("Markdown documentation for key " + name + " not found").handle();
TrackEvent.withWarn("Markdown documentation for key " + name + " not found")
.handle();
}
return markdownDocumentations.getOrDefault(name, "");
@ -311,7 +307,16 @@ public class AppI18n {
this.prettyTime = new PrettyTime(
AppPrefs.get() != null
? AppPrefs.get().language.getValue().getLocale()
? AppPrefs.get().language().getValue().getLocale()
: SupportedLocale.ENGLISH.getLocale());
}
@SuppressWarnings("removal")
public static class CallingClass extends SecurityManager {
public static final CallingClass INSTANCE = new CallingClass();
public Class<?>[] getCallingClasses() {
return getClassContext();
}
}
}

View file

@ -5,7 +5,8 @@ import io.xpipe.app.browser.BrowserModel;
import io.xpipe.app.comp.DeveloperTabComp;
import io.xpipe.app.comp.store.StoreLayoutComp;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.prefs.PrefsComp;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.prefs.AppPrefsComp;
import io.xpipe.app.util.LicenseProvider;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
@ -18,20 +19,26 @@ import lombok.extern.jackson.Jacksonized;
import java.util.ArrayList;
import java.util.List;
@Getter
public class AppLayoutModel {
@Data
@Builder
@Jacksonized
public static class SavedState {
double sidebarWidth;
double browserConnectionsWidth;
}
private static AppLayoutModel INSTANCE;
@Getter
private final SavedState savedState;
@Getter
private final List<Entry> entries;
private final Property<Entry> selected;
private final ObservableValue<Entry> selectedWrapper;
public AppLayoutModel(SavedState savedState) {
this.savedState = savedState;
this.entries = createEntryList();
this.selected = new SimpleObjectProperty<>(entries.get(1));
this.selectedWrapper = PlatformThread.sync(selected);
}
public static AppLayoutModel get() {
return INSTANCE;
}
@ -46,19 +53,16 @@ public class AppLayoutModel {
INSTANCE = null;
}
@Getter
private final SavedState savedState;
private final List<Entry> entries;
private final Property<Entry> selected;
public Property<Entry> getSelectedInternal() {
return selected;
}
public AppLayoutModel(SavedState savedState) {
this.savedState = savedState;
this.entries = createEntryList();
this.selected = new SimpleObjectProperty<>(entries.get(1));
public ObservableValue<Entry> getSelected() {
return selectedWrapper;
}
public void selectBrowser() {
selected.setValue(entries.get(0));
selected.setValue(entries.getFirst());
}
public void selectSettings() {
@ -75,17 +79,14 @@ public class AppLayoutModel {
private List<Entry> createEntryList() {
var l = new ArrayList<>(List.of(
new Entry(
AppI18n.observable("browser"), "mdi2f-file-cabinet", new BrowserComp(BrowserModel.DEFAULT)),
new Entry(AppI18n.observable("browser"), "mdi2f-file-cabinet", new BrowserComp(BrowserModel.DEFAULT)),
new Entry(AppI18n.observable("connections"), "mdi2c-connection", new StoreLayoutComp()),
new Entry(
AppI18n.observable("settings"), "mdsmz-miscellaneous_services", new PrefsComp(this))));
new Entry(AppI18n.observable("settings"), "mdsmz-miscellaneous_services", new AppPrefsComp())));
// new SideMenuBarComp.Entry(AppI18n.observable("help"), "mdi2b-book-open-variant", new
// StorageLayoutComp()),
// new SideMenuBarComp.Entry(AppI18n.observable("account"), "mdi2a-account", new StorageLayoutComp())
if (AppProperties.get().isDeveloperMode() && !AppProperties.get().isImage()) {
l.add(new Entry(
AppI18n.observable("developer"), "mdi2b-book-open-variant", new DeveloperTabComp()));
l.add(new Entry(AppI18n.observable("developer"), "mdi2b-book-open-variant", new DeveloperTabComp()));
}
l.add(new Entry(
@ -96,5 +97,14 @@ public class AppLayoutModel {
return l;
}
@Data
@Builder
@Jacksonized
public static class SavedState {
double sidebarWidth;
double browserConnectionsWidth;
}
public record Entry(ObservableValue<String> name, String icon, Comp<?> comp) {}
}

View file

@ -24,7 +24,6 @@ import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoField;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@ -32,23 +31,26 @@ import java.util.concurrent.ConcurrentHashMap;
public class AppLogs {
public static final List<String> DEFAULT_LEVELS = List.of("error", "warn", "info", "debug", "trace");
public static final List<String> LOG_LEVELS = List.of("error", "warn", "info", "debug", "trace");
private static final String WRITE_SYSOUT_PROP = "io.xpipe.app.writeSysOut";
private static final String WRITE_LOGS_PROP = "io.xpipe.app.writeLogs";
private static final String DEBUG_PLATFORM_PROP = "io.xpipe.app.debugPlatform";
private static final String LOG_LEVEL_PROP = "io.xpipe.app.logLevel";
private static final String DEFAULT_LOG_LEVEL = "info";
private static final DateTimeFormatter FORMATTER =
private static final DateTimeFormatter NAME_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss").withZone(ZoneId.systemDefault());
private static final DateTimeFormatter MESSAGE_FORMATTER =
DateTimeFormatter.ofPattern("HH:mm:ss:SSS").withZone(ZoneId.systemDefault());
private static AppLogs INSTANCE;
@Getter
private final PrintStream originalSysOut;
@Getter
private final PrintStream originalSysErr;
private final Path logDir;
@Getter
@ -60,16 +62,15 @@ public class AppLogs {
@Getter
private final String logLevel;
private final PrintStream outStream;
private final Map<String, PrintStream> categoryWriters;
private final PrintStream outFileStream;
public AppLogs(Path logDir, boolean writeToSysout, boolean writeToFile, String logLevel) {
public AppLogs(
Path logDir, boolean writeToSysout, boolean writeToFile, String logLevel, PrintStream outFileStream) {
this.logDir = logDir;
this.writeToSysout = writeToSysout;
this.writeToFile = writeToFile;
this.logLevel = logLevel;
this.outStream = System.out;
this.categoryWriters = new HashMap<>();
this.outFileStream = outFileStream;
this.originalSysOut = System.out;
this.originalSysErr = System.err;
@ -96,20 +97,34 @@ public class AppLogs {
}
public static void init() {
if (INSTANCE != null) {
return;
}
var logDir = AppProperties.get().getDataDir().resolve("logs");
// Regularly clean logs dir
if (XPipeSession.get().isNewBuildSession() && Files.exists(logDir)) {
try {
FileUtils.cleanDirectory(logDir.toFile());
List<Path> all;
try (var s = Files.list(logDir)) {
all = s.toList();
}
for (Path path : all) {
// Don't delete installer logs
if (path.getFileName().toString().contains("installer")) {
continue;
}
FileUtils.forceDelete(path.toFile());
}
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
}
}
var shouldLogToFile = shouldWriteLogs();
var now = Instant.now();
var name = FORMATTER.format(now);
var name = NAME_FORMATTER.format(now);
Path usedLogsDir = logDir.resolve(name);
// When two instances are being launched within the same second, add milliseconds
@ -117,23 +132,34 @@ public class AppLogs {
usedLogsDir = logDir.resolve(name + "_" + now.get(ChronoField.MILLI_OF_SECOND));
}
PrintStream outFileStream = null;
var shouldLogToFile = shouldWriteLogs();
if (shouldLogToFile) {
try {
Files.createDirectories(usedLogsDir);
var file = usedLogsDir.resolve("xpipe.log");
var fos = new FileOutputStream(file.toFile(), true);
var buf = new BufferedOutputStream(fos);
outFileStream = new PrintStream(buf, false);
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).build().handle();
shouldLogToFile = false;
}
}
var shouldLogToSysout = shouldWriteSysout();
if (shouldLogToFile && outFileStream == null) {
TrackEvent.info("Log file initialization failed. Writing to standard out");
shouldLogToSysout = true;
shouldLogToFile = false;
}
if (shouldLogToFile && !shouldLogToSysout) {
TrackEvent.info("Writing log output to " + usedLogsDir + " from now on");
}
var level = determineLogLevel();
INSTANCE = new AppLogs(usedLogsDir, shouldLogToSysout, shouldLogToFile, level);
INSTANCE = new AppLogs(usedLogsDir, shouldLogToSysout, shouldLogToFile, level, outFileStream);
}
public static void teardown() {
@ -149,45 +175,19 @@ public class AppLogs {
return INSTANCE;
}
private void close() {
outStream.close();
categoryWriters.forEach((k, s) -> {
s.close();
});
}
private String getCategory(TrackEvent event) {
if (event.getCategory() != null) {
return event.getCategory();
private static String determineLogLevel() {
if (System.getProperty(LOG_LEVEL_PROP) != null) {
String p = System.getProperty(LOG_LEVEL_PROP);
return LOG_LEVELS.contains(p) ? p : "trace";
}
return "misc";
return DEFAULT_LOG_LEVEL;
}
private synchronized PrintStream getLogStream(TrackEvent e) {
return categoryWriters.computeIfAbsent(getCategory(e), (cat) -> {
var file = logDir.resolve(cat + ".log");
FileOutputStream fos;
try {
fos = new FileOutputStream(file.toFile(), true);
} catch (IOException ex) {
return outStream;
}
return new PrintStream(fos, false);
});
}
public synchronized PrintStream getCatchAllLogStream() {
return categoryWriters.computeIfAbsent("xpipe", (cat) -> {
var file = logDir.resolve(cat + ".log");
FileOutputStream fos;
try {
fos = new FileOutputStream(file.toFile(), true);
} catch (IOException ex) {
return outStream;
}
return new PrintStream(fos, false);
});
private void close() {
if (outFileStream != null) {
outFileStream.close();
}
}
private boolean shouldDebugPlatform() {
@ -210,12 +210,7 @@ public class AppLogs {
return;
}
TrackEvent.builder()
.type("info")
.category("sysout")
.message(line)
.build()
.handle();
TrackEvent.builder().type("info").message(line).build().handle();
baos.reset();
} else {
baos.write(b);
@ -245,15 +240,6 @@ public class AppLogs {
}));
}
private static String determineLogLevel() {
if (System.getProperty(LOG_LEVEL_PROP) != null) {
String p = System.getProperty(LOG_LEVEL_PROP);
return DEFAULT_LEVELS.contains(p) ? p : "info";
}
return DEFAULT_LOG_LEVEL;
}
public void logException(String description, Throwable e) {
var deob = Deobfuscator.deobfuscateToString(e);
var event = TrackEvent.builder()
@ -264,9 +250,9 @@ public class AppLogs {
}
public synchronized void logEvent(TrackEvent event) {
var li = DEFAULT_LEVELS.indexOf(determineLogLevel());
var li = LOG_LEVELS.indexOf(determineLogLevel());
int i = li == -1 ? 5 : li;
int current = DEFAULT_LEVELS.indexOf(event.getType());
int current = LOG_LEVELS.indexOf(event.getType());
if (current <= i) {
if (writeToSysout) {
logSysOut(event);
@ -281,12 +267,9 @@ public class AppLogs {
var time = MESSAGE_FORMATTER.format(event.getInstant());
var string =
new StringBuilder(time).append(" - ").append(event.getType()).append(": ");
if (event.getCategory() != null) {
string.append("[").append(event.getCategory()).append("] ");
}
string.append(event);
var toLog = string.toString();
outStream.println(toLog);
this.originalSysOut.println(toLog);
}
private void logToFile(TrackEvent event) {
@ -295,8 +278,7 @@ public class AppLogs {
new StringBuilder(time).append(" - ").append(event.getType()).append(": ");
string.append(event);
var toLog = string.toString();
getLogStream(event).println(toLog);
getCatchAllLogStream().println(toLog);
outFileStream.println(toLog);
}
private void setLogLevels() {
@ -312,10 +294,6 @@ public class AppLogs {
}
}
public Path getLogsDirectory() {
return logDir.getParent();
}
public Path getSessionLogsDirectory() {
return logDir;
}
@ -339,7 +317,7 @@ public class AppLogs {
normalizedName = name;
}
return loggers.computeIfAbsent(normalizedName, Slf4jLogger::new);
return loggers.computeIfAbsent(normalizedName, s -> new Slf4jLogger());
}
};
@ -369,12 +347,6 @@ public class AppLogs {
public static final class Slf4jLogger extends AbstractLogger {
private final String name;
public Slf4jLogger(String name) {
this.name = name;
}
@Override
protected String getFullyQualifiedCallerName() {
return "logger";
@ -390,7 +362,6 @@ public class AppLogs {
}
}
TrackEvent.builder()
.category(name)
.type(level.toString().toLowerCase())
.message(msg)
.build()
@ -399,62 +370,62 @@ public class AppLogs {
@Override
public boolean isTraceEnabled() {
return DEFAULT_LEVELS.indexOf("trace")
<= DEFAULT_LEVELS.indexOf(AppLogs.get().getLogLevel());
return LOG_LEVELS.indexOf("trace")
<= LOG_LEVELS.indexOf(AppLogs.get().getLogLevel());
}
@Override
public boolean isTraceEnabled(Marker marker) {
return DEFAULT_LEVELS.indexOf("trace")
<= DEFAULT_LEVELS.indexOf(AppLogs.get().getLogLevel());
return LOG_LEVELS.indexOf("trace")
<= LOG_LEVELS.indexOf(AppLogs.get().getLogLevel());
}
@Override
public boolean isDebugEnabled() {
return DEFAULT_LEVELS.indexOf("debug")
<= DEFAULT_LEVELS.indexOf(AppLogs.get().getLogLevel());
return LOG_LEVELS.indexOf("debug")
<= LOG_LEVELS.indexOf(AppLogs.get().getLogLevel());
}
@Override
public boolean isDebugEnabled(Marker marker) {
return DEFAULT_LEVELS.indexOf("debug")
<= DEFAULT_LEVELS.indexOf(AppLogs.get().getLogLevel());
return LOG_LEVELS.indexOf("debug")
<= LOG_LEVELS.indexOf(AppLogs.get().getLogLevel());
}
@Override
public boolean isInfoEnabled() {
return DEFAULT_LEVELS.indexOf("info")
<= DEFAULT_LEVELS.indexOf(AppLogs.get().getLogLevel());
return LOG_LEVELS.indexOf("info")
<= LOG_LEVELS.indexOf(AppLogs.get().getLogLevel());
}
@Override
public boolean isInfoEnabled(Marker marker) {
return DEFAULT_LEVELS.indexOf("info")
<= DEFAULT_LEVELS.indexOf(AppLogs.get().getLogLevel());
return LOG_LEVELS.indexOf("info")
<= LOG_LEVELS.indexOf(AppLogs.get().getLogLevel());
}
@Override
public boolean isWarnEnabled() {
return DEFAULT_LEVELS.indexOf("warn")
<= DEFAULT_LEVELS.indexOf(AppLogs.get().getLogLevel());
return LOG_LEVELS.indexOf("warn")
<= LOG_LEVELS.indexOf(AppLogs.get().getLogLevel());
}
@Override
public boolean isWarnEnabled(Marker marker) {
return DEFAULT_LEVELS.indexOf("warn")
<= DEFAULT_LEVELS.indexOf(AppLogs.get().getLogLevel());
return LOG_LEVELS.indexOf("warn")
<= LOG_LEVELS.indexOf(AppLogs.get().getLogLevel());
}
@Override
public boolean isErrorEnabled() {
return DEFAULT_LEVELS.indexOf("error")
<= DEFAULT_LEVELS.indexOf(AppLogs.get().getLogLevel());
return LOG_LEVELS.indexOf("error")
<= LOG_LEVELS.indexOf(AppLogs.get().getLogLevel());
}
@Override
public boolean isErrorEnabled(Marker marker) {
return DEFAULT_LEVELS.indexOf("error")
<= DEFAULT_LEVELS.indexOf(AppLogs.get().getLogLevel());
return LOG_LEVELS.indexOf("error")
<= LOG_LEVELS.indexOf(AppLogs.get().getLogLevel());
}
}
}

View file

@ -83,7 +83,6 @@ public class AppMainWindow {
private void logChange() {
TrackEvent.withDebug("Window resize")
.windowCategory()
.tag("x", stage.getX())
.tag("y", stage.getY())
.tag("width", stage.getWidth())
@ -98,7 +97,6 @@ public class AppMainWindow {
applyState(state);
TrackEvent.withDebug("Window initialized")
.windowCategory()
.tag("x", stage.getX())
.tag("y", stage.getY())
.tag("width", stage.getWidth())

View file

@ -19,15 +19,20 @@ public class AppProperties {
private static final String EXTENSION_PATHS_PROP = "io.xpipe.app.extensions";
private static AppProperties INSTANCE;
boolean fullVersion;
@Getter
String version;
@Getter
String build;
UUID buildUuid;
String sentryUrl;
String arch;
@Getter
boolean image;
boolean staging;
boolean useVirtualThreads;
boolean debugThreads;
@ -101,6 +106,10 @@ public class AppProperties {
return INSTANCE;
}
public boolean isDevelopmentEnvironment() {
return !AppProperties.get().isImage() && AppProperties.get().isDeveloperMode();
}
public boolean isDeveloperMode() {
if (AppPrefs.get() == null) {
return false;
@ -108,5 +117,4 @@ public class AppProperties {
return AppPrefs.get().developerMode().getValue();
}
}

View file

@ -57,7 +57,10 @@ public class AppSocketServer {
.handle();
} catch (Exception ex) {
// Not terminal!
ErrorEvent.fromThrowable(ex).description("Unable to start local socket server on port " + port).build().handle();
ErrorEvent.fromThrowable(ex)
.description("Unable to start local socket server on port " + port)
.build()
.handle();
}
}
@ -112,7 +115,7 @@ public class AppSocketServer {
private boolean performExchange(Socket clientSocket, int id) throws Exception {
if (clientSocket.isClosed()) {
TrackEvent.trace("beacon", "Socket closed");
TrackEvent.trace("Socket closed");
return false;
}
@ -121,14 +124,14 @@ public class AppSocketServer {
node = JacksonMapper.getDefault().readTree(blockIn);
}
if (node.isMissingNode()) {
TrackEvent.trace("beacon", "Received EOF");
TrackEvent.trace("Received EOF");
return false;
}
TrackEvent.trace("beacon", "Received raw request: \n" + node.toPrettyString());
TrackEvent.trace("Received raw request: \n" + node.toPrettyString());
var req = parseRequest(node);
TrackEvent.trace("beacon", "Parsed request: \n" + req.toString());
TrackEvent.trace("Parsed request: \n" + req.toString());
var prov = MessageExchangeImpls.byRequest(req);
if (prov.isEmpty()) {
@ -145,19 +148,19 @@ public class AppSocketServer {
@Override
public OutputStream sendBody() throws IOException {
TrackEvent.trace("beacon", "Starting writing body for #" + id);
TrackEvent.trace("Starting writing body for #" + id);
return AppSocketServer.this.sendBody(clientSocket);
}
@Override
public InputStream receiveBody() throws IOException {
TrackEvent.trace("beacon", "Starting to read body for #" + id);
TrackEvent.trace("Starting to read body for #" + id);
return AppSocketServer.this.receiveBody(clientSocket);
}
},
req);
TrackEvent.trace("beacon", "Sending response to #" + id + ": \n" + res.toString());
TrackEvent.trace("Sending response to #" + id + ": \n" + res.toString());
AppSocketServer.this.sendResponse(clientSocket, res);
try {
@ -170,7 +173,6 @@ public class AppSocketServer {
}
TrackEvent.builder()
.category("beacon")
.type("trace")
.message("Socket connection #" + id + " performed exchange "
+ req.getClass().getSimpleName())
@ -187,7 +189,7 @@ public class AppSocketServer {
informationNode = JacksonMapper.getDefault().readTree(blockIn);
}
if (informationNode.isMissingNode()) {
TrackEvent.trace("beacon", "Received EOF");
TrackEvent.trace("Received EOF");
return;
}
var information =
@ -197,7 +199,6 @@ public class AppSocketServer {
}
TrackEvent.builder()
.category("beacon")
.type("trace")
.message("Created new socket connection #" + id)
.tag("client", information != null ? information.toDisplayString() : "Unknown")
@ -211,29 +212,29 @@ public class AppSocketServer {
}
}
TrackEvent.builder()
.category("beacon")
.type("trace")
.message("Socket connection #" + id + " finished successfully")
.build()
.handle();
} catch (ClientException ce) {
TrackEvent.trace("beacon", "Sending client error to #" + id + ": " + ce.getMessage());
TrackEvent.trace("Sending client error to #" + id + ": " + ce.getMessage());
sendClientErrorResponse(clientSocket, ce.getMessage());
} catch (ServerException se) {
TrackEvent.trace("beacon", "Sending server error to #" + id + ": " + se.getMessage());
ErrorEvent.fromThrowable(se).build().handle();
TrackEvent.trace("Sending server error to #" + id + ": " + se.getMessage());
Deobfuscator.deobfuscate(se);
sendServerErrorResponse(clientSocket, se);
var toReport = se.getCause() != null ? se.getCause() : se;
ErrorEvent.fromThrowable(toReport).build().handle();
} catch (SocketException ex) {
// Do not send error and omit it, as this might happen often
// We do not send the error as the socket connection might be broken
ErrorEvent.fromThrowable(ex).omitted(true).build().handle();
} catch (Throwable ex) {
TrackEvent.trace("beacon", "Sending internal server error to #" + id + ": " + ex.getMessage());
ErrorEvent.fromThrowable(ex).build().handle();
TrackEvent.trace("Sending internal server error to #" + id + ": " + ex.getMessage());
Deobfuscator.deobfuscate(ex);
sendServerErrorResponse(clientSocket, ex);
ErrorEvent.fromThrowable(ex).build().handle();
}
} catch (SocketException ex) {
// Omit it, as this might happen often
@ -243,16 +244,13 @@ public class AppSocketServer {
} finally {
try {
clientSocket.close();
TrackEvent.trace("beacon", "Closed socket #" + id);
TrackEvent.trace("Closed socket #" + id);
} catch (IOException e) {
ErrorEvent.fromThrowable(e).build().handle();
}
}
TrackEvent.builder()
.category("beacon")
.type("trace")
.message("Socket connection #" + id + " finished unsuccessfully");
TrackEvent.builder().type("trace").message("Socket connection #" + id + " finished unsuccessfully");
}
private void performExchangesAsync(Socket clientSocket) {
@ -296,7 +294,7 @@ public class AppSocketServer {
}
var content = writer.toString();
TrackEvent.trace("beacon", "Sending raw response:\n" + content);
TrackEvent.trace("Sending raw response:\n" + content);
try (OutputStream blockOut = BeaconFormat.writeBlocks(outSocket.getOutputStream())) {
blockOut.write(content.getBytes(StandardCharsets.UTF_8));
}
@ -336,7 +334,7 @@ public class AppSocketServer {
private <T extends RequestMessage> T parseRequest(JsonNode header) throws Exception {
ObjectNode content = (ObjectNode) header.required("xPipeMessage");
TrackEvent.trace("beacon", "Parsed raw request:\n" + content.toPrettyString());
TrackEvent.trace("Parsed raw request:\n" + content.toPrettyString());
var type = content.required("messageType").textValue();
var phase = content.required("messagePhase").textValue();

View file

@ -15,8 +15,9 @@ public class AppState {
boolean initialLaunch;
@NonFinal
@Setter
@Setter
String userName;
@NonFinal
@Setter
String userEmail;

View file

@ -28,7 +28,7 @@ public class AppStyle {
loadStylesheets();
if (AppPrefs.get() != null) {
AppPrefs.get().useSystemFont.addListener((c, o, n) -> {
AppPrefs.get().useSystemFont().addListener((c, o, n) -> {
changeFontUsage(n);
});
}
@ -48,17 +48,19 @@ public class AppStyle {
return;
}
TrackEvent.trace("core", "Loading styles for module " + module.getName());
TrackEvent.trace("Loading styles for module " + module.getName());
Files.walkFileTree(path, new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
try {
var bytes = Files.readAllBytes(file);
if (file.getFileName().toString().endsWith(".bss")) {
var s = "data:application/octet-stream;base64," + Base64.getEncoder().encodeToString(bytes);
var s = "data:application/octet-stream;base64,"
+ Base64.getEncoder().encodeToString(bytes);
STYLESHEET_CONTENTS.put(file, s);
} else if (file.getFileName().toString().endsWith(".css")) {
var s = "data:text/css;base64," + Base64.getEncoder().encodeToString(bytes);
var s = "data:text/css;base64,"
+ Base64.getEncoder().encodeToString(bytes);
STYLESHEET_CONTENTS.put(file, s);
}
} catch (IOException ex) {
@ -93,7 +95,7 @@ public class AppStyle {
}
public static void addStylesheets(Scene scene) {
if (AppPrefs.get() != null && !AppPrefs.get().useSystemFont.get()) {
if (AppPrefs.get() != null && !AppPrefs.get().useSystemFont().getValue()) {
scene.getStylesheets().add(FONT_CONTENTS);
}

View file

@ -1,7 +1,6 @@
package io.xpipe.app.core;
import atlantafx.base.theme.*;
import com.jthemedetecor.OsThemeDetector;
import io.xpipe.app.ext.PrefsChoiceValue;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
@ -14,7 +13,10 @@ import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.application.ColorScheme;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.css.PseudoClass;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
@ -71,30 +73,23 @@ public class AppTheme {
}
try {
OsThemeDetector detector = OsThemeDetector.getDetector();
if (AppPrefs.get().theme.getValue() == null) {
try {
setDefault(detector.isDark());
} catch (Throwable ex) {
ErrorEvent.fromThrowable(ex).omit().handle();
setDefault(false);
}
setDefault(Platform.getPreferences().getColorScheme());
}
// The gnome detector sometimes runs into issues, also it's not that important
if (!OsType.getLocal().equals(OsType.LINUX)) {
detector.registerListener(dark -> {
PlatformThread.runLaterIfNeeded(() -> {
if (dark && !AppPrefs.get().theme.getValue().isDark()) {
AppPrefs.get().theme.setValue(Theme.getDefaultDarkTheme());
}
Platform.getPreferences().colorSchemeProperty().addListener((observableValue, colorScheme, t1) -> {
Platform.runLater(() -> {
if (t1 == ColorScheme.DARK
&& !AppPrefs.get().theme.getValue().isDark()) {
AppPrefs.get().theme.setValue(Theme.getDefaultDarkTheme());
}
if (!dark && AppPrefs.get().theme.getValue().isDark()) {
AppPrefs.get().theme.setValue(Theme.getDefaultLightTheme());
}
});
if (t1 != ColorScheme.DARK
&& AppPrefs.get().theme.getValue().isDark()) {
AppPrefs.get().theme.setValue(Theme.getDefaultLightTheme());
}
});
}
});
} catch (Throwable t) {
ErrorEvent.fromThrowable(t).omit().handle();
}
@ -110,8 +105,8 @@ public class AppTheme {
init = true;
}
private static void setDefault(boolean dark) {
if (dark) {
private static void setDefault(ColorScheme colorScheme) {
if (colorScheme == ColorScheme.DARK) {
AppPrefs.get().theme.setValue(Theme.getDefaultDarkTheme());
} else {
AppPrefs.get().theme.setValue(Theme.getDefaultLightTheme());
@ -189,8 +184,8 @@ public class AppTheme {
}
@Override
public String toTranslatedString() {
return name;
public ObservableValue<String> toTranslatedString() {
return new SimpleStringProperty(name);
}
}
@ -211,6 +206,12 @@ public class AppTheme {
// Also include your custom theme here
public static final List<Theme> ALL =
List.of(PRIMER_LIGHT, PRIMER_DARK, NORD_LIGHT, NORD_DARK, CUPERTINO_LIGHT, CUPERTINO_DARK, DRACULA);
protected final String id;
@Getter
protected final String cssId;
protected final atlantafx.base.theme.Theme theme;
static Theme getDefaultLightTheme() {
return switch (OsType.getLocal()) {
@ -228,13 +229,6 @@ public class AppTheme {
};
}
protected final String id;
@Getter
protected final String cssId;
protected final atlantafx.base.theme.Theme theme;
public boolean isDark() {
return theme.isDarkMode();
}
@ -244,8 +238,8 @@ public class AppTheme {
}
@Override
public String toTranslatedString() {
return theme.getName();
public ObservableValue<String> toTranslatedString() {
return new SimpleStringProperty(theme.getName());
}
@Override

View file

@ -14,6 +14,7 @@ public class AppTray {
private static AppTray INSTANCE;
private final AppTrayIcon icon;
@Getter
private final ErrorHandler errorHandler;

View file

@ -14,21 +14,23 @@ public class AppTrayIcon {
private final SystemTray tray;
private final TrayIcon trayIcon;
private final PopupMenu popupMenu = new PopupMenu();
public AppTrayIcon() {
ensureSystemTraySupported();
tray = SystemTray.getSystemTray();
var image = switch (OsType.getLocal()) {
case OsType.Windows windows -> "img/logo/logo_16x16.png";
case OsType.Linux linux -> "img/logo/logo_24x24.png";
case OsType.MacOs macOs -> "img/logo/logo_macos_tray_24x24.png";
};
var image =
switch (OsType.getLocal()) {
case OsType.Windows windows -> "img/logo/logo_16x16.png";
case OsType.Linux linux -> "img/logo/logo_24x24.png";
case OsType.MacOs macOs -> "img/logo/logo_macos_tray_24x24.png";
};
var url = AppResources.getResourceURL(AppResources.XPIPE_MODULE, image).orElseThrow();
this.trayIcon = new TrayIcon(loadImageFromURL(url), App.getApp().getStage().getTitle(), popupMenu);
PopupMenu popupMenu = new PopupMenu();
this.trayIcon =
new TrayIcon(loadImageFromURL(url), App.getApp().getStage().getTitle(), popupMenu);
this.trayIcon.setToolTip("XPipe");
this.trayIcon.setImageAutoSize(true);
@ -58,6 +60,19 @@ public class AppTrayIcon {
});
}
private static Image loadImageFromURL(URL iconImagePath) {
try {
return ImageIO.read(iconImagePath);
} catch (IOException e) {
ErrorEvent.fromThrowable(e).handle();
return AppImages.toAwtImage(AppImages.DEFAULT_IMAGE);
}
}
public static boolean isSupported() {
return Desktop.isDesktopSupported() && SystemTray.isSupported();
}
public final TrayIcon getAwtTrayIcon() {
return trayIcon;
}
@ -65,17 +80,7 @@ public class AppTrayIcon {
private void ensureSystemTraySupported() {
if (!SystemTray.isSupported()) {
throw new UnsupportedOperationException(
"SystemTray icons are not "
+ "supported by the current desktop environment.");
}
}
private static Image loadImageFromURL(URL iconImagePath) {
try {
return ImageIO.read(iconImagePath);
} catch (IOException e) {
ErrorEvent.fromThrowable(e).handle();
return AppImages.toAwtImage(AppImages.DEFAULT_IMAGE);
"SystemTray icons are not " + "supported by the current desktop environment.");
}
}
@ -129,11 +134,9 @@ public class AppTrayIcon {
public void showInfoMessage(String title, String message) {
if (OsType.getLocal().equals(OsType.MACOS)) {
showMacAlert(title, message,"Information");
showMacAlert(title, message, "Information");
} else {
EventQueue.invokeLater(() ->
this.trayIcon.displayMessage(
title, message, TrayIcon.MessageType.INFO));
EventQueue.invokeLater(() -> this.trayIcon.displayMessage(title, message, TrayIcon.MessageType.INFO));
}
}
@ -143,11 +146,9 @@ public class AppTrayIcon {
public void showWarningMessage(String title, String message) {
if (OsType.getLocal().equals(OsType.MACOS)) {
showMacAlert(title, message,"Warning");
showMacAlert(title, message, "Warning");
} else {
EventQueue.invokeLater(() ->
this.trayIcon.displayMessage(
title, message, TrayIcon.MessageType.WARNING));
EventQueue.invokeLater(() -> this.trayIcon.displayMessage(title, message, TrayIcon.MessageType.WARNING));
}
}
@ -157,11 +158,9 @@ public class AppTrayIcon {
public void showErrorMessage(String title, String message) {
if (OsType.getLocal().equals(OsType.MACOS)) {
showMacAlert(title, message,"Error");
showMacAlert(title, message, "Error");
} else {
EventQueue.invokeLater(() ->
this.trayIcon.displayMessage(
title, message, TrayIcon.MessageType.ERROR));
EventQueue.invokeLater(() -> this.trayIcon.displayMessage(title, message, TrayIcon.MessageType.ERROR));
}
}
@ -171,11 +170,9 @@ public class AppTrayIcon {
public void showMessage(String title, String message) {
if (OsType.getLocal().equals(OsType.MACOS)) {
showMacAlert(title, message,"Message");
showMacAlert(title, message, "Message");
} else {
EventQueue.invokeLater(() ->
this.trayIcon.displayMessage(
title, message, TrayIcon.MessageType.NONE));
EventQueue.invokeLater(() -> this.trayIcon.displayMessage(title, message, TrayIcon.MessageType.NONE));
}
}
@ -183,26 +180,14 @@ public class AppTrayIcon {
this.showMessage(null, message);
}
public static boolean isSupported() {
return Desktop.isDesktopSupported() && SystemTray.isSupported();
}
private void showMacAlert(String subTitle, String message, String title) {
String execute = String.format(
"display notification \"%s\""
+ " with title \"%s\""
+ " subtitle \"%s\"",
message != null ? message : "",
title != null ? title : "",
subTitle != null ? subTitle : ""
);
"display notification \"%s\"" + " with title \"%s\"" + " subtitle \"%s\"",
message != null ? message : "", title != null ? title : "", subTitle != null ? subTitle : "");
try {
Runtime.getRuntime()
.exec(new String[] { "osascript", "-e", execute });
Runtime.getRuntime().exec(new String[] {"osascript", "-e", execute});
} catch (IOException e) {
throw new UnsupportedOperationException(
"Cannot run osascript with given parameters.");
throw new UnsupportedOperationException("Cannot run osascript with given parameters.");
}
}
}

View file

@ -3,6 +3,7 @@ package io.xpipe.app.core;
import io.xpipe.app.comp.base.LoadingOverlayComp;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.process.OsType;
import javafx.application.Platform;
@ -19,6 +20,7 @@ import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.text.Text;
import javafx.stage.Modality;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.Window;
@ -63,6 +65,9 @@ public class AppWindowHelper {
public static Stage sideWindow(
String title, Function<Stage, Comp<?>> contentFunc, boolean bindSize, ObservableValue<Boolean> loading) {
var stage = new Stage();
if (AppMainWindow.getInstance() != null) {
stage.initOwner(AppMainWindow.getInstance().getStage());
}
stage.setTitle(title);
if (AppMainWindow.getInstance() != null) {
stage.initOwner(AppMainWindow.getInstance().getStage());
@ -72,6 +77,10 @@ public class AppWindowHelper {
setupContent(stage, contentFunc, bindSize, loading);
setupStylesheets(stage.getScene());
if (AppPrefs.get() != null && AppPrefs.get().enforceWindowModality().get()) {
stage.initModality(Modality.WINDOW_MODAL);
}
stage.setOnShown(e -> {
// If we set the theme pseudo classes earlier when the window is not shown
// they do not apply. Is this a bug in JavaFX?
@ -100,8 +109,7 @@ public class AppWindowHelper {
childStage.setY(stage.getY() + stage.getHeight() / 2 - childStage.getHeight() / 2);
}
public static void showAlert(
Consumer<Alert> c, Consumer<Optional<ButtonType>> bt) {
public static void showAlert(Consumer<Alert> c, Consumer<Optional<ButtonType>> bt) {
ThreadHelper.runAsync(() -> {
var r = showBlockingAlert(c);
if (bt != null) {
@ -110,6 +118,36 @@ public class AppWindowHelper {
});
}
public static void setContent(Alert alert, String s) {
alert.getDialogPane().setMinWidth(505);
alert.getDialogPane().setPrefWidth(505);
alert.getDialogPane().setMaxWidth(505);
alert.getDialogPane().setContent(AppWindowHelper.alertContentText(s));
}
public static boolean showConfirmationAlert(String title, String header, String content) {
return AppWindowHelper.showBlockingAlert(alert -> {
alert.titleProperty().bind(AppI18n.observable(title));
alert.headerTextProperty().bind(AppI18n.observable(header));
setContent(alert, AppI18n.get(content));
alert.setAlertType(Alert.AlertType.CONFIRMATION);
})
.map(b -> b.getButtonData().isDefaultButton())
.orElse(false);
}
public static boolean showConfirmationAlert(
ObservableValue<String> title, ObservableValue<String> header, ObservableValue<String> content) {
return AppWindowHelper.showBlockingAlert(alert -> {
alert.titleProperty().bind(title);
alert.headerTextProperty().bind(header);
setContent(alert, content.getValue());
alert.setAlertType(Alert.AlertType.CONFIRMATION);
})
.map(b -> b.getButtonData().isDefaultButton())
.orElse(false);
}
public static Optional<ButtonType> showBlockingAlert(Consumer<Alert> c) {
Supplier<Alert> supplier = () -> {
Alert a = AppWindowHelper.createEmptyAlert();
@ -224,7 +262,6 @@ public class AppWindowHelper {
if (event.getCode().equals(KeyCode.W) && event.isShortcutDown()) {
stage.close();
event.consume();
return;
}
}
});
@ -236,7 +273,11 @@ public class AppWindowHelper {
}
var allScreenBounds = computeWindowScreenBounds(stage);
if (!areNumbersValid(allScreenBounds.getMinX(), allScreenBounds.getMinY(), allScreenBounds.getMaxX(), allScreenBounds.getMaxY())) {
if (!areNumbersValid(
allScreenBounds.getMinX(),
allScreenBounds.getMinY(),
allScreenBounds.getMaxX(),
allScreenBounds.getMaxY())) {
return Optional.empty();
}
@ -287,41 +328,44 @@ public class AppWindowHelper {
private static List<Screen> getWindowScreens(Stage stage) {
if (!areNumbersValid(stage.getX(), stage.getY(), stage.getWidth(), stage.getHeight())) {
return stage.getOwner() != null && stage.getOwner() instanceof Stage ownerStage ? getWindowScreens(ownerStage) : List.of(Screen.getPrimary());
return stage.getOwner() != null && stage.getOwner() instanceof Stage ownerStage
? getWindowScreens(ownerStage)
: List.of(Screen.getPrimary());
}
return Screen.getScreensForRectangle(new Rectangle2D(stage.getX(), stage.getY(), stage.getWidth(), stage.getHeight()));
return Screen.getScreensForRectangle(
new Rectangle2D(stage.getX(), stage.getY(), stage.getWidth(), stage.getHeight()));
}
private static Rectangle2D computeWindowScreenBounds(Stage stage) {
double minX = Double.POSITIVE_INFINITY ;
double minY = Double.POSITIVE_INFINITY ;
double maxX = Double.NEGATIVE_INFINITY ;
double maxY = Double.NEGATIVE_INFINITY ;
double minX = Double.POSITIVE_INFINITY;
double minY = Double.POSITIVE_INFINITY;
double maxX = Double.NEGATIVE_INFINITY;
double maxY = Double.NEGATIVE_INFINITY;
for (Screen screen : getWindowScreens(stage)) {
Rectangle2D screenBounds = screen.getBounds();
if (screenBounds.getMinX() < minX) {
minX = screenBounds.getMinX();
}
if (screenBounds.getMinY() < minY) {
minY = screenBounds.getMinY() ;
minY = screenBounds.getMinY();
}
if (screenBounds.getMaxX() > maxX) {
maxX = screenBounds.getMaxX();
}
if (screenBounds.getMaxY() > maxY) {
maxY = screenBounds.getMaxY() ;
maxY = screenBounds.getMaxY();
}
}
// Taskbar adjustment
maxY -= 50;
var w = maxX-minX;
var h = maxY-minY;
var w = maxX - minX;
var h = maxY - minY;
// This should not happen but on weird Linux systems nothing is impossible
if (w < 0 || h < 0) {
return new Rectangle2D(0,0,800, 600);
return new Rectangle2D(0, 0, 800, 600);
}
return new Rectangle2D(minX, minY, w, h);

View file

@ -17,54 +17,6 @@ import java.util.Optional;
public class AppAvCheck {
@Getter
public static enum AvType {
BITDEFENDER("Bitdefender") {
@Override
public String getDescription() {
return "Bitdefender sometimes isolates XPipe and some shell programs, effectively making it unusable.";
}
@Override
public boolean isActive() {
return WindowsRegistry.exists(WindowsRegistry.HKEY_LOCAL_MACHINE,"SOFTWARE\\Bitdefender", "InstallDir");
}
},
MALWAREBYTES("Malwarebytes") {
@Override
public String getDescription() {
return "The free Malwarebytes version performs less invasive scans, so it shouldn't be a problem. If you are running the paid Malwarebytes Pro version, you will have access to the `Exploit Protection` under the `Real-time Protection` mode. When this setting is active, any shell access is slowed down, resulting in XPipe becoming very slow.";
}
@Override
public boolean isActive() {
return WindowsRegistry.exists(WindowsRegistry.HKEY_LOCAL_MACHINE,"SOFTWARE\\Malwarebytes", "id");
}
},
MCAFEE("McAfee") {
@Override
public String getDescription() {
return "McAfee slows down XPipe considerably. It also sometimes preemptively disables some Win32 commands that XPipe depends on, leading to errors.";
}
@Override
public boolean isActive() {
return WindowsRegistry.exists(WindowsRegistry.HKEY_LOCAL_MACHINE,"SOFTWARE\\McAfee", "mi");
}
};
private final String name;
AvType(String name) {
this.name = name;
}
public abstract String getDescription();
public abstract boolean isActive();
}
private static Optional<AvType> detect() {
for (AvType value : AvType.values()) {
if (value.isActive()) {
@ -93,21 +45,75 @@ public class AppAvCheck {
alert.setTitle(AppI18n.get("antivirusNoticeTitle"));
alert.setAlertType(Alert.AlertType.NONE);
AppResources.with(
AppResources.XPIPE_MODULE,
"misc/antivirus.md",
file -> {
var markdown = new MarkdownComp(Files.readString(file), s -> {
AppResources.with(AppResources.XPIPE_MODULE, "misc/antivirus.md", file -> {
var markdown = new MarkdownComp(Files.readString(file), s -> {
var t = found.get();
return s.formatted(t.getName(), t.getName(), t.getDescription(), AppProperties.get().getVersion(), AppProperties.get().getVersion(), t.getName());
}).prefWidth(550).prefHeight(600).createRegion();
alert.getDialogPane().setContent(markdown);
alert.getDialogPane().setPadding(new Insets(15));
});
return s.formatted(
t.getName(),
t.getName(),
t.getDescription(),
AppProperties.get().getVersion(),
AppProperties.get().getVersion(),
t.getName());
})
.prefWidth(550)
.prefHeight(600)
.createRegion();
alert.getDialogPane().setContent(markdown);
alert.getDialogPane().setPadding(new Insets(15));
});
alert.getButtonTypes().add(new ButtonType(AppI18n.get("gotIt"), ButtonBar.ButtonData.OK_DONE));
});
a.filter(b -> b.getButtonData().isDefaultButton())
.ifPresentOrElse(buttonType -> {}, () -> OperationMode.halt(1));
}
@Getter
public enum AvType {
BITDEFENDER("Bitdefender") {
@Override
public String getDescription() {
return "Bitdefender sometimes isolates XPipe and some shell programs, effectively making it unusable.";
}
@Override
public boolean isActive() {
return WindowsRegistry.exists(
WindowsRegistry.HKEY_LOCAL_MACHINE, "SOFTWARE\\Bitdefender", "InstallDir");
}
},
MALWAREBYTES("Malwarebytes") {
@Override
public String getDescription() {
return "The free Malwarebytes version performs less invasive scans, so it shouldn't be a problem. If you are running the paid Malwarebytes Pro version, you will have access to the `Exploit Protection` under the `Real-time Protection` mode. When this setting is active, any shell access is slowed down, resulting in XPipe becoming very slow.";
}
@Override
public boolean isActive() {
return WindowsRegistry.exists(WindowsRegistry.HKEY_LOCAL_MACHINE, "SOFTWARE\\Malwarebytes", "id");
}
},
MCAFEE("McAfee") {
@Override
public String getDescription() {
return "McAfee slows down XPipe considerably. It also sometimes preemptively disables some Win32 commands that XPipe depends on, leading to errors.";
}
@Override
public boolean isActive() {
return WindowsRegistry.exists(WindowsRegistry.HKEY_LOCAL_MACHINE, "SOFTWARE\\McAfee", "mi");
}
};
private final String name;
AvType(String name) {
this.name = name;
}
public abstract String getDescription();
public abstract boolean isActive();
}
}

View file

@ -0,0 +1,36 @@
package io.xpipe.app.core.check;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.core.process.OsType;
import java.util.concurrent.TimeUnit;
public class AppCertutilCheck {
private static boolean getResult() {
var fc = new ProcessBuilder(System.getenv("WINDIR") + "\\System32\\certutil")
.redirectError(ProcessBuilder.Redirect.DISCARD);
try {
var proc = fc.start();
var out = new String(proc.getInputStream().readAllBytes());
proc.waitFor(1, TimeUnit.SECONDS);
return proc.exitValue() == 0 && !out.contains("The system cannot execute the specified program");
} catch (Exception e) {
return false;
}
}
public static void check() {
if (AppPrefs.get().disableCertutilUse().get()) {
return;
}
if (!OsType.getLocal().equals(OsType.WINDOWS)) {
return;
}
if (!getResult()) {
AppPrefs.get().disableCertutilUse.set(true);
}
}
}

View file

@ -2,8 +2,8 @@ package io.xpipe.app.core.check;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.LocalShell;
import io.xpipe.core.process.ProcessControlProvider;
import io.xpipe.core.process.ProcessOutputException;
import io.xpipe.core.process.ShellDialects;
import java.util.Optional;
@ -12,7 +12,8 @@ public class AppShellCheck {
public static void check() {
var err = selfTestErrorCheck();
if (err.isPresent()) {
var msg = """
var msg =
"""
Shell self-test failed for %s:
%s
@ -24,7 +25,12 @@ public class AppShellCheck {
- The operating system is not supported
You can reach out to us if you want to properly diagnose the cause individually and hopefully fix it.
""".formatted(ShellDialects.getPlatformDefault().getDisplayName(), err.get());
"""
.formatted(
ProcessControlProvider.get()
.getEffectiveLocalDialect()
.getDisplayName(),
err.get());
ErrorEvent.fromThrowable(new IllegalStateException(msg)).handle();
}
}

View file

@ -18,8 +18,8 @@ public class AppTempCheck {
}
if (dir == null || !Files.exists(dir) || !Files.isDirectory(dir)) {
ErrorEvent.fromThrowable(
new IOException("Specified temporary directory " + tmpdir + ", set via the environment variable %TEMP% is invalid."))
ErrorEvent.fromThrowable(new IOException("Specified temporary directory " + tmpdir
+ ", set via the environment variable %TEMP% is invalid."))
.term()
.handle();
}

View file

@ -4,16 +4,18 @@ import io.xpipe.app.browser.BrowserModel;
import io.xpipe.app.comp.store.StoreViewState;
import io.xpipe.app.core.*;
import io.xpipe.app.core.check.AppAvCheck;
import io.xpipe.app.core.check.AppCertutilCheck;
import io.xpipe.app.core.check.AppShellCheck;
import io.xpipe.app.ext.ActionProvider;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.GitStorageHandler;
import io.xpipe.app.update.XPipeDistributionType;
import io.xpipe.app.util.FileBridge;
import io.xpipe.app.util.LicenseProvider;
import io.xpipe.app.util.LocalShell;
import io.xpipe.app.util.LockedSecretValue;
import io.xpipe.app.util.UnlockAlert;
import io.xpipe.core.util.JacksonMapper;
public class BaseMode extends OperationMode {
@ -39,29 +41,29 @@ public class BaseMode extends OperationMode {
// For debugging
// if (true) throw new IllegalStateException();
TrackEvent.info("mode", "Initializing base mode components ...");
TrackEvent.info("Initializing base mode components ...");
AppExtensionManager.init(true);
JacksonMapper.initModularized(AppExtensionManager.getInstance().getExtendedLayer());
JacksonMapper.configure(objectMapper -> {
objectMapper.registerSubtypes(LockedSecretValue.class);
});
// Load translations before storage initialization to localize store error messages
// Also loaded before antivirus alert to localize that
AppI18n.init();
LicenseProvider.get().init();
AppPrefs.initLocal();
AppCertutilCheck.check();
AppAvCheck.check();
LocalShell.init();
AppShellCheck.check();
XPipeDistributionType.init();
AppPrefs.init();
AppCharsets.init();
AppCharsetter.init();
AppShellCheck.check();
AppPrefs.setDefaults();
// Initialize socket server as we should be prepared for git askpass commands
AppSocketServer.init();
GitStorageHandler.getInstance().init();
GitStorageHandler.getInstance().setupRepositoryAndPull();
AppPrefs.initSharedRemote();
UnlockAlert.showIfNeeded();
DataStorage.init();
AppFileWatcher.init();
FileBridge.init();
ActionProvider.initProviders();
TrackEvent.info("mode", "Finished base components initialization");
TrackEvent.info("Finished base components initialization");
initialized = true;
}
@ -70,7 +72,7 @@ public class BaseMode extends OperationMode {
@Override
public void finalTeardown() {
TrackEvent.info("mode", "Background mode shutdown started");
TrackEvent.info("Background mode shutdown started");
BrowserModel.DEFAULT.reset();
StoreViewState.reset();
DataStorage.reset();
@ -80,6 +82,6 @@ public class BaseMode extends OperationMode {
AppDataLock.unlock();
// Shut down socket server last to keep a non-daemon thread running
AppSocketServer.reset();
TrackEvent.info("mode", "Background mode shutdown finished");
TrackEvent.info("Background mode shutdown finished");
}
}

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