Squash branch commits

This commit is contained in:
crschnick 2024-02-13 12:10:20 +00:00
parent ce45ff9ec6
commit 7c7fa28190
317 changed files with 6133 additions and 7951 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,29 +0,0 @@
package io.xpipe.api.test;
import io.xpipe.api.DataTableAccumulator;
import io.xpipe.core.data.node.TupleNode;
import io.xpipe.core.data.node.ValueNode;
import io.xpipe.core.data.type.TupleType;
import io.xpipe.core.data.type.ValueType;
import org.junit.jupiter.api.Test;
import java.util.List;
public class DataTableAccumulatorTest extends ApiTest {
@Test
public void test() {
var type = TupleType.of(List.of("col1", "col2"), List.of(ValueType.of(), ValueType.of()));
var acc = DataTableAccumulator.create(type);
var val = type.convert(TupleNode.of(List.of(ValueNode.of("val1"), ValueNode.of("val2"))))
.orElseThrow();
acc.add(val);
var table = acc.finish(":test");
// Assertions.assertEquals(table.getInfo().getDataType(), TupleType.tableType(List.of("col1", "col2")));
// Assertions.assertEquals(table.getInfo().getRowCountIfPresent(), OptionalInt.empty());
// var read = table.read(1).at(0);
// Assertions.assertEquals(val, read);
}
}

View file

@ -1,22 +0,0 @@
package io.xpipe.api.test;
import io.xpipe.api.DataSource;
import io.xpipe.core.store.DataStoreId;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
public class DataTableTest extends ApiTest {
@BeforeAll
public static void setupStorage() throws Exception {
DataSource.create(
DataStoreId.fromString(":usernames"), "csv", DataTableTest.class.getResource("username.csv"));
}
@Test
public void testGet() {
var table = DataSource.getById(":usernames").asTable();
var r = table.read(2);
var a = 0;
}
}

View file

@ -0,0 +1,14 @@
package io.xpipe.api.test;
import io.xpipe.beacon.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,122 +1,68 @@
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.4'
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"
]
}
apply from: "$rootDir/gradle/gradle_scripts/junit_suite.gradle"
import org.gradle.internal.os.OperatingSystem
@ -125,7 +71,6 @@ if (OperatingSystem.current() == OperatingSystem.LINUX) {
}
def extensionJarDepList = project.allExtensions.stream().map(p -> p.getTasksByName('jar', true)).toList();
jar {
finalizedBy(extensionJarDepList)
}
@ -146,14 +91,18 @@ 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.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 +130,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

@ -1,159 +0,0 @@
dependencies {
implementation files("${project.layout.buildDirectory.get()}/generated-modules/flexmark-0.64.0.jar")
implementation files("${project.layout.buildDirectory.get()}/generated-modules/flexmark-util-data-0.64.0.jar")
implementation files("${project.layout.buildDirectory.get()}/generated-modules/flexmark-util-ast-0.64.0.jar")
implementation files("${project.layout.buildDirectory.get()}/generated-modules/flexmark-util-builder-0.64.0.jar")
implementation files("${project.layout.buildDirectory.get()}/generated-modules/flexmark-util-sequence-0.64.0.jar")
implementation files("${project.layout.buildDirectory.get()}/generated-modules/flexmark-util-misc-0.64.0.jar")
implementation files("${project.layout.buildDirectory.get()}/generated-modules/flexmark-util-dependency-0.64.0.jar")
implementation files("${project.layout.buildDirectory.get()}/generated-modules/flexmark-util-collection-0.64.0.jar")
implementation files("${project.layout.buildDirectory.get()}/generated-modules/flexmark-util-format-0.64.0.jar")
implementation files("${project.layout.buildDirectory.get()}/generated-modules/flexmark-util-html-0.64.0.jar")
implementation files("${project.layout.buildDirectory.get()}/generated-modules/flexmark-util-visitor-0.64.0.jar")
}
addDependenciesModuleInfo {
overwriteExistingFiles = true
jdepsExtraArgs = ['-q']
outputDirectory = file("${project.layout.buildDirectory.get()}/generated-modules")
modules {
module {
artifact 'com.vladsch.flexmark:flexmark:0.64.0'
moduleInfoSource = '''
module com.vladsch.flexmark {
exports com.vladsch.flexmark.html;
exports com.vladsch.flexmark.html.renderer;
exports com.vladsch.flexmark.parser;
exports com.vladsch.flexmark.parser.core;
requires com.vladsch.flexmark_util_data;
requires com.vladsch.flexmark_util_ast;
requires com.vladsch.flexmark_util_builder;
requires com.vladsch.flexmark_util_sequence;
requires com.vladsch.flexmark_util_misc;
requires com.vladsch.flexmark_util_dependency;
requires com.vladsch.flexmark_util_collection;
requires com.vladsch.flexmark_util_format;
requires com.vladsch.flexmark_util_html;
requires com.vladsch.flexmark_util_visitor;
}
'''
}
module {
artifact 'com.vladsch.flexmark:flexmark-util-data:0.64.0'
moduleInfoSource = '''
module com.vladsch.flexmark_util_data {
exports com.vladsch.flexmark.util.data;
requires com.vladsch.flexmark_util_misc;
}
'''
}
module {
artifact 'com.vladsch.flexmark:flexmark-util-ast:0.64.0'
moduleInfoSource = '''
module com.vladsch.flexmark_util_ast {
exports com.vladsch.flexmark.util.ast;
requires com.vladsch.flexmark_util_data;
requires com.vladsch.flexmark_util_misc;
requires com.vladsch.flexmark_util_collection;
requires com.vladsch.flexmark_util_sequence;
requires com.vladsch.flexmark_util_visitor;
}
'''
}
module {
artifact 'com.vladsch.flexmark:flexmark-util-builder:0.64.0'
moduleInfoSource = '''
module com.vladsch.flexmark_util_builder {
exports com.vladsch.flexmark.util.builder;
requires com.vladsch.flexmark_util_data;
requires com.vladsch.flexmark_util_misc;
}
'''
}
module {
artifact 'com.vladsch.flexmark:flexmark-util-sequence:0.64.0'
moduleInfoSource = '''
module com.vladsch.flexmark_util_sequence {
exports com.vladsch.flexmark.util.sequence;
exports com.vladsch.flexmark.util.sequence.mappers;
exports com.vladsch.flexmark.util.sequence.builder;
opens com.vladsch.flexmark.util.sequence;
requires com.vladsch.flexmark_util_misc;
requires com.vladsch.flexmark_util_data;
requires com.vladsch.flexmark_util_collection;
}
'''
}
module {
artifact 'com.vladsch.flexmark:flexmark-util-misc:0.64.0'
moduleInfoSource = '''
module com.vladsch.flexmark_util_misc {
exports com.vladsch.flexmark.util.misc;
}
'''
}
module {
artifact 'com.vladsch.flexmark:flexmark-util-dependency:0.64.0'
moduleInfoSource = '''
module com.vladsch.flexmark_util_dependency {
exports com.vladsch.flexmark.util.dependency;
requires com.vladsch.flexmark_util_collection;
requires com.vladsch.flexmark_util_misc;
}
'''
}
module {
artifact 'com.vladsch.flexmark:flexmark-util-collection:0.64.0'
moduleInfoSource = '''
module com.vladsch.flexmark_util_collection {
exports com.vladsch.flexmark.util.collection;
exports com.vladsch.flexmark.util.collection.iteration;
requires com.vladsch.flexmark_util_misc;
}
'''
}
module {
artifact 'com.vladsch.flexmark:flexmark-util-format:0.64.0'
moduleInfoSource = '''
module com.vladsch.flexmark_util_format {
exports com.vladsch.flexmark.util.format;
requires com.vladsch.flexmark_util_data;
requires com.vladsch.flexmark_util_sequence;
requires com.vladsch.flexmark_util_misc;
requires com.vladsch.flexmark_util_ast;
requires com.vladsch.flexmark_util_collection;
}
'''
}
module {
artifact 'com.vladsch.flexmark:flexmark-util-html:0.64.0'
moduleInfoSource = '''
module com.vladsch.flexmark_util_html {
exports com.vladsch.flexmark.util.html;
opens com.vladsch.flexmark.util.html;
requires com.vladsch.flexmark_util_misc;
requires com.vladsch.flexmark_util_sequence;
}
'''
}
module {
artifact 'com.vladsch.flexmark:flexmark-util-visitor:0.64.0'
moduleInfoSource = '''
module com.vladsch.flexmark_util_visitor {
exports com.vladsch.flexmark.util.visitor;
}
'''
}
}
}

View file

@ -1,30 +0,0 @@
dependencies {
implementation files("${project.layout.buildDirectory.get()}/generated-modules/github-api-1.301.jar")
}
addDependenciesModuleInfo {
overwriteExistingFiles = true
jdepsExtraArgs = ['-q']
outputDirectory = file("${project.layout.buildDirectory.get()}/generated-modules")
modules {
module {
artifact 'org.kohsuke:github-api:1.301'
moduleInfoSource = '''
module org.kohsuke.github {
exports org.kohsuke.github;
exports org.kohsuke.github.function;
exports org.kohsuke.github.authorization;
exports org.kohsuke.github.extras;
exports org.kohsuke.github.connector;
requires java.logging;
requires org.apache.commons.io;
requires org.apache.commons.lang3;
requires com.fasterxml.jackson.databind;
opens org.kohsuke.github;
}
'''
}
}
}

View file

@ -1,87 +0,0 @@
dependencies {
implementation files("${project.layout.buildDirectory.get()}/generated-modules/richtextfx-0.10.6.jar")
implementation files("${project.layout.buildDirectory.get()}/generated-modules/flowless-0.6.6.jar")
implementation files("${project.layout.buildDirectory.get()}/generated-modules/undofx-2.1.1.jar")
implementation files("${project.layout.buildDirectory.get()}/generated-modules/wellbehavedfx-0.3.3.jar")
implementation files("${project.layout.buildDirectory.get()}/generated-modules/reactfx-2.0-M5.jar")
}
addDependenciesModuleInfo {
overwriteExistingFiles = true
jdepsExtraArgs = ['-q']
outputDirectory = file("${project.layout.buildDirectory.get()}/generated-modules")
modules {
module {
artifact group: 'org.fxmisc.flowless', name: 'flowless', version: '0.6.6'
moduleInfoSource = '''
module org.fxmisc.flowless {
exports org.fxmisc.flowless;
requires static javafx.base;
requires static javafx.controls;
requires org.reactfx;
requires org.fxmisc.wellbehavedfx;
}
'''
}
module {
artifact group: 'org.fxmisc.undo', name: 'undofx', version: '2.1.1'
moduleInfoSource = '''
module org.fxmisc.undofx {
exports org.fxmisc.undo;
requires static javafx.base;
requires static javafx.controls;
requires org.reactfx;
requires org.fxmisc.wellbehavedfx;
}
'''
}
module {
artifact group: 'org.fxmisc.wellbehaved', name: 'wellbehavedfx', version: '0.3.3'
moduleInfoSource = '''
module org.fxmisc.wellbehavedfx {
exports org.fxmisc.wellbehaved.event;
exports org.fxmisc.wellbehaved.event.template;
requires static javafx.base;
requires static javafx.controls;
requires org.reactfx;
}
'''
}
module {
artifact group: 'org.fxmisc.richtext', name: 'richtextfx', version: '0.10.6'
moduleInfoSource = '''
module org.fxmisc.richtext {
exports org.fxmisc.richtext;
exports org.fxmisc.richtext.model;
exports org.fxmisc.richtext.event;
requires org.fxmisc.flowless;
requires org.fxmisc.undofx;
requires org.fxmisc.wellbehavedfx;
requires static javafx.base;
requires static javafx.controls;
requires org.reactfx;
}
'''
}
module {
artifact group: 'org.reactfx', name: 'reactfx', version: '2.0-M5'
moduleInfoSource = '''
module org.reactfx {
exports org.reactfx;
exports org.reactfx.collection;
exports org.reactfx.value;
exports org.reactfx.util;
requires static javafx.base;
requires static javafx.controls;
}
'''
}
}
}

View file

@ -1,41 +0,0 @@
dependencies {
implementation files("${project.layout.buildDirectory.get()}/generated-modules/sentry-6.29.0.jar")
}
addDependenciesModuleInfo {
overwriteExistingFiles = true
jdepsExtraArgs = ['-q']
outputDirectory = file("${project.layout.buildDirectory.get()}/generated-modules")
modules {
module {
artifact 'io.sentry:sentry:6.29.0'
moduleInfoSource = '''
module io.sentry {
exports io.sentry;
opens io.sentry;
exports io.sentry.protocol;
opens io.sentry.protocol;
exports io.sentry.config;
opens io.sentry.config;
exports io.sentry.transport;
opens io.sentry.transport;
exports io.sentry.util;
opens io.sentry.util;
exports io.sentry.cache;
opens io.sentry.cache;
exports io.sentry.exception;
opens io.sentry.exception;
exports io.sentry.hints;
opens io.sentry.hints;
}
'''
}
}
}

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,14 @@
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

@ -5,12 +5,46 @@ 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 enum FileConflictChoice {
CANCEL,
SKIP,
SKIP_ALL,
REPLACE,
REPLACE_ALL
}
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;

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;
@ -32,7 +32,7 @@ public class BrowserClipboard {
public String toClipboardString() {
return entries.stream().map(fileEntry -> "\"" + fileEntry.getPath() + "\"").collect(
Collectors.joining(ShellDialects.getPlatformDefault().getNewLine().getNewLineString()));
Collectors.joining(ProcessControlProvider.get().getEffectiveLocalDialect().getNewLine().getNewLineString()));
}
}

View file

@ -69,11 +69,6 @@ public class BrowserComp extends SimpleComp {
return true;
}
// Also show on local
if (model.getSelected().getValue() != null) {
// return model.getSelected().getValue().isLocal();
}
return false;
}, model.getOpenFileSystems(), model.getSelected())));
localDownloadStage.prefHeight(200);

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

@ -70,6 +70,12 @@ 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) {

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,46 @@ 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 {
return (model.getProgress().getValue().getName() != null ? model.getProgress().getValue().getName() + " " : "") + transferredCount.getValue() + " / " + allCount.getValue();
}
},
transferredCount,
allCount,
model.getProgress()));
return progressComp;
}
private Comp<?> createClipboardStatus() {
var cc = PlatformThread.sync(BrowserClipboard.currentCopyClipboard);
var ccCount = Bindings.createStringBinding(
() -> {
@ -32,7 +74,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 +91,6 @@ public class BrowserStatusBarComp extends SimpleComp {
.count();
},
model.getFileList().getAll()));
var selectedComp = new LabelComp(Bindings.createStringBinding(
() -> {
if (selectedCount.getValue().intValue() == 0) {
@ -57,19 +101,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,48 @@ 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 binding = BindingsHelper.mappedContentBinding(model.getItems(), item -> item.getFileEntry());
var list = new BrowserSelectionListComp(binding, entry -> Bindings.createStringBinding(() -> {
var sourceItem = stage.getItems().stream().filter(item -> item.getFileEntry() == entry).findAny();
var sourceItem = model.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("?");
var name = sourceItem.get().downloadFinished().get() ? "Local" : DataStorage.get().getStoreDisplayName(entry.getFileSystem().getStore()).orElse("?");
return FileNames.getFileName(entry.getPath()) + " (" + name + ")";
}, stage.getAllDownloaded()))
}, 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()))))
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 +93,45 @@ 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);
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();
var selected = model.getItems().stream().map(BrowserTransferModel.Item::getFileEntry).toList();
Dragboard db = struc.get().startDragAndDrop(TransferMode.COPY);
db.setContent(BrowserClipboard.startDrag(null, selected));
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();
@ -140,31 +148,26 @@ public class BrowserTransferComp extends SimpleComp {
})
.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();
model.clear();
event.consume();
});
}),
PlatformThread.sync(stage.getDownloading()));
PlatformThread.sync(model.getDownloading()));
return stack.styleClass("transfer").createRegion();
}
}

View file

