mirror of
https://github.com/xpipe-io/xpipe.git
synced 2024-11-25 09:00:26 +00:00
Squash branch commits
This commit is contained in:
parent
ce45ff9ec6
commit
7c7fa28190
317 changed files with 6133 additions and 7951 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
14
api/src/test/java/io/xpipe/api/test/StartupTest.java
Normal file
14
api/src/test/java/io/xpipe/api/test/StartupTest.java
Normal 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();
|
||||
}
|
||||
}
|
149
app/build.gradle
149
app/build.gradle
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
'''
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
'''
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
'''
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
'''
|
||||
}
|
||||
}
|
||||
}
|
4
app/src/localTest/java/module-info.java
Normal file
4
app/src/localTest/java/module-info.java
Normal file
|
@ -0,0 +1,4 @@
|
|||
open module io.xpipe.app.localTest {
|
||||
requires org.junit.jupiter.api;
|
||||
requires io.xpipe.app;
|
||||
}
|
14
app/src/localTest/java/test/Test.java
Normal file
14
app/src/localTest/java/test/Test.java
Normal 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());
|
||||
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
89
app/src/main/java/io/xpipe/app/comp/base/DialogComp.java
Normal file
89
app/src/main/java/io/xpipe/app/comp/base/DialogComp.java
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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)) {
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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) {}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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) -> {
|
||||
|
|
|
@ -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(() -> {
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()))) {
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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 ...");
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 -> {
|
||||
|
|
|
@ -49,7 +49,7 @@ public class TerminalErrorHandler extends GuiErrorHandlerBase implements ErrorHa
|
|||
return;
|
||||
}
|
||||
|
||||
if (OperationMode.isInStartup()) {
|
||||
if (OperationMode.isInStartup() && !AppProperties.get().isDevelopmentEnvironment()) {
|
||||
handleProbableUpdate();
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) -> {
|
||||
|
|
|
@ -24,7 +24,7 @@ public abstract class LauncherInput {
|
|||
return;
|
||||
}
|
||||
|
||||
TrackEvent.withDebug("launcher", "Handling arguments")
|
||||
TrackEvent.withDebug("Handling arguments")
|
||||
.elements(arguments)
|
||||
.handle();
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
52
app/src/main/java/io/xpipe/app/prefs/AppPrefsComp.java
Normal file
52
app/src/main/java/io/xpipe/app/prefs/AppPrefsComp.java
Normal 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
Loading…
Reference in a new issue