@ -4,14 +4,20 @@ import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.BooleanScope;
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;
@ -33,10 +39,25 @@ public class BrowserTransferModel {
@Value
public static class Item {
OpenFileSystemModel openFileSystemModel;
String name;
FileSystem.FileEntry fileEntry;
Path localFile;
BooleanProperty finishedDownload = new SimpleBooleanProperty();
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);
}
}
BrowserModel browserModel;
@ -45,15 +66,18 @@ public class BrowserTransferModel {
BooleanProperty allDownloaded = new SimpleBooleanProperty();
public void clear() {
try {
FileUtils.deleteDirectory(TEMP.toFile());
try (var ls = Files.list(TEMP)) {
var list = ls.toList();
for (Path path : list) {
Files.delete(path);
}
} catch (IOException e) {
ErrorEvent.fromThrowable(e).handle();
}
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 +85,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,7 +128,11 @@ 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;
}
@ -86,9 +141,12 @@ public class BrowserTransferModel {
FileSystemHelper.dropFilesInto(
FileSystemHelper.getLocal(TEMP),
List.of(item.getFileEntry()),
true);
true,
progress -> {
item.getProgress().setValue(progress);
item.getOpenFileSystemModel().getProgress().setValue(progress);
});
}
item.finishedDownload.set(true);
} catch (Throwable t) {
ErrorEvent.fromThrowable(t).handle();
items.remove(item);

View file

@ -0,0 +1,27 @@
package io.xpipe.app.browser;
import lombok.Value;
@Value
public class BrowserTransferProgress {
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);
}
String name;
long transferred;
long total;
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;
@ -72,7 +72,7 @@ public class BrowserWelcomeComp extends SimpleComp {
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);
AppFont.setSize(header, 1);
vbox.getChildren().add(header);
var storeList = new VBox();

View file

@ -7,10 +7,16 @@ 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 {
@ -144,8 +150,7 @@ public class FileSystemHelper {
Files.isDirectory(file) ? FileKind.DIRECTORY : FileKind.FILE);
}
public static void dropLocalFilesInto(FileSystem.FileEntry entry, List<Path> files) {
try {
public static void dropLocalFilesInto(FileSystem.FileEntry entry, List<Path> files, Consumer<BrowserTransferProgress> progress) throws Exception {
var entries = files.stream()
.map(path -> {
try {
@ -155,14 +160,11 @@ public class FileSystemHelper {
}
})
.toList();
dropFilesInto(entry, entries, false);
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
}
dropFilesInto(entry, entries, false, p -> progress.accept(p));
}
public static void delete(List<FileSystem.FileEntry> files) {
if (files.size() == 0) {
if (files.isEmpty()) {
return;
}
@ -176,22 +178,33 @@ 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, 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);
progress.accept(BrowserTransferProgress.finished(file.getName(), file.getSize()));
} else {
dropFileAcrossFileSystems(target, file);
dropFileAcrossFileSystems(target, file, progress, lastConflictChoice, files.size() > 1);
}
}
}
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) throws Exception {
// Prevent dropping directory into itself
if (source.getPath().equals(target.getPath())) {
return;
@ -208,6 +221,10 @@ public class FileSystemHelper {
throw ErrorEvent.unreportable(new IllegalArgumentException("Target directory " + targetFile + " does already exist"));
}
if (!handleChoice(lastConflictChoice, target.getFileSystem(), targetFile, multiple)) {
return;
}
if (explicitCopy) {
target.getFileSystem().copy(sourceFile, targetFile);
} else {
@ -215,7 +232,7 @@ 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)
throws Exception {
if (target.getKind() != FileKind.DIRECTORY) {
throw new IllegalStateException("Target " + target.getPath() + " is not a directory");
@ -229,6 +246,7 @@ public class FileSystemHelper {
return;
}
AtomicLong totalSize = new AtomicLong();
if (source.getKind() == FileKind.DIRECTORY) {
var directoryName = FileNames.getFileName(source.getPath());
flatFiles.put(source, directoryName);
@ -237,11 +255,14 @@ public class FileSystemHelper {
List<FileSystem.FileEntry> list = source.getFileSystem().listFilesRecursively(source.getPath());
list.forEach(fileEntry -> {
flatFiles.put(fileEntry, FileNames.toUnix(FileNames.relativize(baseRelative, fileEntry.getPath())));
totalSize.addAndGet(fileEntry.getSize());
});
} else {
flatFiles.put(source, FileNames.getFileName(source.getPath()));
totalSize.addAndGet(source.getSize());
}
AtomicLong transferred = new AtomicLong();
for (var e : flatFiles.entrySet()) {
var sourceFile = e.getKey();
var targetFile = FileNames.join(target.getPath(), e.getValue());
@ -252,11 +273,106 @@ 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 (!handleChoice(lastConflictChoice, target.getFileSystem(), targetFile, multiple || flatFiles.size() > 1)) {
continue;
}
InputStream inputStream = null;
OutputStream outputStream = null;
try {
inputStream = sourceFile.getFileSystem().openInput(sourceFile.getPath());
outputStream = target.getFileSystem().openOutput(targetFile, source.getSize());
transfer(source,inputStream, outputStream,transferred, totalSize, progress);
inputStream.transferTo(OutputStream.nullOutputStream());
} catch (Exception ex) {
if (inputStream != null) {
try {
inputStream.close();
} catch (Exception om) {
ErrorEvent.fromThrowable(om).handle();
}
}
if (outputStream != null) {
try {
outputStream.close();
} catch (Exception om) {
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 final int DEFAULT_BUFFER_SIZE = 16384;
private static void transfer(FileSystem.FileEntry source, InputStream inputStream, OutputStream outputStream, AtomicLong transferred, AtomicLong total, Consumer<BrowserTransferProgress> progress) throws IOException {
var bs = (int) Math.min(DEFAULT_BUFFER_SIZE, source.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(source.getName(), transferred.get(), total.get()));
}
}
}

View file

@ -6,7 +6,7 @@ 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.store.*;
@ -40,8 +40,8 @@ public final class OpenFileSystemModel {
private final BooleanProperty inOverview = new SimpleBooleanProperty();
private final String name;
private final String tooltip;
private boolean local;
private int customScriptsStartIndex;
private final Property<BrowserTransferProgress> progress = new SimpleObjectProperty<>(BrowserTransferProgress.empty());
public OpenFileSystemModel(BrowserModel browserModel, DataStoreEntryRef<? extends FileSystemStore> entry) {
this.browserModel = browserModel;
@ -56,6 +56,19 @@ 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() {
if (fileSystem == null) {
return;
}
var s = fileSystem.getShell();
s.ifPresent(ShellControl::start);
}
public void withShell(FailableConsumer<ShellControl, Exception> c, boolean refresh) {
ThreadHelper.runFailableAsync(() -> {
if (fileSystem == null) {
@ -132,7 +145,7 @@ public final class OpenFileSystemModel {
}
// Start shell in case we exited
getFileSystem().getShell().orElseThrow().start();
startIfNeeded();
// Fix common issues with paths
var adjustedPath = FileSystemHelper.adjustPath(this, path);
@ -158,26 +171,21 @@ 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,
directory,
fileSystem
.getShell()
.get()
.subShell(processControl -> adjustedPath, (sc) -> adjustedPath)
.withInitSnippet(new SimpleScriptSnippet(
fileSystem
.getShell()
.get()
.getShellDialect()
.getCdCommand(currentPath.get()),
ScriptSnippet.ExecutionType.BOTH)));
.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 +235,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 +256,8 @@ public final class OpenFileSystemModel {
return;
}
FileSystemHelper.dropLocalFilesInto(entry, files);
startIfNeeded();
FileSystemHelper.dropLocalFilesInto(entry, files, progress::setValue);
refreshSync();
});
});
@ -266,14 +276,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, browserTransferProgress -> {
progress.setValue(browserTransferProgress);
});
refreshSync();
});
});
@ -294,6 +300,7 @@ 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)));
@ -320,6 +327,7 @@ public final class OpenFileSystemModel {
return;
}
startIfNeeded();
var abs = FileNames.join(getCurrentDirectory().getPath(), linkName);
fileSystem.symbolicLink(abs, targetFile);
refreshSync();
@ -375,9 +383,6 @@ public final class OpenFileSystemModel {
}
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 +413,11 @@ 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);
}
try {
var name = (directory != null ? directory + " - " : "") + entry.get().getName();
TerminalHelper.open(entry.getEntry(), name, connection);
TerminalLauncher.open(entry.getEntry(), name, directory, 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);
}
startIfNeeded();
}
});
});

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,10 +24,11 @@ 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);

View file

@ -0,0 +1,89 @@
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.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);
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;
}
@Override
public CompStructure<Region> createBase() {
var entryR = content().createRegion();
entryR.getStyleClass().add("dialog-content");
var sp = new ScrollPane(entryR);
sp.setFitToWidth(true);
entryR.minHeightProperty().bind(sp.heightProperty());
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();
public Comp<?> bottom() {
return null;
}
}

View file

@ -1,69 +0,0 @@
package io.xpipe.app.comp.base;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppResources;
import io.xpipe.app.ext.DownloadModuleInstall;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.LabelComp;
import io.xpipe.app.util.Hyperlinks;
import io.xpipe.app.util.OptionsBuilder;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.TextArea;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import lombok.EqualsAndHashCode;
import lombok.Value;
import java.nio.file.Files;
@Value
@EqualsAndHashCode(callSuper = true)
public class InstallExtensionComp extends SimpleComp {
DownloadModuleInstall install;
@Override
protected Region createSimple() {
var builder = new OptionsBuilder();
builder.addTitle("installRequired");
var header = new LabelComp(AppI18n.observable("extensionInstallDescription"))
.apply(struc -> struc.get().setWrapText(true));
builder.addComp(header);
if (install.getVendorURL() != null) {
var vendorLink = Comp.of(() -> {
var hl = new Hyperlink(install.getVendorURL());
hl.setOnAction(e -> Hyperlinks.open(install.getVendorURL()));
return hl;
});
builder.addComp(vendorLink);
}
if (install.getLicenseFile() != null) {
builder.addTitle("license");
var changeNotice = new LabelComp(AppI18n.observable("extensionInstallLicenseNote"))
.apply(struc -> struc.get().setWrapText(true));
builder.addComp(changeNotice);
var license = Comp.of(() -> {
var text = new TextArea();
text.setEditable(false);
AppResources.with(install.getModule(), install.getLicenseFile(), file -> {
var s = Files.readString(file);
text.setText(s);
});
text.setWrapText(true);
VBox.setVgrow(text, Priority.ALWAYS);
AppFont.verySmall(text);
return text;
});
builder.addComp(license);
}
return builder.build();
}
}

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)) {

View file

@ -16,6 +16,9 @@ import javafx.scene.layout.StackPane;
public class LoadingOverlayComp extends Comp<CompStructure<StackPane>> {
private static final double FPS = 30.0;
private static final double cycleDurationSeconds = 4.0;
public static LoadingOverlayComp noProgress(Comp<?> comp, ObservableValue<Boolean> loading) {
return new LoadingOverlayComp(comp, loading, new SimpleDoubleProperty(-1));
}
@ -39,6 +42,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

@ -1,285 +0,0 @@
package io.xpipe.app.comp.base;
import atlantafx.base.controls.Spacer;
import atlantafx.base.theme.Styles;
import com.jfoenix.controls.JFXTabPane;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.beans.property.ReadOnlyProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.css.PseudoClass;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.Tab;
import javafx.scene.layout.*;
import lombok.Getter;
import java.util.List;
public abstract class MultiStepComp extends Comp<CompStructure<VBox>> {
private static final PseudoClass COMPLETED = PseudoClass.getPseudoClass("completed");
private static final PseudoClass CURRENT = PseudoClass.getPseudoClass("current");
private static final PseudoClass NEXT = PseudoClass.getPseudoClass("next");
private final Property<Boolean> completed = new SimpleBooleanProperty();
private final Property<Step<?>> currentStep = new SimpleObjectProperty<>();
@Getter
private List<Entry> entries;
@Getter
private int currentIndex = 0;
private Step<?> getValue() {
return currentStep.getValue();
}
private void set(Step<?> step) {
currentStep.setValue(step);
}
public void next() {
PlatformThread.runLaterIfNeeded(() -> {
if (isFinished()) {
return;
}
if (!getValue().canContinue()) {
return;
}
if (isLastPage()) {
getValue().onContinue();
finish();
currentIndex++;
completed.setValue(true);
return;
}
int index = Math.min(getCurrentIndex() + 1, entries.size() - 1);
if (currentIndex == index) {
return;
}
getValue().onContinue();
entries.get(index).step().onInit();
currentIndex = index;
set(entries.get(index).step());
});
}
public void previous() {
PlatformThread.runLaterIfNeeded(() -> {
int index = Math.max(currentIndex - 1, 0);
if (currentIndex == index) {
return;
}
getValue().onBack();
currentIndex = index;
set(entries.get(index).step());
});
}
public boolean isCompleted(Entry e) {
return entries.indexOf(e) < currentIndex;
}
public boolean isNext(Entry e) {
return entries.indexOf(e) > currentIndex;
}
public boolean isCurrent(Entry e) {
return entries.indexOf(e) == currentIndex;
}
public boolean isFirstPage() {
return currentIndex == 0;
}
public boolean isLastPage() {
return currentIndex == entries.size() - 1;
}
public boolean isFinished() {
return currentIndex == entries.size();
}
protected Region createStepOverview(Region content) {
if (entries.size() == 1) {
return new Region();
}
HBox box = new HBox();
box.setFillHeight(true);
box.getStyleClass().add("top");
box.setAlignment(Pos.CENTER);
var comp = this;
int number = 1;
for (var entry : comp.getEntries()) {
VBox element = new VBox();
element.setFillWidth(true);
element.setAlignment(Pos.CENTER);
var label = new Label();
label.textProperty().bind(entry.name);
label.getStyleClass().add("name");
element.getChildren().add(label);
element.getStyleClass().add("entry");
var line = new Region();
boolean first = number == 1;
boolean last = number == comp.getEntries().size();
line.prefWidthProperty()
.bind(Bindings.createDoubleBinding(
() -> element.getWidth() / ((first || last) ? 2 : 1), element.widthProperty()));
line.setMinWidth(0);
line.getStyleClass().add("line");
var lineBox = new HBox(line);
lineBox.setFillHeight(true);
if (first) {
lineBox.setAlignment(Pos.CENTER_RIGHT);
} else if (last) {
lineBox.setAlignment(Pos.CENTER_LEFT);
} else {
lineBox.setAlignment(Pos.CENTER);
}
var circle = new Region();
circle.getStyleClass().add("circle");
var numberLabel = new Label("" + number);
numberLabel.getStyleClass().add("number");
var stack = new StackPane();
stack.getChildren().add(lineBox);
stack.getChildren().add(circle);
stack.getChildren().add(numberLabel);
stack.setAlignment(Pos.CENTER);
element.getChildren().add(stack);
Runnable updatePseudoClasses = () -> {
element.pseudoClassStateChanged(CURRENT, comp.isCurrent(entry));
element.pseudoClassStateChanged(NEXT, comp.isNext(entry));
element.pseudoClassStateChanged(COMPLETED, comp.isCompleted(entry));
};
updatePseudoClasses.run();
comp.currentStep.addListener((c, o, n) -> {
updatePseudoClasses.run();
});
box.getChildren().add(element);
element.prefWidthProperty()
.bind(Bindings.createDoubleBinding(
() -> content.getWidth() / comp.getEntries().size(), content.widthProperty()));
number++;
}
return box;
}
protected Region createStepNavigation() {
MultiStepComp comp = this;
HBox buttons = new HBox();
buttons.setFillHeight(true);
buttons.getChildren().add(new Region());
buttons.getChildren().add(new Spacer());
buttons.getStyleClass().add("buttons");
buttons.setSpacing(5);
SimpleChangeListener.apply(currentStep, val -> {
buttons.getChildren().set(0, val.bottom() != null ? val.bottom().createRegion() : new Region());
});
buttons.setAlignment(Pos.CENTER_RIGHT);
var nextText = Bindings.createStringBinding(
() -> isLastPage() ? AppI18n.get("finishStep") : AppI18n.get("nextStep"), currentStep);
var nextButton = new ButtonComp(nextText, null, comp::next)
.styleClass(Styles.ACCENT)
.styleClass("next");
var previousButton = new ButtonComp(AppI18n.observable("previousStep"), null, comp::previous)
.styleClass("next")
.apply(struc -> struc.get()
.disableProperty()
.bind(Bindings.createBooleanBinding(this::isFirstPage, currentStep)));
previousButton.apply(
s -> s.get().visibleProperty().bind(Bindings.createBooleanBinding(() -> !isFirstPage(), currentStep)));
buttons.getChildren().add(previousButton.createRegion());
buttons.getChildren().add(nextButton.createRegion());
return buttons;
}
@Override
public CompStructure<VBox> createBase() {
this.entries = setup();
this.set(entries.get(currentIndex).step);
VBox content = new VBox();
var comp = this;
Region box = createStepOverview(content);
var compContent = new JFXTabPane();
compContent.getStyleClass().add("content");
for (var ignored : comp.getEntries()) {
compContent.getTabs().add(new Tab(null, null));
}
var entryR = comp.getValue().createRegion();
entryR.getStyleClass().add("step");
compContent.getTabs().set(currentIndex, new Tab(null, entryR));
compContent.getSelectionModel().select(currentIndex);
content.getChildren().addAll(box, compContent, createStepNavigation());
content.getStyleClass().add("multi-step-comp");
content.setFillWidth(true);
VBox.setVgrow(compContent, Priority.ALWAYS);
currentStep.addListener((c, o, n) -> {
var nextTab = compContent
.getTabs()
.get(entries.stream().map(e -> e.step).toList().indexOf(n));
if (nextTab.getContent() == null) {
var createdRegion = n.createRegion();
createdRegion.getStyleClass().add("step");
nextTab.setContent(createdRegion);
}
compContent.getSelectionModel().select(comp.getCurrentIndex());
});
return new SimpleCompStructure<>(content);
}
protected abstract List<Entry> setup();
protected abstract void finish();
public ReadOnlyProperty<Boolean> completedProperty() {
return completed;
}
public abstract static class Step<S extends CompStructure<?>> extends Comp<S> {
public Comp<?> bottom() {
return null;
}
public void onInit() {}
public void onBack() {}
public void onContinue() {}
public boolean canContinue() {
return true;
}
}
public record Entry(ObservableValue<String> name, Step<?> step) {}
}

View file

@ -57,6 +57,7 @@ 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 String getImage(String name) {
if (name == null) {
@ -66,7 +67,7 @@ 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))
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);

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,23 @@ 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,11 +68,32 @@ 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",
@ -65,7 +104,7 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
}
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 +113,16 @@ 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);
});

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

@ -3,16 +3,16 @@ 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.BooleanProperty;
import javafx.beans.property.Property;
import javafx.beans.value.ObservableValue;
import javafx.scene.layout.Region;
public class NamedToggleComp extends SimpleComp {
public class ToggleSwitchComp extends SimpleComp {
private final BooleanProperty selected;
private final Property<Boolean> selected;
private final ObservableValue<String> name;
public NamedToggleComp(BooleanProperty selected, ObservableValue<String> name) {
public ToggleSwitchComp(Property<Boolean> selected, ObservableValue<String> name) {
this.selected = selected;
this.name = name;
}
@ -22,7 +22,7 @@ public class NamedToggleComp extends SimpleComp {
var s = new ToggleSwitch();
s.setSelected(selected.getValue());
s.selectedProperty().addListener((observable, oldValue, newValue) -> {
selected.set(newValue);
selected.setValue(newValue);
});
selected.addListener((observable, oldValue, newValue) -> {
PlatformThread.runLaterIfNeeded(() -> {

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,18 @@ 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));
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 {
information.textProperty().bind(PlatformThread.sync(info));
return info.getValue();
}
}, grid.hoverProperty(), info, summary)));
}
});
return information;
}

View file

@ -1,13 +1,13 @@
package io.xpipe.app.comp.store;
import atlantafx.base.controls.Spacer;
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.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 +25,14 @@ import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.scene.control.Alert;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Separator;
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;
@ -58,14 +61,15 @@ public class StoreCreationComp extends MultiStepComp.Step<CompStructure<?>> {
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;
@ -178,36 +182,13 @@ 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() {
private final StoreCreationComp creator = new StoreCreationComp(
this, prop, store, filter, initialName, existingEntry, staticDisplay);
@Override
protected List<Entry> setup() {
loading.bind(creator.busy);
return List.of(new Entry(AppI18n.observable("a"), creator));
DialogComp.showWindow("addConnection", stage -> new StoreCreationComp(
stage, con, prop, store, filter, initialName, existingEntry, staticDisplay));
}
@Override
protected void finish() {
window.close();
if (creator.entry.getValue() != null) {
con.accept(creator.entry.getValue());
}
}
};
},
false,
loading);
stage.show();
});
protected ObservableValue<Boolean> busy() {
return busy;
}
@Override
@ -259,18 +240,97 @@ public class StoreCreationComp extends MultiStepComp.Step<CompStructure<?>> {
.build();
}
private void commit() {
if (finished.get()) {
return;
}
finished.setValue(true);
if (entry.getValue() != null) {
consumer.accept(entry.getValue());
}
PlatformThread.runLaterIfNeeded(() -> {
window.close();
});
}
@Override
public CompStructure<? extends Region> createBase() {
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 (messageProp.getValue() != null && !changedSinceError.get()) {
if (AppPrefs.get().developerMode().getValue() && showInvalidConfirmAlert()) {
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) {
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());
}
});
}
@Override
public Comp<?> content() {
var back = Comp.of(this::createLayout);
var message = new ErrorOverlayComp(back, messageProp);
return message.createStructure();
return message;
}
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 +357,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

@ -42,7 +42,8 @@ public abstract class StoreEntryComp extends SimpleComp {
public static StoreEntryComp create(
StoreEntryWrapper entry, Comp<?> content, boolean preferLarge) {
if (!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);
@ -54,7 +55,10 @@ public abstract class StoreEntryComp extends SimpleComp {
if (prov != null) {
return prov.customEntryComp(e, topLevel);
} else {
return new StandardStoreEntryComp(e.getWrapper(), null);
var forceCondensed = AppPrefs.get() != null && AppPrefs.get().condenseConnectionDisplay().get();
return forceCondensed ?
new DenseStoreEntryComp(e.getWrapper(), true, null) :
new StandardStoreEntryComp(e.getWrapper(), 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,22 @@ 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 text = new VBox(title, introDesc);
text.setSpacing(5);
text.setAlignment(Pos.CENTER_LEFT);
var hbox = new HBox(img, text);
hbox.setSpacing(35);

View file

@ -5,7 +5,6 @@ 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.prefs.AppPrefs;
import io.xpipe.app.util.JfxHelper;
import javafx.beans.property.Property;
import javafx.scene.control.ComboBox;
@ -22,7 +21,7 @@ import java.util.function.Supplier;
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
@AllArgsConstructor
public class DataStoreProviderChoiceComp extends Comp<CompStructure<ComboBox<DataStoreProvider>>> {
public class StoreProviderChoiceComp extends Comp<CompStructure<ComboBox<DataStoreProvider>>> {
Predicate<DataStoreProvider> filter;
Property<DataStoreProvider> provider;
@ -58,7 +57,7 @@ public class DataStoreProviderChoiceComp extends Comp<CompStructure<ComboBox<Dat
});
cb.setButtonCell(cellFactory.get());
var l = getProviders().stream()
.filter(p -> AppPrefs.get().developerShowHiddenProviders().get() || p.getCreationCategory() != null || staticDisplay)
.filter(p -> p.getCreationCategory() != null || staticDisplay)
.toList();
l.forEach(dataStoreProvider -> cb.getItems().add(dataStoreProvider));
if (provider.getValue() == null) {

View file

@ -70,7 +70,7 @@ public class StoreSection {
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);
}

View file

@ -38,7 +38,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(

View file

@ -11,7 +11,14 @@ import java.util.stream.Stream;
public interface StoreSortMode {
StoreSection representative(StoreSection s);
StoreSortMode ALPHABETICAL_DESC = new StoreSortMode() {
@Override
public StoreSection representative(StoreSection s) {
return s;
}
@Override
public String getId() {
return "alphabetical-desc";
@ -25,6 +32,11 @@ public interface StoreSortMode {
};
StoreSortMode ALPHABETICAL_ASC = new StoreSortMode() {
@Override
public StoreSection representative(StoreSection s) {
return s;
}
@Override
public String getId() {
return "alphabetical-asc";
@ -39,6 +51,14 @@ public interface StoreSortMode {
};
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";
@ -56,6 +76,14 @@ 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";

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;
@ -70,7 +71,7 @@ public class StoreViewState {
private StoreViewState() {
initContent();
addStorageListeners();
addListeners();
}
private void updateContent() {
@ -112,7 +113,20 @@ 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

View file

@ -1,36 +0,0 @@
package io.xpipe.app.core;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.core.charsetter.Charsetter;
import io.xpipe.core.charsetter.CharsetterContext;
import io.xpipe.core.charsetter.StreamCharset;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
public class AppCharsets {
private static final List<String> observedCharsets = new ArrayList<>();
public static void init() {
var system = System.getProperty("file.encoding");
var systemLocale = Locale.getDefault();
var appLocale = AppPrefs.get().language.getValue().getLocale();
var used = AppCache.get("observedCharsets", List.class, () -> new ArrayList<String>());
var ctx = new CharsetterContext(system, systemLocale, appLocale, used);
Charsetter.init(ctx);
}
public static void observe(StreamCharset c) {
if (c == null) {
return;
}
var used = AppCache.get("observedCharsets", List.class, () -> new ArrayList<String>());
used.add(c.getCharset().name());
AppCache.update("observedCharsets", used);
init();
}
}

View file

@ -1,48 +0,0 @@
package io.xpipe.app.core;
import io.xpipe.core.charsetter.Charsetter;
import io.xpipe.core.charsetter.StreamCharset;
import io.xpipe.core.util.FailableConsumer;
import io.xpipe.core.util.FailableSupplier;
import org.apache.commons.io.ByteOrderMark;
import org.apache.commons.io.input.BOMInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
public class AppCharsetter extends Charsetter {
private static final int MAX_BYTES = 8192;
public static void init() {
Charsetter.INSTANCE = new AppCharsetter();
}
public Result read(FailableSupplier<InputStream> in, FailableConsumer<InputStreamReader, Exception> con)
throws Exception {
checkInit();
try (var is = in.get();
var bin = new BOMInputStream(is)) {
ByteOrderMark bom = bin.getBOM();
String charsetName = bom == null ? null : bom.getCharsetName();
var charset = charsetName != null
? StreamCharset.get(Charset.forName(charsetName), bom.getCharsetName() != null)
: null;
bin.mark(MAX_BYTES);
var bytes = bin.readNBytes(MAX_BYTES);
bin.reset();
if (charset == null) {
charset = StreamCharset.get(inferCharset(bytes), false);
}
var nl = inferNewLine(bytes);
if (con != null) {
con.accept(new InputStreamReader(bin, charset.getCharset()));
}
return new Result(charset, nl);
}
}
}

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;
@ -110,36 +109,11 @@ 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) {
@ -154,43 +128,12 @@ public class AppExtensionManager {
}
}
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();
}
}
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 +149,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

@ -114,7 +114,7 @@ public class AppFileWatcher {
this.baseDir = dir;
this.listener = listener;
createRecursiveWatchers(dir);
TrackEvent.withTrace("watcher", "Added watched directory")
TrackEvent.withTrace("Added watched directory")
.tag("location", dir)
.handle();
}
@ -177,7 +177,7 @@ 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())

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

@ -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();
});
@ -210,7 +210,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();
@ -311,7 +311,7 @@ public class AppI18n {
this.prettyTime = new PrettyTime(
AppPrefs.get() != null
? AppPrefs.get().language.getValue().getLocale()
? AppPrefs.get().language().getValue().getLocale()
: SupportedLocale.ENGLISH.getLocale());
}
}

View file

@ -5,7 +5,7 @@ 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.prefs.AppPrefsComp;
import io.xpipe.app.util.LicenseProvider;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
@ -79,7 +79,7 @@ public class AppLayoutModel {
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))));
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())

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,14 +31,14 @@ 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());
@ -60,16 +59,14 @@ 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 +93,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 +128,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() {
@ -150,44 +172,9 @@ public class AppLogs {
}
private void close() {
outStream.close();
categoryWriters.forEach((k, s) -> {
s.close();
});
if (outFileStream != null) {
outFileStream.close();
}
private String getCategory(TrackEvent event) {
if (event.getCategory() != null) {
return event.getCategory();
}
return "misc";
}
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 boolean shouldDebugPlatform() {
@ -212,7 +199,6 @@ public class AppLogs {
TrackEvent.builder()
.type("info")
.category("sysout")
.message(line)
.build()
.handle();
@ -248,7 +234,7 @@ 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 LOG_LEVELS.contains(p) ? p : "trace";
}
return DEFAULT_LOG_LEVEL;
@ -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;
}
@ -390,7 +368,6 @@ public class AppLogs {
}
}
TrackEvent.builder()
.category(name)
.type(level.toString().toLowerCase())
.message(msg)
.build()
@ -399,62 +376,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

@ -101,6 +101,11 @@ public class AppProperties {
return INSTANCE;
}
public boolean isDevelopmentEnvironment() {
return !AppProperties.get().isImage()
&& AppProperties.get().isDeveloperMode();
}
public boolean isDeveloperMode() {
if (AppPrefs.get() == null) {
return false;

View file

@ -112,7 +112,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 +121,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 +145,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 +170,6 @@ public class AppSocketServer {
}
TrackEvent.builder()
.category("beacon")
.type("trace")
.message("Socket connection #" + id + " performed exchange "
+ req.getClass().getSimpleName())
@ -187,7 +186,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 +196,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 +209,28 @@ 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);
ErrorEvent.fromThrowable(se).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,14 +240,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");
}
@ -296,7 +292,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 +332,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

@ -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,7 +48,7 @@ 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) {
@ -93,7 +93,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,21 @@ 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()) {
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()) {
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 +103,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 +182,8 @@ public class AppTheme {
}
@Override
public String toTranslatedString() {
return name;
public ObservableValue<String> toTranslatedString() {
return new SimpleStringProperty(name);
}
}
@ -244,8 +237,8 @@ public class AppTheme {
}
@Override
public String toTranslatedString() {
return theme.getName();
public ObservableValue<String> toTranslatedString() {
return new SimpleStringProperty(theme.getName());
}
@Override

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?
@ -110,6 +119,24 @@ 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(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();

View file

@ -0,0 +1,35 @@
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") + "\\Windows32\\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;
@ -24,7 +24,7 @@ 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

@ -4,16 +4,14 @@ 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.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.*;
import io.xpipe.core.util.JacksonMapper;
public class BaseMode extends OperationMode {
@ -39,30 +37,33 @@ 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.init();
AppCertutilCheck.check();
AppAvCheck.check();
LocalShell.init();
AppShellCheck.check();
XPipeDistributionType.init();
AppPrefs.init();
AppCharsets.init();
AppCharsetter.init();
AppShellCheck.check();
AppPrefs.setDefaults();
// Initialize socket server before storage
// as we should be prepared for git askpass commands
AppSocketServer.init();
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;
var sec = new VaultKeySecretValue(new char[] {1, 2, 3});
sec.getSecret();
}
@Override
@ -70,7 +71,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 +81,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");
}
}

View file

@ -7,7 +7,6 @@ import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.update.UpdateChangelogAlert;
import io.xpipe.app.util.UnlockAlert;
import javafx.stage.Stage;
public class GuiMode extends PlatformMode {
@ -21,10 +20,9 @@ public class GuiMode extends PlatformMode {
public void onSwitchTo() throws Throwable {
super.onSwitchTo();
UnlockAlert.showIfNeeded();
AppGreetings.showIfNeeded();
TrackEvent.info("mode", "Waiting for window setup completion ...");
TrackEvent.info("Waiting for window setup completion ...");
PlatformThread.runLaterIfNeededBlocking(() -> {
if (AppMainWindow.getInstance() == null) {
try {
@ -35,7 +33,7 @@ public class GuiMode extends PlatformMode {
}
AppMainWindow.getInstance().show();
});
TrackEvent.info("mode", "Window setup complete");
TrackEvent.info("Window setup complete");
UpdateChangelogAlert.showIfNeeded();
}
@ -43,7 +41,7 @@ public class GuiMode extends PlatformMode {
@Override
public void onSwitchFrom() {
PlatformThread.runLaterIfNeededBlocking(() -> {
TrackEvent.info("mode", "Closing windows");
TrackEvent.info("Closing windows");
Stage.getWindows().stream().toList().forEach(w -> {
w.hide();
});

View file

@ -13,7 +13,6 @@ import io.xpipe.app.util.XPipeSession;
import io.xpipe.core.util.FailableRunnable;
import io.xpipe.core.util.XPipeDaemonMode;
import io.xpipe.core.util.XPipeInstallation;
import io.xpipe.core.util.XPipeSystemId;
import javafx.application.Platform;
import lombok.Getter;
@ -93,7 +92,7 @@ public abstract class OperationMode {
// throw new OutOfMemoryError();
// }
TrackEvent.info("mode", "Initial setup");
TrackEvent.info("Initial setup");
AppProperties.init();
AppState.init();
XPipeSession.init(AppProperties.get().getBuildUuid());
@ -104,8 +103,7 @@ public abstract class OperationMode {
AppProperties.logArguments(args);
AppProperties.logSystemProperties();
AppProperties.logPassedProperties();
XPipeSystemId.init();
TrackEvent.info("mode", "Finished initial setup");
TrackEvent.info("Finished initial setup");
} catch (Throwable ex) {
ErrorEvent.fromThrowable(ex).term().handle();
}
@ -211,11 +209,6 @@ public abstract class OperationMode {
OperationMode.halt(1);
}
// In case we perform any operations such as opening a terminal
// give it some time to open while this process is still alive
// Otherwise it might quit because the parent process is dead already
ThreadHelper.sleep(1000);
OperationMode.halt(0);
};
@ -250,7 +243,7 @@ public abstract class OperationMode {
}
// Run a timer to always exit after some time in case we get stuck
if (!hasError) {
if (!hasError && !AppProperties.get().isDevelopmentEnvironment()) {
ThreadHelper.runAsync(() -> {
ThreadHelper.sleep(25000);
TrackEvent.info("Shutdown took too long. Halting ...");

View file

@ -23,27 +23,27 @@ public abstract class PlatformMode extends OperationMode {
return;
}
TrackEvent.info("mode", "Platform mode initial setup");
TrackEvent.info("Platform mode initial setup");
PlatformState.initPlatformOrThrow();
AppFont.init();
AppTheme.init();
AppStyle.init();
AppImages.init();
AppLayoutModel.init();
TrackEvent.info("mode", "Finished essential component initialization before platform");
TrackEvent.info("Finished essential component initialization before platform");
TrackEvent.info("mode", "Launching application ...");
TrackEvent.info("Launching application ...");
ThreadHelper.createPlatformThread("app", false, () -> {
TrackEvent.info("mode", "Application thread started");
TrackEvent.info("Application thread started");
Application.launch(App.class);
})
.start();
TrackEvent.info("mode", "Waiting for platform application startup ...");
TrackEvent.info("Waiting for platform application startup ...");
while (App.getApp() == null) {
ThreadHelper.sleep(100);
}
TrackEvent.info("mode", "Application startup finished ...");
TrackEvent.info("Application startup finished ...");
// If we downloaded an update, and decided to no longer automatically update, don't remind us!
// You can still update manually in the about tab
@ -56,12 +56,12 @@ public abstract class PlatformMode extends OperationMode {
@Override
public void finalTeardown() throws Throwable {
TrackEvent.info("mode", "Shutting down platform components");
TrackEvent.info("Shutting down platform components");
onSwitchFrom();
StoreViewState.reset();
AppLayoutModel.reset();
PlatformState.teardown();
TrackEvent.info("mode", "Platform shutdown finished");
TrackEvent.info("Platform shutdown finished");
BACKGROUND.finalTeardown();
}
}

View file

@ -23,19 +23,19 @@ public class TrayMode extends PlatformMode {
super.onSwitchTo();
PlatformThread.runLaterIfNeededBlocking(() -> {
if (AppTray.get() == null) {
TrackEvent.info("mode", "Initializing tray");
TrackEvent.info("Initializing tray");
AppTray.init();
}
AppTray.get().show();
TrackEvent.info("mode", "Finished tray initialization");
TrackEvent.info("Finished tray initialization");
});
}
@Override
public void onSwitchFrom() {
if (AppTray.get() != null) {
TrackEvent.info("mode", "Closing tray");
TrackEvent.info("Closing tray");
PlatformThread.runLaterIfNeededBlocking(() -> AppTray.get().hide());
}
}

View file

@ -2,7 +2,7 @@ package io.xpipe.app.exchange;
import io.xpipe.app.core.AppStyle;
import io.xpipe.app.core.AppTheme;
import io.xpipe.app.util.AskpassAlert;
import io.xpipe.app.util.SecretManager;
import io.xpipe.beacon.BeaconHandler;
import io.xpipe.beacon.exchange.AskpassExchange;
@ -11,9 +11,16 @@ public class AskpassExchangeImpl extends AskpassExchange
@Override
public Response handleRequest(BeaconHandler handler, Request msg) {
var found = msg.getSecretId() != null ? SecretManager.getProgress(msg.getRequest(), msg.getSecretId()) : SecretManager.getProgress(msg.getRequest());
if (found.isEmpty()) {
return Response.builder().build();
}
AppStyle.init();
AppTheme.init();
var r = AskpassAlert.query(msg.getPrompt(), msg.getRequest(), msg.getStoreId(), msg.getSubId());
return Response.builder().value(r != null ? r.inPlace() : null).build();
var p = found.get();
var secret = p.process(msg.getPrompt());
return Response.builder().value(secret != null ? secret.inPlace() : null).build();
}
}

View file

@ -31,7 +31,7 @@ public class DialogExchangeImpl extends DialogExchange
@Override
public DialogExchange.Response handleRequest(BeaconHandler handler, Request msg) throws Exception {
if (msg.isCancel()) {
TrackEvent.withTrace("beacon", "Received cancel dialog request")
TrackEvent.withTrace("Received cancel dialog request")
.tag("key", msg.getDialogKey())
.handle();
openDialogs.remove(msg.getDialogKey());
@ -42,7 +42,7 @@ public class DialogExchangeImpl extends DialogExchange
var dialog = openDialogs.get(msg.getDialogKey());
var e = dialog.receive(msg.getValue());
TrackEvent.withTrace("beacon", "Received normal dialog request")
TrackEvent.withTrace("Received normal dialog request")
.tag("key", msg.getDialogKey())
.tag("value", msg.getValue())
.tag("newElement", e)

View file

@ -16,7 +16,9 @@ public class LaunchExchangeImpl extends LaunchExchange
public Response handleRequest(BeaconHandler handler, Request msg) throws Exception {
var store = getStoreEntryById(msg.getId(), false);
if (store.getStore() instanceof LaunchableStore s) {
var command = s.prepareLaunchCommand().prepareTerminalOpen(TerminalInitScriptConfig.ofName(store.getName()));
var command = s.prepareLaunchCommand().prepareTerminalOpen(
TerminalInitScriptConfig.ofName(store.getName()),
null);
return Response.builder().command(split(command)).build();
}

View file

@ -0,0 +1,17 @@
package io.xpipe.app.exchange;
import io.xpipe.app.util.TerminalLauncherManager;
import io.xpipe.beacon.BeaconHandler;
import io.xpipe.beacon.ClientException;
import io.xpipe.beacon.ServerException;
import io.xpipe.beacon.exchange.TerminalLaunchExchange;
public class TerminalLaunchExchangeImpl extends TerminalLaunchExchange
implements MessageExchangeImpl<TerminalLaunchExchange.Request, TerminalLaunchExchange.Response> {
@Override
public Response handleRequest(BeaconHandler handler, Request msg) throws ServerException, ClientException {
var r = TerminalLauncherManager.performLaunch(msg.getRequest());
return Response.builder().targetFile(r).build();
}
}

View file

@ -0,0 +1,17 @@
package io.xpipe.app.exchange;
import io.xpipe.app.util.TerminalLauncherManager;
import io.xpipe.beacon.BeaconHandler;
import io.xpipe.beacon.ClientException;
import io.xpipe.beacon.ServerException;
import io.xpipe.beacon.exchange.TerminalWaitExchange;
public class TerminalWaitExchangeImpl extends TerminalWaitExchange
implements MessageExchangeImpl<TerminalWaitExchange.Request, TerminalWaitExchange.Response> {
@Override
public Response handleRequest(BeaconHandler handler, Request msg) throws ServerException, ClientException {
TerminalLauncherManager.waitForCompletion(msg.getRequest());
return Response.builder().build();
}
}

View file

@ -2,6 +2,7 @@ package io.xpipe.app.exchange.cli;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.exchange.MessageExchangeImpl;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.beacon.BeaconHandler;
import io.xpipe.beacon.ClientException;
import io.xpipe.beacon.exchange.cli.ModeExchange;
@ -11,8 +12,9 @@ public class ModeExchangeImpl extends ModeExchange
@Override
public Response handleRequest(BeaconHandler handler, Request msg) throws Exception {
if (OperationMode.get() == null) {
throw new ClientException("Mode switch already in progress");
// Wait for startup
while (OperationMode.get() == null) {
ThreadHelper.sleep(100);
}
var mode = OperationMode.map(msg.getMode());

View file

@ -20,7 +20,7 @@ public class SinkExchangeImpl extends SinkExchange
ShellStore store = ds.getStore().asNeeded();
try (var fs = store.createFileSystem();
var inputStream = handler.receiveBody();
var output = fs.openOutput(msg.getPath())) {
var output = fs.openOutput(msg.getPath(), -1)) {
inputStream.transferTo(output);
}

View file

@ -35,10 +35,6 @@ public interface DataStoreProvider {
return false;
}
default ModuleInstall getRequiredAdditionalInstallation() {
return null;
}
default void validate() {
for (Class<?> storeClass : getStoreClasses()) {
if (!JacksonizedValue.class.isAssignableFrom(storeClass)) {

View file

@ -1,31 +0,0 @@
package io.xpipe.app.ext;
import lombok.Getter;
import java.util.List;
public abstract class DownloadModuleInstall extends ModuleInstall {
private final String licenseFile;
private final String vendorURL;
@Getter
private final List<String> assets;
public DownloadModuleInstall(String id, String module, String licenseFile, String vendorURL, List<String> assets) {
super(id, module);
this.licenseFile = licenseFile;
this.vendorURL = vendorURL;
this.assets = assets;
}
@Override
public String getLicenseFile() {
return licenseFile;
}
@Override
public String getVendorURL() {
return vendorURL;
}
}

View file

@ -1,24 +0,0 @@
package io.xpipe.app.ext;
import lombok.Getter;
import java.nio.file.Path;
@Getter
public abstract class ModuleInstall {
private final String id;
private final String module;
protected ModuleInstall(String id, String module) {
this.id = id;
this.module = module;
}
public abstract String getLicenseFile();
public abstract String getVendorURL();
public abstract void installInternal(Path directory) throws Exception;
}

View file

@ -2,6 +2,7 @@ package io.xpipe.app.ext;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.util.Translatable;
import javafx.beans.value.ObservableValue;
import lombok.SneakyThrows;
import java.util.Arrays;
@ -54,8 +55,8 @@ public interface PrefsChoiceValue extends Translatable {
}
@Override
default String toTranslatedString() {
return AppI18n.get(getId());
default ObservableValue<String> toTranslatedString() {
return AppI18n.observable(getId());
}
String getId();

View file

@ -1,10 +1,9 @@
package io.xpipe.app.ext;
import com.dlsc.preferencesfx.model.Setting;
import java.util.List;
import io.xpipe.app.fxcomps.Comp;
import javafx.beans.property.Property;
public interface PrefsHandler {
void addSetting(List<String> category, String group, Setting<?, ?> setting, Class<?> c);
<T> void addSetting(String id, Class<T> c, Property<T> property, Comp<?> comp);
}

View file

@ -1,8 +1,6 @@
package io.xpipe.app.ext;
import com.dlsc.formsfx.model.structure.Field;
import io.xpipe.core.util.ModuleLayerLoader;
import javafx.beans.value.ObservableBooleanValue;
import java.util.List;
import java.util.ServiceLoader;
@ -44,12 +42,7 @@ public abstract class PrefsProvider {
.orElseThrow();
}
protected <T extends Field<?>> T editable(T o, ObservableBooleanValue v) {
o.editableProperty().bind(v);
return o;
}
public abstract void addPrefs(PrefsHandler handler);
public abstract void init();
public abstract void initDefaultValues();
}

View file

@ -7,6 +7,7 @@ import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.util.Translatable;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
@ -16,11 +17,19 @@ import javafx.util.StringConverter;
import lombok.AccessLevel;
import lombok.experimental.FieldDefaults;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
public class ChoiceComp<T> extends Comp<CompStructure<ComboBox<T>>> {
public static <T extends Translatable> ChoiceComp<T> ofTranslatable(Property<T> value, List<T> range, boolean includeNone) {
var map = range.stream().collect(Collectors.toMap(o -> o, Translatable::toTranslatedString, (v1, v2) -> v2, LinkedHashMap::new));
return new ChoiceComp<>(value, map,includeNone);
}
Property<T> value;
ObservableValue<Map<T, ObservableValue<String>>> range;
boolean includeNone;

View file

@ -9,10 +9,10 @@ import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextArea;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import org.fxmisc.richtext.InlineCssTextArea;
import org.kordamp.ikonli.javafx.FontIcon;
public class CodeSnippetComp extends Comp<CompStructure<?>> {
@ -36,7 +36,7 @@ public class CodeSnippetComp extends Comp<CompStructure<?>> {
(int) (color.getRed() * 255), (int) (color.getGreen() * 255), (int) (color.getBlue() * 255));
}
private void fillArea(VBox lineNumbers, InlineCssTextArea s) {
private void fillArea(VBox lineNumbers, TextArea s) {
lineNumbers.getChildren().clear();
s.clear();
@ -47,8 +47,7 @@ public class CodeSnippetComp extends Comp<CompStructure<?>> {
lineNumbers.getChildren().add(numberLabel);
for (var el : line.elements()) {
String hex = toRGBCode(el.color());
s.append(el.text(), "-fx-fill: " + hex + ";");
s.appendText(el.text());
}
boolean last = number == value.getValue().lines().size();
@ -74,7 +73,7 @@ public class CodeSnippetComp extends Comp<CompStructure<?>> {
@Override
public CompStructure<?> createBase() {
var s = new InlineCssTextArea();
var s = new javafx.scene.control.TextArea();
s.setEditable(false);
s.setBackground(null);
s.getStyleClass().add("code-snippet");

View file

@ -12,6 +12,7 @@ import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.DataStoreCategoryChoiceComp;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.LocalStore;
import io.xpipe.core.store.ShellStore;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
@ -154,9 +155,7 @@ public class DataStoreChoiceComp<T extends DataStore> extends SimpleComp {
return null;
}
if (mode == Mode.PROXY
&& entry.getStore() instanceof ShellStore
&& ShellStore.isLocal(entry.getStore().asNeeded())) {
if (mode == Mode.PROXY && entry.getStore() instanceof LocalStore) {
return AppI18n.get("none");
}

View file

@ -1,27 +1,15 @@
package io.xpipe.app.fxcomps.impl;
import com.jfoenix.controls.JFXTooltip;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.augment.Augment;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.Shortcuts;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.beans.value.ObservableValue;
import javafx.event.EventHandler;
import javafx.event.WeakEventHandler;
import javafx.geometry.NodeOrientation;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.stage.Window;
import javafx.util.Duration;
import javafx.scene.control.Tooltip;
public class FancyTooltipAugment<S extends CompStructure<?>> implements Augment<S> {
private static final TooltipBehavior BEHAVIOR =
new TooltipBehavior(Duration.millis(400), Duration.INDEFINITE, Duration.millis(100));
private final ObservableValue<String> text;
public FancyTooltipAugment(ObservableValue<String> text) {
@ -35,7 +23,7 @@ public class FancyTooltipAugment<S extends CompStructure<?>> implements Augment<
@Override
public void augment(S struc) {
var region = struc.get();
var tt = new JFXTooltip();
var tt = new Tooltip();
var toDisplay = text.getValue();
if (Shortcuts.getShortcut(region) != null) {
toDisplay = toDisplay + " (" + Shortcuts.getShortcut(region).getDisplayText() + ")";
@ -46,188 +34,6 @@ public class FancyTooltipAugment<S extends CompStructure<?>> implements Augment<
tt.setMaxWidth(400);
tt.getStyleClass().add("fancy-tooltip");
BEHAVIOR.install(region, tt);
}
private static class TooltipBehavior {
private static final String TOOLTIP_PROP = "jfoenix-tooltip";
private final Timeline hoverTimer = new Timeline();
private final Timeline visibleTimer = new Timeline();
private final Timeline leftTimer = new Timeline();
/**
* the currently hovered node
*/
private Node hoveredNode;
/**
* the next tooltip to be shown
*/
private JFXTooltip nextTooltip;
private final EventHandler<MouseEvent> exitHandler = (MouseEvent event) -> {
// stop running hover timer as the mouse exited the node
if (hoverTimer.getStatus() == Timeline.Status.RUNNING) {
hoverTimer.stop();
} else if (visibleTimer.getStatus() == Timeline.Status.RUNNING) {
// if tool tip was already showing, stop the visible timer
// and start the left timer to hide the current tooltip
visibleTimer.stop();
leftTimer.playFromStart();
}
hoveredNode = null;
nextTooltip = null;
};
private final WeakEventHandler<MouseEvent> weakExitHandler = new WeakEventHandler<>(exitHandler);
/**
* the current showing tooltip
*/
private JFXTooltip currentTooltip;
// if mouse is pressed then stop all timers / clear all fields
private final EventHandler<MouseEvent> pressedHandler = (MouseEvent event) -> {
// stop timers
hoverTimer.stop();
visibleTimer.stop();
leftTimer.stop();
// hide current tooltip
if (currentTooltip != null) {
currentTooltip.hide();
}
// clear fields
hoveredNode = null;
currentTooltip = null;
nextTooltip = null;
};
private final WeakEventHandler<MouseEvent> weakPressedHandler = new WeakEventHandler<>(pressedHandler);
private TooltipBehavior(Duration hoverDelay, Duration visibleDuration, Duration leftDelay) {
setHoverDelay(hoverDelay);
hoverTimer.setOnFinished(event -> {
ensureHoveredNodeIsVisible(() -> {
// set tooltip orientation
NodeOrientation nodeOrientation = hoveredNode.getEffectiveNodeOrientation();
nextTooltip.getScene().setNodeOrientation(nodeOrientation);
// show tooltip
showTooltip(nextTooltip);
currentTooltip = nextTooltip;
hoveredNode = null;
// start visible timer
visibleTimer.playFromStart();
});
// clear next tooltip
nextTooltip = null;
});
setVisibleDuration(visibleDuration);
visibleTimer.setOnFinished(event -> hideCurrentTooltip());
setLeftDelay(leftDelay);
leftTimer.setOnFinished(event -> hideCurrentTooltip());
}
private void setHoverDelay(Duration duration) {
hoverTimer.getKeyFrames().setAll(new KeyFrame(duration));
}
private void setVisibleDuration(Duration duration) {
visibleTimer.getKeyFrames().setAll(new KeyFrame(duration));
}
private final EventHandler<MouseEvent> moveHandler = (MouseEvent event) -> {
// if tool tip is already showing, do nothing
if (visibleTimer.getStatus() == Timeline.Status.RUNNING) {
return;
}
hoveredNode = (Node) event.getSource();
Object property = hoveredNode.getProperties().get(TOOLTIP_PROP);
if (property instanceof JFXTooltip tooltip) {
ensureHoveredNodeIsVisible(() -> {
// if a tooltip is already showing then show this tooltip immediately
if (leftTimer.getStatus() == Timeline.Status.RUNNING) {
if (currentTooltip != null) {
currentTooltip.hide();
}
currentTooltip = tooltip;
// show the tooltip
showTooltip(tooltip);
// stop left timer and start the visible timer to hide the tooltip
// once finished
leftTimer.stop();
visibleTimer.playFromStart();
} else {
// else mark the tooltip as the next tooltip to be shown once the hover
// timer is finished (restart the timer)
// t.setActivated(true);
nextTooltip = tooltip;
hoverTimer.stop();
hoverTimer.playFromStart();
}
});
} else {
uninstall(hoveredNode);
}
};
private void setLeftDelay(Duration duration) {
leftTimer.getKeyFrames().setAll(new KeyFrame(duration));
}
private final WeakEventHandler<MouseEvent> weakMoveHandler = new WeakEventHandler<>(moveHandler);
private void hideCurrentTooltip() {
currentTooltip.hide();
currentTooltip = null;
hoveredNode = null;
}
private void showTooltip(JFXTooltip tooltip) {
// anchors are computed differently for each tooltip
tooltip.show(hoveredNode, -1, -1);
}
private void install(Node node, JFXTooltip tooltip) {
if (node == null) {
return;
}
if (tooltip == null) {
uninstall(node);
return;
}
node.removeEventHandler(MouseEvent.MOUSE_MOVED, weakMoveHandler);
node.removeEventHandler(MouseEvent.MOUSE_EXITED, weakExitHandler);
node.removeEventHandler(MouseEvent.MOUSE_PRESSED, weakPressedHandler);
node.addEventHandler(MouseEvent.MOUSE_MOVED, weakMoveHandler);
node.addEventHandler(MouseEvent.MOUSE_EXITED, weakExitHandler);
node.addEventHandler(MouseEvent.MOUSE_PRESSED, weakPressedHandler);
node.getProperties().put(TOOLTIP_PROP, tooltip);
}
private void uninstall(Node node) {
if (node == null) {
return;
}
node.removeEventHandler(MouseEvent.MOUSE_MOVED, weakMoveHandler);
node.removeEventHandler(MouseEvent.MOUSE_EXITED, weakExitHandler);
node.removeEventHandler(MouseEvent.MOUSE_PRESSED, weakPressedHandler);
Object tooltip = node.getProperties().get(TOOLTIP_PROP);
if (tooltip != null) {
node.getProperties().remove(TOOLTIP_PROP);
if (tooltip.equals(currentTooltip) || tooltip.equals(nextTooltip)) {
weakPressedHandler.handle(null);
}
}
}
private void ensureHoveredNodeIsVisible(Runnable visibleRunnable) {
final Window owner = getWindow(hoveredNode);
if (owner != null && owner.isShowing()) {
final boolean treeVisible = true; // NodeHelper.isTreeVisible(hoveredNode);
if (treeVisible && owner.isFocused()) {
visibleRunnable.run();
}
}
}
private Window getWindow(final Node node) {
final Scene scene = node == null ? null : node.getScene();
return scene == null ? null : scene.getWindow();
}
Tooltip.install(struc.get(), tt);
}
}

View file

@ -50,13 +50,13 @@ public class OptionsComp extends Comp<CompStructure<Pane>> {
var nameRegions = new ArrayList<Region>();
Region firstComp = null;
for (var entry : getEntries()) {
Region compRegion = null;
if (entry.comp() != null) {
compRegion = entry.comp().createRegion();
}
if (firstComp == null) {
compRegion.getStyleClass().add("first");
firstComp = compRegion;
}

View file

@ -1,12 +1,13 @@
package io.xpipe.app.fxcomps.impl;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.util.SecretHelper;
import io.xpipe.core.util.SecretValue;
import io.xpipe.core.util.InPlaceSecretValue;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.control.PasswordField;
import javafx.scene.control.TextField;
@ -14,14 +15,25 @@ import java.util.Objects;
public class SecretFieldComp extends Comp<CompStructure<TextField>> {
private final Property<SecretValue> value;
public static SecretFieldComp ofString(Property<String> s) {
var prop = new SimpleObjectProperty<InPlaceSecretValue>(s.getValue() != null ? InPlaceSecretValue.of(s.getValue()) : null);
prop.addListener((observable, oldValue, newValue) -> {
s.setValue(newValue != null ? new String(newValue.getSecret()) : null);
});
s.addListener((observableValue, s1, t1) -> {
prop.set(t1 != null ? InPlaceSecretValue.of(t1) : null);
});
return new SecretFieldComp(prop);
}
public SecretFieldComp(Property<SecretValue> value) {
private final Property<InPlaceSecretValue> value;
public SecretFieldComp(Property<InPlaceSecretValue> value) {
this.value = value;
}
protected SecretValue encrypt(char[] c) {
return SecretHelper.encrypt(c);
protected InPlaceSecretValue encrypt(char[] c) {
return InPlaceSecretValue.of(c);
}
@Override
@ -42,6 +54,7 @@ public class SecretFieldComp extends Comp<CompStructure<TextField>> {
text.setText(n != null ? n.getSecretValue() : null);
});
});
AppFont.small(text);
return new SimpleCompStructure<>(text);
}
}

View file

@ -1,64 +0,0 @@
package io.xpipe.app.fxcomps.impl;
import com.jfoenix.controls.JFXTabPane;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.util.PlatformThread;
import javafx.beans.property.Property;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.Tab;
import lombok.Getter;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
@Getter
public class TabPaneComp extends Comp<CompStructure<JFXTabPane>> {
private final Property<Entry> selected;
private final List<Entry> entries;
public TabPaneComp(Property<Entry> selected, List<Entry> entries) {
this.selected = selected;
this.entries = entries;
}
@Override
public CompStructure<JFXTabPane> createBase() {
JFXTabPane tabPane = new JFXTabPane();
tabPane.getStyleClass().add("tab-pane-comp");
for (var e : entries) {
Tab tab = new Tab();
var ll = new Label(null);
if (e.graphic != null) {
ll.setGraphic(new FontIcon(e.graphic()));
}
ll.textProperty().bind(e.name());
ll.getStyleClass().add("name");
ll.setAlignment(Pos.CENTER);
tab.setGraphic(ll);
var content = e.comp().createRegion();
tab.setContent(content);
tabPane.getTabs().add(tab);
content.prefWidthProperty().bind(tabPane.widthProperty());
}
tabPane.getSelectionModel().select(entries.indexOf(selected.getValue()));
tabPane.getSelectionModel().selectedIndexProperty().addListener((c, o, n) -> {
selected.setValue(entries.get(n.intValue()));
});
selected.addListener((c, o, n) -> {
PlatformThread.runLaterIfNeeded(() -> {
tabPane.getSelectionModel().select(entries.indexOf(n));
});
});
return new SimpleCompStructure<>(tabPane);
}
public record Entry(ObservableValue<String> name, String graphic, Comp<?> comp) {}
}

View file

@ -1,19 +1,14 @@
package io.xpipe.app.issue;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.TabPaneComp;
import io.xpipe.core.util.Deobfuscator;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Insets;
import javafx.scene.control.TextArea;
import javafx.scene.layout.Region;
import lombok.AllArgsConstructor;
import java.util.ArrayList;
@AllArgsConstructor
public class ErrorDetailsComp extends SimpleComp {
@ -31,18 +26,12 @@ public class ErrorDetailsComp extends SimpleComp {
return tf;
}
return null;
return new Region();
}
@Override
protected Region createSimple() {
var items = new ArrayList<TabPaneComp.Entry>();
if (event.getThrowable() != null) {
items.add(new TabPaneComp.Entry(
AppI18n.observable("stackTrace"), "mdoal-code", Comp.of(this::createStrackTraceContent)));
}
var tb = new TabPaneComp(new SimpleObjectProperty<>(items.size() > 0 ? items.get(0) : null), items);
var tb = Comp.of(this::createStrackTraceContent);
tb.apply(r -> AppFont.small(r.get()));
return tb.createRegion();
}

View file

@ -8,6 +8,7 @@ import lombok.Singular;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
@Builder
@Getter
@ -61,12 +62,31 @@ public class ErrorEvent {
return builder().description(msg);
}
public void handle() {
EventHandler.get().modify(this);
EventHandler.get().handle(this);
public List<Throwable> getThrowableChain() {
var list = new ArrayList<Throwable>();
Throwable t = getThrowable();
while (t != null) {
list.addFirst(t);
t = t.getCause();
}
return list;
}
private boolean shouldIgnore(Throwable throwable) {
return (throwable != null && HANDLED.stream().anyMatch(t -> t == throwable) && !terminal) ||
(throwable != null && throwable.getCause() != throwable && shouldIgnore(throwable.getCause()));
}
public void handle() {
// Check object identity to allow for multiple exceptions with same trace
if (shouldIgnore(throwable)) {
return;
}
EventHandler.get().modify(this);
EventHandler.get().handle(this);
HANDLED.add(throwable);
}
public void addAttachment(Path file) {
attachments = new ArrayList<>(attachments);
@ -101,6 +121,7 @@ public class ErrorEvent {
}
private static final Map<Throwable, ErrorEventBuilder> EVENT_BASES = new ConcurrentHashMap<>();
private static final Set<Throwable> HANDLED = new CopyOnWriteArraySet<>();
public static <T extends Throwable> T unreportableIfEndsWith(T t, String... s) {
return unreportableIf(

View file

@ -14,7 +14,6 @@ public class EventHandlerImpl extends EventHandler {
var suffix = ee.getThrowable() != null ? Deobfuscator.deobfuscateToString(ee.getThrowable()) : "";
te.message(prefix + suffix);
te.type("error");
te.category("exception");
te.tag("omitted", ee.isOmitted());
te.tag("terminal", ee.isTerminal());
te.elements(ee.getAttachments().stream().map(Path::toString).toList());
@ -31,15 +30,6 @@ public class EventHandlerImpl extends EventHandler {
}
}
private void handleBasic(ErrorEvent ee) {
if (ee.getDescription() != null) {
System.err.println(ee.getDescription());
}
if (ee.getThrowable() != null) {
Deobfuscator.printStackTrace(ee.getThrowable());
}
}
@Override
public void handle(ErrorEvent ee) {
if (ee.isTerminal()) {

View file

@ -3,6 +3,8 @@ package io.xpipe.app.issue;
import io.xpipe.app.util.LicenseProvider;
import io.xpipe.app.util.LicenseRequiredException;
import java.util.stream.Stream;
public class GuiErrorHandler extends GuiErrorHandlerBase implements ErrorHandler {
private final ErrorHandler log = new LogErrorHandler();
@ -28,8 +30,10 @@ public class GuiErrorHandler extends GuiErrorHandlerBase implements ErrorHandler
}
private void handleGui(ErrorEvent event) {
if (event.getThrowable() instanceof LicenseRequiredException lex) {
LicenseProvider.get().showLicenseAlert(lex);
var lex = event.getThrowableChain().stream().flatMap(throwable -> throwable instanceof LicenseRequiredException le ?
Stream.of(le) : Stream.of()).findFirst();
if (lex.isPresent()) {
LicenseProvider.get().showLicenseAlert(lex.get());
event.setShouldSendDiagnostics(true);
ErrorAction.ignore().handle(event);
} else {

View file

@ -48,8 +48,10 @@ public class SentryErrorHandler implements ErrorHandler {
options.setTag("arch", System.getProperty("os.arch"));
options.setDist(XPipeDistributionType.get().getId());
options.setTag("staging", String.valueOf(AppProperties.get().isStaging()));
options.setCacheDirPath(AppProperties.get().getDataDir().resolve("cache").toString());
options.setSendModules(false);
options.setAttachThreads(false);
options.setEnableDeduplication(false);
options.setCacheDirPath(AppProperties.get().getDataDir().resolve("cache").toString());
});
}
init = true;
@ -147,7 +149,7 @@ public class SentryErrorHandler implements ErrorHandler {
return Sentry.captureEvent(event, sc -> fillScope(ee, sc));
}
private static void fillScope(ErrorEvent ee, Scope s) {
private static void fillScope(ErrorEvent ee, IScope s) {
if (ee.isShouldSendDiagnostics()) {
var atts = ee.getAttachments().stream()
.map(d -> {

View file

@ -49,7 +49,7 @@ public class TerminalErrorHandler extends GuiErrorHandlerBase implements ErrorHa
return;
}
if (OperationMode.isInStartup()) {
if (OperationMode.isInStartup() && !AppProperties.get().isDevelopmentEnvironment()) {
handleProbableUpdate();
}

View file

@ -18,7 +18,6 @@ public class TrackEvent {
private final Instant instant = Instant.now();
private String type;
private String message;
private String category;
@Singular
private Map<String, Object> tags;
@ -26,30 +25,14 @@ public class TrackEvent {
@Singular
private List<Object> elements;
public static TrackEventBuilder storage() {
return TrackEvent.builder().category("storage");
}
public static TrackEventBuilder fromMessage(String type, String message) {
return builder().type(type).message(message);
}
public static void simple(String type, String message) {
builder().type(type).message(message).build().handle();
}
public static TrackEventBuilder withInfo(String message) {
return builder().type("info").message(message);
}
public static TrackEventBuilder withInfo(String category, String message) {
return builder().category(category).type("info").message(message);
}
public static TrackEventBuilder withWarn(String category, String message) {
return builder().category(category).type("warn").message(message);
}
public static TrackEventBuilder withWarn(String message) {
return builder().type("warn").message(message);
}
@ -58,10 +41,6 @@ public class TrackEvent {
return builder().type("trace").message(message);
}
public static TrackEventBuilder withTrace(String cat, String message) {
return builder().category(cat).type("trace").message(message);
}
public static void info(String message) {
builder().type("info").message(message).build().handle();
}
@ -74,14 +53,6 @@ public class TrackEvent {
return builder().type("debug").message(message);
}
public static TrackEventBuilder withDebug(String cat, String message) {
return builder().category(cat).type("debug").message(message);
}
public static void debug(String cat, String message) {
builder().category(cat).type("debug").message(message).build().handle();
}
public static void debug(String message) {
builder().type("debug").message(message).build().handle();
}
@ -90,14 +61,6 @@ public class TrackEvent {
builder().type("trace").message(message).build().handle();
}
public static void info(String cat, String message) {
builder().category(cat).type("info").message(message).build().handle();
}
public static void trace(String cat, String message) {
builder().category(cat).type("trace").message(message).build().handle();
}
public static TrackEventBuilder withError(String message) {
return builder().type("error").message(message);
}
@ -142,19 +105,8 @@ public class TrackEvent {
public static class TrackEventBuilder {
public TrackEventBuilder trace() {
this.type("trace");
return this;
}
public TrackEventBuilder windowCategory() {
this.category("window");
return this;
}
public TrackEventBuilder copy() {
var copy = builder();
copy.category = category;
copy.message = message;
copy.tags$key = new ArrayList<>(tags$key);
copy.tags$value = new ArrayList<>(tags$value);

View file

@ -43,7 +43,7 @@ public class LauncherCommand implements Callable<Integer> {
final List<String> inputs = List.of();
public static void runLauncher(String[] args) {
TrackEvent.builder().category("launcher").type("debug").message("Launcher received commands: " + Arrays.asList(args)).handle();
TrackEvent.builder().type("debug").message("Launcher received commands: " + Arrays.asList(args)).handle();
var cmd = new CommandLine(new LauncherCommand());
cmd.setExecutionExceptionHandler((ex, commandLine, parseResult) -> {

View file

@ -24,7 +24,7 @@ public abstract class LauncherInput {
return;
}
TrackEvent.withDebug("launcher", "Handling arguments")
TrackEvent.withDebug("Handling arguments")
.elements(arguments)
.handle();

View file

@ -1,12 +1,15 @@
package io.xpipe.app.prefs;
import atlantafx.base.theme.Styles;
import io.xpipe.app.comp.base.TileButtonComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppProperties;
import io.xpipe.app.core.AppWindowHelper;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.impl.LabelComp;
import io.xpipe.app.fxcomps.impl.VerticalComp;
import io.xpipe.app.util.Hyperlinks;
import io.xpipe.app.util.JfxHelper;
import io.xpipe.app.util.OptionsBuilder;
import javafx.geometry.Insets;
import javafx.scene.control.ScrollPane;
@ -14,7 +17,7 @@ import javafx.scene.layout.Region;
import java.util.List;
public class AboutComp extends Comp<CompStructure<?>> {
public class AboutCategory extends AppPrefsCategory {
private Comp<?> createLinks() {
return new OptionsBuilder()
@ -79,15 +82,44 @@ public class AboutComp extends Comp<CompStructure<?>> {
}
@Override
public CompStructure<?> createBase() {
var props = new PropertiesComp().padding(new Insets(0, 0, 0, 15));
protected String getId() {
return "about";
}
private Comp<?> createProperties() {
var title = Comp.of(() -> {
return JfxHelper.createNamedEntry(AppI18n.get("xPipeClient"), "Version " + AppProperties.get().getVersion() + " ("
+ AppProperties.get().getArch() + ")", "logo/logo_48x48.png");
}).styleClass(Styles.TEXT_BOLD);
var section = new OptionsBuilder()
.addComp(title, null)
.addComp(Comp.vspacer(10))
.name("build")
.addComp(
new LabelComp(AppProperties.get().getBuild()),
null)
.name("runtimeVersion")
.addComp(
new LabelComp(System.getProperty("java.vm.version")),
null)
.name("virtualMachine")
.addComp(
new LabelComp(System.getProperty("java.vm.vendor") + " " + System.getProperty("java.vm.name")),
null)
.buildComp();
return section.styleClass("properties-comp");
}
@Override
protected Comp<?> create() {
var props = createProperties().padding(new Insets(0, 0, 0, 15));
var update = new UpdateCheckComp().grow(true, false);
var box = new VerticalComp(List.of(props, Comp.separator(), update, Comp.separator(), createLinks()))
return new VerticalComp(List.of(props, Comp.separator(), update, Comp.separator(), createLinks()))
.apply(s -> s.get().setFillWidth(true))
.apply(struc -> struc.get().setSpacing(15))
.styleClass("information")
.styleClass("about-tab")
.apply(struc -> struc.get().setPrefWidth(600));
return box.createStructure();
}
}

View file

@ -1,177 +0,0 @@
package io.xpipe.app.prefs;
import com.dlsc.formsfx.model.structure.Form;
import com.dlsc.formsfx.model.util.TranslationService;
import com.dlsc.preferencesfx.PreferencesFxEvent;
import com.dlsc.preferencesfx.history.History;
import com.dlsc.preferencesfx.model.Category;
import com.dlsc.preferencesfx.model.PreferencesFxModel;
import com.dlsc.preferencesfx.util.SearchHandler;
import com.dlsc.preferencesfx.util.StorageHandler;
import com.dlsc.preferencesfx.view.*;
import javafx.beans.property.ObjectProperty;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.scene.Node;
import javafx.scene.control.ScrollPane;
import lombok.Getter;
import lombok.SneakyThrows;
import java.util.List;
@Getter
public class AppPreferencesFx {
private final PreferencesFxModel preferencesFxModel;
private NavigationView navigationView;
private NavigationPresenter navigationPresenter;
private UndoRedoBox undoRedoBox;
private BreadCrumbView breadCrumbView;
private BreadCrumbPresenter breadCrumbPresenter;
private CategoryController categoryController;
private PreferencesFxView preferencesFxView;
private PreferencesFxPresenter preferencesFxPresenter;
private AppPreferencesFx(StorageHandler storageHandler, Category... categories) {
preferencesFxModel = new PreferencesFxModel(storageHandler, new SearchHandler(), new History(), categories);
configure();
}
public static AppPreferencesFx of(Category... categories) {
return new AppPreferencesFx(new JsonStorageHandler(), categories);
}
private void configure() {
preferencesFxModel.setSaveSettings(true);
preferencesFxModel.setHistoryDebugState(true);
preferencesFxModel.setInstantPersistent(true);
preferencesFxModel.setButtonsVisible(false);
}
public void loadSettings() {
// setting values are only loaded if they are present already
preferencesFxModel.loadSettingValues();
}
@SneakyThrows
public void setupControls() {
undoRedoBox = new UndoRedoBox(preferencesFxModel.getHistory());
breadCrumbView = new BreadCrumbView(preferencesFxModel, undoRedoBox) {
@Override
public void initializeParts() {}
@Override
public void layoutParts() {}
};
breadCrumbPresenter = new BreadCrumbPresenter(preferencesFxModel, breadCrumbView);
categoryController = new CategoryController();
categoryController.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
categoryController.setFitToWidth(true);
initializeCategoryViews();
// display initial category
categoryController.setView(preferencesFxModel.getDisplayedCategory());
navigationView = new NavigationView(preferencesFxModel);
var searchField = navigationView.getClass().getDeclaredField("searchFld");
searchField.setAccessible(true);
Node search = (Node) searchField.get(navigationView);
search.setManaged(false);
search.setVisible(false);
navigationPresenter = new NavigationPresenter(preferencesFxModel, navigationView);
preferencesFxView =
new PreferencesFxView(preferencesFxModel, navigationView, breadCrumbView, categoryController);
preferencesFxPresenter = new PreferencesFxPresenter(preferencesFxModel, preferencesFxView) {
@Override
public void setupEventHandlers() {
// Ignore window close
}
};
}
public ObjectProperty<TranslationService> translationServiceProperty() {
return preferencesFxModel.translationServiceProperty();
}
/**
* Prepares the CategoryController by creating CategoryView / CategoryPresenter pairs from all
* Categories and loading them into the CategoryController.
*/
private void initializeCategoryViews() {
preferencesFxModel.getFlatCategoriesLst().forEach(category -> {
var categoryView = new CustomCategoryView(preferencesFxModel, category);
CategoryPresenter categoryPresenter =
new CategoryPresenter(preferencesFxModel, category, categoryView, breadCrumbPresenter) {
@Override
@SneakyThrows
public void initializeViewParts() {
var formMethod = CategoryPresenter.class.getDeclaredMethod("createForm");
formMethod.setAccessible(true);
var formField = CategoryPresenter.class.getDeclaredField("form");
formField.setAccessible(true);
formField.set(this, formMethod.invoke(this));
categoryView.initializeFormRenderer((Form) formField.get(this));
this.addI18nListener();
this.addInstantPersistenceListener();
}
};
categoryController.addView(category, categoryView, categoryPresenter);
});
}
/**
* Call this method to manually save the changed settings when showing the preferences by using
* {@link #getView()}.
*/
public void saveSettings() {
preferencesFxModel.saveSettings();
((JsonStorageHandler) preferencesFxModel.getStorageHandler()).save();
}
/**
* Call this method to undo all changes made in the settings when showing the preferences by using
* {@link #getView()}.
*/
public void discardChanges() {
preferencesFxModel.discardChanges();
}
/**
* Registers an event handler with the model. The handler is called when the model receives an
* {@code Event} of the specified type during the bubbling phase of event delivery.
*
* @param eventType the type of the events to receive by the handler
* @param eventHandler the handler to register
* @return PreferencesFx to allow for chaining.
* @throws NullPointerException if either event type or handler are {@code null}.
*/
public AppPreferencesFx addEventHandler(
EventType<PreferencesFxEvent> eventType, EventHandler<? super PreferencesFxEvent> eventHandler) {
preferencesFxModel.addEventHandler(eventType, eventHandler);
return this;
}
/**
* Returns a PreferencesFxView, so that it can be used as a Node.
*
* @return a PreferencesFxView, so that it can be used as a Node.
*/
public PreferencesFxView getView() {
return preferencesFxView;
}
public List<Category> getCategories() {
return preferencesFxModel.getCategories();
}
}

View file

@ -1,45 +1,56 @@
package io.xpipe.app.prefs;
import com.dlsc.formsfx.model.structure.*;
import com.dlsc.preferencesfx.formsfx.view.controls.DoubleSliderControl;
import com.dlsc.preferencesfx.formsfx.view.controls.SimpleTextControl;
import com.dlsc.preferencesfx.model.Category;
import com.dlsc.preferencesfx.model.Group;
import com.dlsc.preferencesfx.model.Setting;
import com.dlsc.preferencesfx.util.VisibilityProperty;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppCache;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.core.AppProperties;
import io.xpipe.app.core.AppTheme;
import io.xpipe.app.ext.PrefsChoiceValue;
import io.xpipe.app.ext.PrefsHandler;
import io.xpipe.app.ext.PrefsProvider;
import io.xpipe.app.fxcomps.impl.StackComp;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.*;
import io.xpipe.core.store.LocalStore;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.ApplicationHelper;
import io.xpipe.app.util.ElevationAccess;
import io.xpipe.app.util.PasswordLockSecretValue;
import io.xpipe.core.util.InPlaceSecretValue;
import io.xpipe.core.util.ModuleHelper;
import io.xpipe.core.util.SecretValue;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.value.ObservableBooleanValue;
import javafx.beans.value.ObservableDoubleValue;
import javafx.beans.value.ObservableStringValue;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import lombok.Getter;
import lombok.SneakyThrows;
import org.kordamp.ikonli.javafx.FontIcon;
import lombok.Value;
import java.nio.file.Path;
import java.util.*;
public class AppPrefs {
@Value
public static class Mapping<T> {
String key;
Property<T> property;
Class<T> valueClass;
boolean vaultSpecific;
public Mapping(String key, Property<T> property, Class<T> valueClass) {
this.key = key;
this.property = property;
this.valueClass = valueClass;
this.vaultSpecific = false;
}
public Mapping(String key, Property<T> property, Class<T> valueClass, boolean vaultSpecific) {
this.key = key;
this.property = property;
this.valueClass = valueClass;
this.vaultSpecific = vaultSpecific;
}
}
public boolean isDevelopmentEnvironment() {
return developerMode().getValue() && !ModuleHelper.isImage();
}
@ -62,219 +73,189 @@ public class AppPrefs {
developerMode());
}
private static final int tooltipDelayMin = 0;
private static final int tooltipDelayMax = 1500;
private static final int editorReloadTimeoutMin = 0;
private static final int editorReloadTimeoutMax = 1500;
private final List<Mapping<?>> mapping = new ArrayList<>();
public static final Path DEFAULT_STORAGE_DIR =
AppProperties.get().getDataDir().resolve("storage");
private static final String DEVELOPER_MODE_PROP = "io.xpipe.app.developerMode";
private static AppPrefs INSTANCE;
private final SimpleListProperty<SupportedLocale> languageList =
new SimpleListProperty<>(FXCollections.observableArrayList(Arrays.asList(SupportedLocale.values())));
private final SimpleListProperty<AppTheme.Theme> themeList =
new SimpleListProperty<>(FXCollections.observableArrayList(AppTheme.Theme.ALL));
private final SimpleListProperty<CloseBehaviour> closeBehaviourList = new SimpleListProperty<>(
FXCollections.observableArrayList(PrefsChoiceValue.getSupported(CloseBehaviour.class)));
private final SimpleListProperty<ExternalEditorType> externalEditorList = new SimpleListProperty<>(
FXCollections.observableArrayList(PrefsChoiceValue.getSupported(ExternalEditorType.class)));
private final SimpleListProperty<String> logLevelList =
new SimpleListProperty<>(FXCollections.observableArrayList("trace", "debug", "info", "warn", "error"));
private final Map<Object, Class<?>> classMap = new HashMap<>();
// Languages
// =========
private final ObjectProperty<SupportedLocale> languageInternal =
typed(new SimpleObjectProperty<>(SupportedLocale.ENGLISH), SupportedLocale.class);
public final Property<SupportedLocale> language = new SimpleObjectProperty<>(SupportedLocale.ENGLISH);
private final SingleSelectionField<SupportedLocale> languageControl = Field.ofSingleSelectionType(
languageList, languageInternal)
.render(() -> new TranslatableComboBoxControl<>());
private final ObjectProperty<SupportedLocale> language =
map(new SimpleObjectProperty<>(SupportedLocale.ENGLISH), "language", SupportedLocale.class);
public ObservableValue<SupportedLocale> language() {
return language;
}
final BooleanProperty dontAcceptNewHostKeys = map(new SimpleBooleanProperty(false), "dontAcceptNewHostKeys", Boolean.class);
public ObservableBooleanValue dontAcceptNewHostKeys() {
return dontAcceptNewHostKeys;
}
final BooleanProperty performanceMode = typed(new SimpleBooleanProperty(false), Boolean.class);
final BooleanProperty performanceMode = map(new SimpleBooleanProperty(false), "performanceMode", Boolean.class);
public ObservableBooleanValue performanceMode() {
return performanceMode;
}
public final ObjectProperty<AppTheme.Theme> theme = typed(new SimpleObjectProperty<>(), AppTheme.Theme.class);
private final SingleSelectionField<AppTheme.Theme> themeControl =
Field.ofSingleSelectionType(themeList, theme).render(() -> new TranslatableComboBoxControl<>());
private final BooleanProperty useSystemFontInternal = typed(new SimpleBooleanProperty(true), Boolean.class);
public final ReadOnlyBooleanProperty useSystemFont = useSystemFontInternal;
private final IntegerProperty tooltipDelayInternal = typed(new SimpleIntegerProperty(1000), Integer.class);
private final IntegerProperty connectionTimeOut = typed(new SimpleIntegerProperty(10), Integer.class);
final BooleanProperty useBundledTools = map(new SimpleBooleanProperty(false), "useBundledTools", Boolean.class);
public ObservableBooleanValue useBundledTools() {
return useBundledTools;
}
public ReadOnlyIntegerProperty connectionTimeout() {
public final ObjectProperty<AppTheme.Theme> theme = map(new SimpleObjectProperty<>(), "theme", AppTheme.Theme.class);
final BooleanProperty useSystemFont = map(new SimpleBooleanProperty(true), "useSystemFont", Boolean.class);
public ObservableValue<Boolean> useSystemFont() {
return useSystemFont;
}
final Property<Integer> uiScale = map(new SimpleObjectProperty<>(null), "uiScale", Integer.class);
public ReadOnlyProperty<Integer> uiScale() {
return uiScale;
}
final Property<Integer> connectionTimeOut = map(new SimpleObjectProperty<>(10), "connectionTimeout", Integer.class);
public ReadOnlyProperty<Integer> connectionTimeOut() {
return connectionTimeOut;
}
private final BooleanProperty saveWindowLocation = typed(new SimpleBooleanProperty(true), Boolean.class);
final BooleanProperty saveWindowLocation = map(new SimpleBooleanProperty(true), "saveWindowLocation", Boolean.class);
// External terminal
// =================
private final ObjectProperty<ExternalTerminalType> terminalType =
typed(new SimpleObjectProperty<>(), ExternalTerminalType.class);
private final SimpleListProperty<ExternalTerminalType> terminalTypeList = new SimpleListProperty<>(
FXCollections.observableArrayList(PrefsChoiceValue.getSupported(ExternalTerminalType.class)));
private final SingleSelectionField<ExternalTerminalType> terminalTypeControl = Field.ofSingleSelectionType(
terminalTypeList, terminalType)
.render(() -> new TranslatableComboBoxControl<>());
final ObjectProperty<ExternalTerminalType> terminalType =
map(new SimpleObjectProperty<>(), "terminalType", ExternalTerminalType.class);
// Lock
// ====
@Getter
private final Property<SecretValue> lockPassword = new SimpleObjectProperty<>();
private final Property<InPlaceSecretValue> lockPassword = new SimpleObjectProperty<>();
@Getter
private final StringProperty lockCrypt = typed(new SimpleStringProperty(""), String.class);
private final StringProperty lockCrypt = mapVaultSpecific(new SimpleStringProperty(), "workspaceLock", String.class);
// Window opacity
// ==============
private final DoubleProperty windowOpacity = typed(new SimpleDoubleProperty(1.0), Double.class);
private final DoubleField windowOpacityField =
Field.ofDoubleType(windowOpacity).render(() -> {
var r = new DoubleSliderControl(0.3, 1.0, 2);
r.setMinWidth(200);
return r;
});
final DoubleProperty windowOpacity = map(new SimpleDoubleProperty(1.0), "windowOpacity", Double.class);
// Custom terminal
// ===============
private final StringProperty customTerminalCommand = typed(new SimpleStringProperty(""), String.class);
private final StringField customTerminalCommandControl = editable(
StringField.ofStringType(customTerminalCommand).placeholder("customTerminalPlaceholder").render(() -> new SimpleTextControl()),
terminalType.isEqualTo(ExternalTerminalType.CUSTOM));
final StringProperty customTerminalCommand = map(new SimpleStringProperty(""), "customTerminalCommand", String.class);
private final BooleanProperty preferTerminalTabs = typed(new SimpleBooleanProperty(true), Boolean.class);
private final BooleanField preferTerminalTabsField =
BooleanField.ofBooleanType(preferTerminalTabs).render(() -> new CustomToggleControl());
final BooleanProperty preferTerminalTabs = map(new SimpleBooleanProperty(true), "preferTerminalTabs", Boolean.class);
// Fast terminal
// ===========
public final BooleanProperty enableFastTerminalStartup = typed(new SimpleBooleanProperty(false), Boolean.class);
public ObservableBooleanValue enableFastTerminalStartup() {
return enableFastTerminalStartup;
final BooleanProperty clearTerminalOnInit = map(new SimpleBooleanProperty(true), "clearTerminalOnInit", Boolean.class);
public ReadOnlyBooleanProperty clearTerminalOnInit() {
return clearTerminalOnInit;
}
public final BooleanProperty disableCertutilUse = map(new SimpleBooleanProperty(false), "disableCertutilUse", Boolean.class);
public ObservableBooleanValue disableCertutilUse() {
return disableCertutilUse;
}
public final BooleanProperty useLocalFallbackShell = map(new SimpleBooleanProperty(false), "useLocalFallbackShell", Boolean.class);
public ObservableBooleanValue useLocalFallbackShell() {
return useLocalFallbackShell;
}
public final BooleanProperty disableTerminalRemotePasswordPreparation = map(new SimpleBooleanProperty(false), "disableTerminalRemotePasswordPreparation", Boolean.class);
public ObservableBooleanValue disableTerminalRemotePasswordPreparation() {
return disableTerminalRemotePasswordPreparation;
}
public final Property<ElevationAccess> elevationPolicy = map(new SimpleObjectProperty<>(ElevationAccess.ALLOW), "elevationPolicy", ElevationAccess.class);
public ObservableValue<ElevationAccess> elevationPolicy() {
return elevationPolicy;
}
public final BooleanProperty dontCachePasswords = map(new SimpleBooleanProperty(false), "dontCachePasswords", Boolean.class);
public ObservableBooleanValue dontCachePasswords() {
return dontCachePasswords;
}
public final BooleanProperty denyTempScriptCreation = map(new SimpleBooleanProperty(false), "denyTempScriptCreation", Boolean.class);
public ObservableBooleanValue denyTempScriptCreation() {
return denyTempScriptCreation;
}
private final BooleanField enableFastTerminalStartupField =
BooleanField.ofBooleanType(enableFastTerminalStartup).render(() -> new CustomToggleControl());
// Password manager
// ================
final StringProperty passwordManagerCommand = typed(new SimpleStringProperty(""), String.class);
final StringProperty passwordManagerCommand = map(new SimpleStringProperty(""), "passwordManagerCommand", String.class);
// Start behaviour
// ===============
private final SimpleListProperty<StartupBehaviour> startupBehaviourList = new SimpleListProperty<>(
FXCollections.observableArrayList(PrefsChoiceValue.getSupported(StartupBehaviour.class)));
private final ObjectProperty<StartupBehaviour> startupBehaviour =
typed(new SimpleObjectProperty<>(StartupBehaviour.GUI), StartupBehaviour.class);
final ObjectProperty<StartupBehaviour> startupBehaviour =
map(new SimpleObjectProperty<>(StartupBehaviour.GUI), "startupBehaviour", StartupBehaviour.class);
private final SingleSelectionField<StartupBehaviour> startupBehaviourControl = Field.ofSingleSelectionType(
startupBehaviourList, startupBehaviour)
.render(() -> new TranslatableComboBoxControl<>());
// Git storage
// ===========
public final BooleanProperty enableGitStorage = typed(new SimpleBooleanProperty(false), Boolean.class);
public final BooleanProperty enableGitStorage = map(new SimpleBooleanProperty(false), "enableGitStorage", Boolean.class);
public ObservableBooleanValue enableGitStorage() {
return enableGitStorage;
}
final StringProperty storageGitRemote = typed(new SimpleStringProperty(""), String.class);
final StringProperty storageGitRemote = map(new SimpleStringProperty(""), "storageGitRemote", String.class);
public ObservableStringValue storageGitRemote() {
return storageGitRemote;
}
// Close behaviour
// ===============
private final ObjectProperty<CloseBehaviour> closeBehaviour =
typed(new SimpleObjectProperty<>(CloseBehaviour.QUIT), CloseBehaviour.class);
private final SingleSelectionField<CloseBehaviour> closeBehaviourControl = Field.ofSingleSelectionType(
closeBehaviourList, closeBehaviour)
.render(() -> new TranslatableComboBoxControl<>());
final ObjectProperty<CloseBehaviour> closeBehaviour =
map(new SimpleObjectProperty<>(CloseBehaviour.QUIT), "closeBehaviour", CloseBehaviour.class);
// External editor
// ===============
final ObjectProperty<ExternalEditorType> externalEditor =
typed(new SimpleObjectProperty<>(), ExternalEditorType.class);
private final SingleSelectionField<ExternalEditorType> externalEditorControl = Field.ofSingleSelectionType(
externalEditorList, externalEditor)
.render(() -> new TranslatableComboBoxControl<>());
map(new SimpleObjectProperty<>(), "externalEditor", ExternalEditorType.class);
final StringProperty customEditorCommand = typed(new SimpleStringProperty(""), String.class);
private final StringField customEditorCommandControl = editable(
StringField.ofStringType(customEditorCommand).placeholder("customEditorPlaceholder").render(() -> new SimpleTextControl()),
externalEditor.isEqualTo(ExternalEditorType.CUSTOM));
private final IntegerProperty editorReloadTimeout = typed(new SimpleIntegerProperty(1000), Integer.class);
final StringProperty customEditorCommand = map(new SimpleStringProperty(""), "customEditorCommand", String.class);
private final IntegerProperty editorReloadTimeout = map(new SimpleIntegerProperty(1000), "editorReloadTimeout", Integer.class);
private final BooleanProperty preferEditorTabs = typed(new SimpleBooleanProperty(true), Boolean.class);
private final BooleanField preferEditorTabsField =
BooleanField.ofBooleanType(preferEditorTabs).render(() -> new CustomToggleControl());
final BooleanProperty preferEditorTabs = map(new SimpleBooleanProperty(true), "preferEditorTabs", Boolean.class);
// Automatically update
// ====================
private final BooleanProperty automaticallyCheckForUpdates = typed(new SimpleBooleanProperty(true), Boolean.class);
private final BooleanField automaticallyCheckForUpdatesField =
BooleanField.ofBooleanType(automaticallyCheckForUpdates).render(() -> new CustomToggleControl());
final BooleanProperty automaticallyCheckForUpdates = map(new SimpleBooleanProperty(true), "automaticallyCheckForUpdates", Boolean.class);
private final BooleanProperty confirmDeletions = map(new SimpleBooleanProperty(true), "confirmDeletions", Boolean.class);
private final BooleanProperty confirmDeletions = typed(new SimpleBooleanProperty(true), Boolean.class);
final BooleanProperty encryptAllVaultData = mapVaultSpecific(new SimpleBooleanProperty(false), "encryptAllVaultData", Boolean.class);
public ObservableBooleanValue encryptAllVaultData() {
return encryptAllVaultData;
}
final BooleanProperty enforceWindowModality = map(new SimpleBooleanProperty(false), "enforceWindowModality", Boolean.class);
public ObservableBooleanValue enforceWindowModality() {
return enforceWindowModality;
}
final BooleanProperty condenseConnectionDisplay = map(new SimpleBooleanProperty(false), "condenseConnectionDisplay", Boolean.class);
public ObservableBooleanValue condenseConnectionDisplay() {
return condenseConnectionDisplay;
}
// Storage
// =======
final ObjectProperty<Path> storageDirectory =
typed(new SimpleObjectProperty<>(DEFAULT_STORAGE_DIR), Path.class);
final StringField storageDirectoryControl =
PrefFields.ofPath(storageDirectory).validate(CustomValidators.absolutePath(), CustomValidators.directory());
map(new SimpleObjectProperty<>(DEFAULT_STORAGE_DIR), "storageDirectory", Path.class);
// Developer mode
// ==============
private final BooleanProperty internalDeveloperMode = typed(new SimpleBooleanProperty(false), Boolean.class);
private final BooleanProperty effectiveDeveloperMode = System.getProperty(DEVELOPER_MODE_PROP) != null
? new SimpleBooleanProperty(Boolean.parseBoolean(System.getProperty(DEVELOPER_MODE_PROP)))
: internalDeveloperMode;
private final BooleanField developerModeField = Field.ofBooleanType(effectiveDeveloperMode)
.editable(System.getProperty(DEVELOPER_MODE_PROP) == null)
.render(() -> new CustomToggleControl());
final BooleanProperty developerMode = map(new SimpleBooleanProperty(false), "developerMode", Boolean.class);
final BooleanProperty developerDisableUpdateVersionCheck =
typed(new SimpleBooleanProperty(false), Boolean.class);
final BooleanField developerDisableUpdateVersionCheckField =
BooleanField.ofBooleanType(developerDisableUpdateVersionCheck).render(() -> new CustomToggleControl());
map(new SimpleBooleanProperty(false), "developerDisableUpdateVersionCheck", Boolean.class);
private final ObservableBooleanValue developerDisableUpdateVersionCheckEffective =
bindDeveloperTrue(developerDisableUpdateVersionCheck);
final BooleanProperty developerDisableGuiRestrictions =
typed(new SimpleBooleanProperty(false), Boolean.class);
final BooleanField developerDisableGuiRestrictionsField =
BooleanField.ofBooleanType(developerDisableGuiRestrictions).render(() -> new CustomToggleControl());
map(new SimpleBooleanProperty(false), "developerDisableGuiRestrictions", Boolean.class);
private final ObservableBooleanValue developerDisableGuiRestrictionsEffective =
bindDeveloperTrue(developerDisableGuiRestrictions);
final BooleanProperty developerShowHiddenProviders = typed(new SimpleBooleanProperty(false), Boolean.class);
final BooleanField developerShowHiddenProvidersField =
BooleanField.ofBooleanType(developerShowHiddenProviders).render(() -> new CustomToggleControl());
private final ObservableBooleanValue developerShowHiddenProvidersEffective =
bindDeveloperTrue(developerShowHiddenProviders);
final BooleanProperty developerShowHiddenEntries = typed(new SimpleBooleanProperty(false), Boolean.class);
final BooleanField developerShowHiddenEntriesField =
BooleanField.ofBooleanType(developerShowHiddenEntries).render(() -> new CustomToggleControl());
private final ObservableBooleanValue developerShowHiddenEntriesEffective =
bindDeveloperTrue(developerShowHiddenEntries);
final BooleanProperty developerDisableConnectorInstallationVersionCheck =
typed(new SimpleBooleanProperty(false), Boolean.class);
final BooleanField developerDisableConnectorInstallationVersionCheckField = BooleanField.ofBooleanType(
developerDisableConnectorInstallationVersionCheck)
.render(() -> new CustomToggleControl());
private final ObservableBooleanValue developerDisableConnectorInstallationVersionCheckEffective =
bindDeveloperTrue(developerDisableConnectorInstallationVersionCheck);
public ReadOnlyProperty<CloseBehaviour> closeBehaviour() {
return closeBehaviour;
}
@ -287,21 +268,27 @@ public class AppPrefs {
return customEditorCommand;
}
public void changeLock(SecretValue newLockPw) {
public void changeLock(InPlaceSecretValue newLockPw) {
if (newLockPw == null) {
lockCrypt.setValue("");
lockPassword.setValue(null);
lockCrypt.setValue(null);
if (DataStorage.get() != null) {
DataStorage.get().forceRewrite();
}
return;
}
lockPassword.setValue(newLockPw);
lockCrypt.setValue(new LockedSecretValue("xpipe".toCharArray()).getEncryptedValue());
lockCrypt.setValue(new PasswordLockSecretValue("xpipe".toCharArray()).getEncryptedValue());
if (DataStorage.get() != null) {
DataStorage.get().forceRewrite();
}
}
public boolean unlock(SecretValue lockPw) {
public boolean unlock(InPlaceSecretValue lockPw) {
lockPassword.setValue(lockPw);
var check = new LockedSecretValue("xpipe".toCharArray()).getEncryptedValue();
if (!check.equals(lockCrypt.get())) {
var check = PasswordLockSecretValue.builder().encryptedValue(lockCrypt.get()).build().getSecret();
if (!Arrays.equals(check, new char[] {'x', 'p', 'i', 'p', 'e'})) {
lockPassword.setValue(null);
return false;
} else {
@ -338,7 +325,9 @@ public class AppPrefs {
}
public ObservableValue<Boolean> developerMode() {
return effectiveDeveloperMode;
return System.getProperty(DEVELOPER_MODE_PROP) != null
? new SimpleBooleanProperty(Boolean.parseBoolean(System.getProperty(DEVELOPER_MODE_PROP)))
: developerMode;
}
public ObservableDoubleValue windowOpacity() {
@ -357,41 +346,43 @@ public class AppPrefs {
return developerDisableGuiRestrictionsEffective;
}
public ObservableBooleanValue developerDisableConnectorInstallationVersionCheck() {
return developerDisableConnectorInstallationVersionCheckEffective;
}
public ObservableBooleanValue developerShowHiddenProviders() {
return developerShowHiddenProvidersEffective;
}
public ObservableBooleanValue developerShowHiddenEntries() {
return developerShowHiddenEntriesEffective;
}
private AppPreferencesFx preferencesFx;
private boolean controlsSetup;
@Getter
private final Set<Field<?>> proRequiredSettings = new HashSet<>();
private final List<AppPrefsCategory> categories;
private final AppPrefsStorageHandler globalStorageHandler = new AppPrefsStorageHandler(
AppProperties.get().getDataDir().resolve("settings").resolve("preferences.json"));
private final AppPrefsStorageHandler vaultStorageHandler = new AppPrefsStorageHandler(
storageDirectory().getValue().resolve("preferences.json"));
private final Map<Mapping<?>, Comp<?>> customEntries = new LinkedHashMap<>();
@Getter
private final Property<AppPrefsCategory> selectedCategory;
private final PrefsHandler extensionHandler = new PrefsHandlerImpl();
private AppPrefs() {
try {
preferencesFx = createPreferences();
} catch (Exception e) {
ErrorEvent.fromThrowable(e).terminal(true).build().handle();
this.categories = List.of(new AboutCategory(), new SystemCategory(), new AppearanceCategory(),
new SyncCategory(), new VaultCategory(), new TerminalCategory(), new EditorCategory(), new ConnectionsCategory(), new SecurityCategory(),
new PasswordManagerCategory(), new TroubleshootCategory(), new DeveloperCategory());
var selected = AppCache.get("selectedPrefsCategory", Integer.class, () -> 0);
if (selected == null) {
selected = 0;
}
SimpleChangeListener.apply(languageInternal, val -> {
language.setValue(val);
});
this.selectedCategory = new SimpleObjectProperty<>(categories.get(selected >= 0 && selected < categories.size() ? selected : 0));
}
public static void init() {
INSTANCE = new AppPrefs();
INSTANCE.preferencesFx.loadSettings();
INSTANCE.initValues();
PrefsProvider.getAll().forEach(prov -> prov.init());
PrefsProvider.getAll().forEach(prov -> prov.addPrefs(INSTANCE.extensionHandler));
INSTANCE.load();
INSTANCE.encryptAllVaultData.addListener((observableValue, aBoolean, t1) -> {
if (DataStorage.get() != null) {
DataStorage.get().forceRewrite();
}
});
}
public static void setDefaults() {
INSTANCE.initDefaultValues();
PrefsProvider.getAll().forEach(prov -> prov.initDefaultValues());
}
public static void reset() {
@ -405,121 +396,73 @@ public class AppPrefs {
return INSTANCE;
}
// Storage directory
// =================
private <T> T typed(T o, Class<?> clazz) {
classMap.put(o, clazz);
@SuppressWarnings("unchecked")
private <T> T map(T o, String name, Class<?> clazz) {
mapping.add(new Mapping<T>(name, (Property<T>) o, (Class<T>) clazz));
return o;
}
private <T extends Field<?>> T editable(T o, ObservableBooleanValue v) {
o.editableProperty().bind(v);
@SuppressWarnings("unchecked")
private <T> T mapVaultSpecific(T o, String name, Class<?> clazz) {
mapping.add(new Mapping<T>(name, (Property<T>) o, (Class<T>) clazz, true));
return o;
}
public AppPreferencesFx createControls() {
if (!controlsSetup) {
preferencesFx.setupControls();
SimpleChangeListener.apply(languageInternal, val -> {
preferencesFx.translationServiceProperty().set(new QuietResourceBundleService());
});
controlsSetup = true;
}
return preferencesFx;
}
public <T> void setFromExternal(ReadOnlyProperty<T> prop, T newValue) {
public <T> void setFromExternal(ObservableValue<T> prop, T newValue) {
var writable = (Property<T>) prop;
PlatformThread.runLaterIfNeededBlocking(() -> {
writable.setValue(newValue);
save();
});
}
public <T> void setFromText(ReadOnlyProperty<T> prop, String newValue) {
var field = getFieldForEntry(prop);
if (field == null || !field.isEditable()) {
return;
}
field.userInputProperty().set(newValue);
if (!field.validate()) {
return;
}
field.persist();
save();
}
public void initValues() {
public void initDefaultValues() {
if (externalEditor.get() == null) {
ExternalEditorType.detectDefault();
}
if (terminalType.get() == null) {
terminalType.set(ExternalTerminalType.getDefault());
terminalType.set(ExternalTerminalType.determineDefault());
}
}
public Comp<?> getCustomComp(String id) {
return customEntries.entrySet().stream().filter(e -> e.getKey().getKey().equals(id)).findFirst().map(Map.Entry::getValue).orElseThrow();
}
public void load() {
for (Mapping<?> value : mapping) {
var def = value.getProperty().getValue();
AppPrefsStorageHandler handler = value.isVaultSpecific() ? vaultStorageHandler : globalStorageHandler;
var r = loadValue(handler, value);
// This can be used to facilitate backwards compatibility
// Overdose is not really needed as many moved properties have changed anyways
var isDefault = Objects.equals(r, def);
if (isDefault && value.isVaultSpecific()) {
loadValue(globalStorageHandler,value);
}
}
}
private <T> T loadValue(AppPrefsStorageHandler handler, Mapping<T> value) {
var val = handler.loadObject(value.getKey(), value.getValueClass(), value.getProperty().getValue());
value.getProperty().setValue(val);
return val;
}
public void save() {
preferencesFx.saveSettings();
for (Mapping<?> m : mapping) {
AppPrefsStorageHandler handler = m.isVaultSpecific() ? vaultStorageHandler : globalStorageHandler;
handler.updateObject(m.getKey(), m.getProperty().getValue());
}
vaultStorageHandler.save();
globalStorageHandler.save();
}
public void cancel() {
preferencesFx.discardChanges();
}
public Class<?> getSettingType(String breadcrumb) {
var s = getSetting(breadcrumb);
if (s == null) {
throw new IllegalStateException("Unknown breadcrumb " + breadcrumb);
}
var found = classMap.get(s.valueProperty());
if (found == null) {
throw new IllegalStateException("Unassigned type for " + breadcrumb);
}
return found;
}
private Setting<?, ?> getSetting(String breadcrumb) {
for (var c : preferencesFx.getCategories()) {
if (c.getGroups() == null) {
continue;
}
for (var g : c.getGroups()) {
for (var s : g.getSettings()) {
if (s.getBreadcrumb().equals(breadcrumb)) {
return s;
}
}
}
}
return null;
}
private DataField<?, ?, ?> getFieldForEntry(ReadOnlyProperty<?> property) {
for (var c : preferencesFx.getCategories()) {
if (c.getGroups() == null) {
continue;
}
for (var g : c.getGroups()) {
for (var s : g.getSettings()) {
if (s.valueProperty().equals(property)) {
return (DataField<?, ?, ?>) s.getElement();
}
}
}
}
return null;
}
public void selectCategory(int index) {
public void selectCategory(int selected) {
AppLayoutModel.get().selectSettings();
preferencesFx
.getNavigationPresenter()
.setSelectedCategory(preferencesFx.getCategories().get(index));
var index = selected >= 0 && selected < categories.size() ? selected : 0;
selectedCategory.setValue(categories.get(index));
}
public String passwordManagerString(String key) {
@ -533,175 +476,14 @@ public class AppPrefs {
return ApplicationHelper.replaceFileArgument(passwordManagerCommand.get(), "KEY", key);
}
@SneakyThrows
private AppPreferencesFx createPreferences() {
var ctr = Setting.class.getDeclaredConstructor(String.class, Element.class, Property.class);
ctr.setAccessible(true);
var terminalTest = ctr.newInstance(
null,
new LazyNodeElement<>(() -> new StackComp(
List.of(new ButtonComp(AppI18n.observable("test"), new FontIcon("mdi2p-play"), () -> {
save();
ThreadHelper.runFailableAsync(() -> {
var term = AppPrefs.get().terminalType().getValue();
if (term != null) {
TerminalHelper.open(
"Test",
new LocalStore().control().command("echo Test"));
}
});
})))
.padding(new Insets(15, 0, 0, 0))
.apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT))
.createRegion()),
null);
var editorTest = ctr.newInstance(
null,
new LazyNodeElement<>(() -> new StackComp(
List.of(new ButtonComp(AppI18n.observable("test"), new FontIcon("mdi2p-play"), () -> {
save();
ThreadHelper.runFailableAsync(() -> {
var editor =
AppPrefs.get().externalEditor().getValue();
if (editor != null) {
FileOpener.openReadOnlyString("Test");
}
});
})))
.padding(new Insets(15, 0, 0, 0))
.apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT))
.createRegion()),
null);
var about = ctr.newInstance(null, new LazyNodeElement<>(() -> new AboutComp().createRegion()), null);
var troubleshoot =
ctr.newInstance(null, new LazyNodeElement<>(() -> new TroubleshootComp().createRegion()), null);
var categories = new ArrayList<>(List.of(
Category.of("about", Group.of(about)),
Category.of(
"system",
Group.of(
"appBehaviour",
Setting.of("startupBehaviour", startupBehaviourControl, startupBehaviour),
Setting.of("closeBehaviour", closeBehaviourControl, closeBehaviour)),
Group.of(
"advanced",
Setting.of("developerMode", developerModeField, internalDeveloperMode)),
Group.of(
"updates",
Setting.of(
"automaticallyUpdate",
automaticallyCheckForUpdatesField,
automaticallyCheckForUpdates))),
new VaultCategory(this).create(),
Category.of(
"appearance",
Group.of(
"uiOptions",
Setting.of("theme", themeControl, theme),
Setting.of("performanceMode", BooleanField.ofBooleanType(performanceMode).render(() -> new CustomToggleControl()), performanceMode),
Setting.of("windowOpacity", windowOpacityField, windowOpacity),
Setting.of("useSystemFont", BooleanField.ofBooleanType(useSystemFontInternal).render(() -> new CustomToggleControl()), useSystemFontInternal),
Setting.of("tooltipDelay", tooltipDelayInternal, tooltipDelayMin, tooltipDelayMax),
Setting.of("language", languageControl, languageInternal)),
Group.of("windowOptions", Setting.of("saveWindowLocation", BooleanField.ofBooleanType(saveWindowLocation).render(() -> new CustomToggleControl()), saveWindowLocation))),
Category.of(
"connections",
Group.of(
Setting.of(
"connectionTimeout",
connectionTimeOut,
5,
50))),
new PasswordCategory(this).create(),
Category.of(
"editor",
Group.of(
Setting.of("editorProgram", externalEditorControl, externalEditor),
editorTest,
Setting.of("customEditorCommand", customEditorCommandControl, customEditorCommand)
.applyVisibility(VisibilityProperty.of(
externalEditor.isEqualTo(ExternalEditorType.CUSTOM))),
Setting.of(
"editorReloadTimeout",
editorReloadTimeout,
editorReloadTimeoutMin,
editorReloadTimeoutMax),
Setting.of("preferEditorTabs", preferEditorTabsField, preferEditorTabs))),
Category.of(
"terminal",
Group.of(
Setting.of("terminalProgram", terminalTypeControl, terminalType),
terminalTest,
Setting.of("customTerminalCommand", customTerminalCommandControl, customTerminalCommand)
.applyVisibility(VisibilityProperty.of(
terminalType.isEqualTo(ExternalTerminalType.CUSTOM))),
Setting.of("preferTerminalTabs", preferTerminalTabsField, preferTerminalTabs)
//Setting.of("enableFastTerminalStartup", enableFastTerminalStartupField, enableFastTerminalStartup)
)),
new DeveloperCategory(this).create(),
Category.of("troubleshoot", Group.of(troubleshoot))));
categories.get(categories.size() - 2).setVisibilityProperty(VisibilityProperty.of(developerMode()));
var handler = new PrefsHandlerImpl(categories);
PrefsProvider.getAll().forEach(prov -> prov.addPrefs(handler));
var cats = handler.getCategories().toArray(Category[]::new);
return AppPreferencesFx.of(cats);
}
static Group group(String name, Setting<?, ?>... settings) {
return Group.of(
name, Arrays.stream(settings).filter(setting -> setting != null).toArray(Setting[]::new));
}
@Getter
private class PrefsHandlerImpl implements PrefsHandler {
private final List<Category> categories;
private PrefsHandlerImpl(List<Category> categories) {
this.categories = categories;
}
@Override
public void addSetting(List<String> category, String group, Setting<?, ?> setting, Class<?> cl) {
classMap.put(setting.valueProperty(), cl);
var foundCat = categories.stream()
.filter(c -> c.getDescription().equals(category.get(0)))
.findAny();
var usedCat = foundCat.orElse(null);
var index = categories.indexOf(usedCat);
if (foundCat.isEmpty()) {
usedCat = Category.of(category.get(0), Group.of());
categories.add(usedCat);
}
var foundGroup = usedCat.getGroups().stream()
.filter(g ->
g.getDescription() != null && g.getDescription().equals(group))
.findAny();
var usedGroup = foundGroup.orElse(null);
if (foundGroup.isEmpty()) {
categories.remove(usedCat);
usedGroup = Group.of(group);
var modCatGroups = new ArrayList<>(usedCat.getGroups());
modCatGroups.add(usedGroup);
usedCat = Category.of(usedCat.getDescription(), modCatGroups.toArray(Group[]::new));
}
var modGroupSettings = new ArrayList<>(usedGroup.getSettings());
modGroupSettings.add(setting);
var newGroup = Group.of(usedGroup.getDescription(), modGroupSettings.toArray(Setting[]::new));
var modCatGroups = new ArrayList<>(usedCat.getGroups());
modCatGroups.removeIf(
g -> g.getDescription() != null && g.getDescription().equals(group));
modCatGroups.add(newGroup);
var newCategory = Category.of(usedCat.getDescription(), modCatGroups.toArray(Group[]::new));
categories.remove(usedCat);
categories.add(index, newCategory);
}
public <T> void addSetting(String id, Class<T> c, Property<T> property, Comp<?> comp) {
var m = new Mapping<T>(id, property, c);
customEntries.put(m,comp);
mapping.add(m);
}
}
}

View file

@ -1,26 +1,10 @@
package io.xpipe.app.prefs;
import com.dlsc.formsfx.model.structure.Element;
import com.dlsc.preferencesfx.model.Category;
import com.dlsc.preferencesfx.model.Setting;
import io.xpipe.app.fxcomps.Comp;
import javafx.beans.property.Property;
import lombok.SneakyThrows;
public abstract class AppPrefsCategory {
@SneakyThrows
public static Setting<?,?> lazyNode(String name, Comp<?> comp, Property<?> property) {
var ctr = Setting.class.getDeclaredConstructor(String.class, Element.class, Property.class);
ctr.setAccessible(true);
return ctr.newInstance(name, new LazyNodeElement<>(() -> comp.createRegion()), property);
}
protected abstract String getId();
protected final AppPrefs prefs;
public AppPrefsCategory(AppPrefs prefs) {
this.prefs = prefs;
}
protected abstract Category create();
protected abstract Comp<?> create();
}

View file

@ -0,0 +1,52 @@
package io.xpipe.app.prefs;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import javafx.geometry.Insets;
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.StackPane;
import java.util.stream.Collectors;
public class AppPrefsComp extends SimpleComp {
@Override
protected Region createSimple() {
var map = AppPrefs.get().getCategories().stream()
.collect(Collectors.toMap(appPrefsCategory -> appPrefsCategory, appPrefsCategory -> {
return appPrefsCategory.create().maxWidth(700).padding(new Insets(40, 40, 20, 40)).styleClass("prefs-container").createRegion();
}));
var pfxSp = new ScrollPane();
SimpleChangeListener.apply(AppPrefs.get().getSelectedCategory(), val -> {
PlatformThread.runLaterIfNeeded(() -> {
pfxSp.setContent(map.get(val));
});
});
AppPrefs.get().getSelectedCategory().addListener((observable, oldValue, newValue) -> {
pfxSp.setVvalue(0);
});
pfxSp.setFitToWidth(true);
var pfxLimit = new StackPane(pfxSp);
pfxLimit.setAlignment(Pos.TOP_LEFT);
var sidebar = new AppPrefsSidebarComp().createRegion();
sidebar.setMinWidth(350);
sidebar.setPrefWidth(350);
sidebar.setMaxWidth(350);
var split = new HBox(sidebar, pfxLimit);
HBox.setHgrow(pfxLimit, Priority.ALWAYS);
split.setFillHeight(true);
split.getStyleClass().add("prefs");
var stack = new StackPane(split);
stack.setPickOnBounds(false);
AppFont.medium(stack);
return stack;
}
}

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