Merge main repository

This commit is contained in:
crschnick 2023-01-27 02:34:46 +00:00
parent 20a334912a
commit 06405de396
614 changed files with 271738 additions and 457 deletions

2
.gitattributes vendored
View file

@ -1,3 +1,5 @@
* text=auto
*.sh text eol=lf
*.bat text eol=crlf
*.png binary
*.xcf binary

View file

@ -1,28 +0,0 @@
name: Build
on:
push:
branches:
- master
jobs:
build:
runs-on: ubuntu-20.04
steps:
- name: Git checkout
uses: actions/checkout@v2
with:
submodules: 'true'
- name: Set up GraalVM
uses: graalvm/setup-graalvm@v1
with:
version: '22.3.0'
java-version: '19'
github-token: ${{ secrets.XPIPE_GITHUB_TOKEN }}
- name: Verify Gradle Wrapper
uses: gradle/wrapper-validation-action@v1
- name: Execute build
run: ./gradlew clean build

View file

@ -1,39 +0,0 @@
name: Publish
on:
push:
branches:
- master
jobs:
publish:
runs-on: ubuntu-20.04
steps:
- name: Git checkout
uses: actions/checkout@v2
with:
submodules: 'true'
- name: Set up GraalVM
uses: graalvm/setup-graalvm@v1
with:
version: '22.3.0'
java-version: '19'
github-token: ${{ secrets.XPIPE_GITHUB_TOKEN }}
- name: Verify Gradle Wrapper
uses: gradle/wrapper-validation-action@v1
- name: Publish
run: ./gradlew publish
env:
GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }}
GPG_KEY: ${{ secrets.GPG_KEY }}
SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }}
SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
- name: JReleaser
run: ./gradlew jreleaserRelease
env:
XPIPE_GITHUB_TOKEN: ${{ secrets.XPIPE_GITHUB_TOKEN }}
XPIPE_DISCORD_WEBHOOK: ${{ secrets.XPIPE_DISCORD_WEBHOOK }}

14
.gitignore vendored
View file

@ -1,11 +1,15 @@
.gradle/
build/
.idea
local/
local_test/
local_stage/
lib/
dev.properties
extensions.txt
local/
dev_storage
local*/
.vs
.vscode
bin
obj
out
bin
.DS_Store
ComponentsGenerated.wxs

167
README.md
View file

@ -1,37 +1,162 @@
[![Build Status](https://github.com/xpipe-io/xpipe_java/actions/workflows/build.yml/badge.svg)](https://github.com/xpipe-io/xpipe_java/actions/workflows/build.yml)
[![Publish Status](https://github.com/xpipe-io/xpipe_java/actions/workflows/publish.yml/badge.svg)](https://github.com/xpipe-io/xpipe_java/actions/workflows/publish.yml)
<img src="https://user-images.githubusercontent.com/72509152/213873342-7638e830-8a95-4b5d-ad3e-5a9a0b4bf538.png" alt="drawing" width="300"/>
## X-Pipe Java
As the name suggests, X-Pipe (short for eXtended Pipe) has
the goal of improving on the established concept of pipes.
As with normal pipes, the main goal of X-Pipe essentially is the transfer of data from producers to consumers.
It focuses on the following three ideas:
The fundamental components of the [X-Pipe project](https://xpipe.io).
This repository contains the following four modules:
- Could we support connections to any remote system with pipes instead of limiting yourself to your local system?
If so, we should focus on supporting already existing tools
to establish remote connections instead of reinventing the wheel?
- Core - Shared core classes of the X-Pipe Java API, X-Pipe extensions, and the X-Pipe daemon implementation
- API - The API that can be used to interact with X-Pipe from any JVM-based language10
- Beacon - The X-Pipe beacon component is responsible for handling all communications between the X-Pipe daemon
- Could we support more than just transferring raw bytes and text?
Why not work on a higher level of abstraction instead,
which would allow for a connection between producers and consumers
that work on the same type of data, e.g. a table, even though the underlying formats are different.
- Could we make the process as user friendly as possible?
Most tools in that space grow to be incredible complex and make it very difficult for users to get started.
The goal is too provide a use a friendly alternative that almost anyone can use instantly.
X-Pipe consists out of two main components that achieve these goals:
- The Connection Explorer provides the ability to flexibly connect to any remote system
- The Data Explorer then builds on top of it to allow you to smartly work with all kinds of data
Note that this project is still in early development!
## Connection Explorer
The connection explorer allows you to connect to, manage, and interact with all kinds of remote systems.
<img src="https://user-images.githubusercontent.com/72509152/213240153-3f742f03-1289-44c3-bf4d-626d9886ffcf.png" alt="drawing" height="450"/>
It comes with the following main features:
#### Ultra Flexible Connector
- Can connect to standard servers, database servers, and more
- Supports established protocols (e.g. SSH and more) plus any custom connection methods that work through the command-line
- Is able to integrate any kind of proxies into the connection process, even ones with different protocols
#### Instant launch for remote shells and commands
- Automatically login into a shell in your favourite terminal with one click (no need to fill password prompts, etc.)
- Works for all kinds of shells. This includes command shells (e.g. bash, PowerShell, cmd, etc.) and database shells (e.g. PSQL Shell)
- Comes with integrations for all commonly used terminals in Windows and Linux
- Exclusively uses established CLI tools and therefore works out of the box on most systems and doesn't require any additional setup
#### Connection Manager
- Easily create and manage all kinds of remote connections
- Securely stores all information exclusively on your computer and encrypts all secret information
- Allows you to share connections and their information to any other trusted system in your network
## Data Explorer
Building on top of the connection explorer, the data explorer
allows you to manage and work with all kinds of data sources:
<img src="https://user-images.githubusercontent.com/72509152/213240736-7a27fb3c-e8c3-4c92-bcea-2a782e53dc31.png" alt="drawing" height="450"/>
#### Work with your data on a higher level
- X-Pipe utilizes structures of data instead of the raw data itself, allowing for
a higher level workflow that is independent of the underlying data format
- Save time when adding data sources by making use of the advanced
auto detection feature of X-Pipe where you don't have to worry about encodings, format configurations, and more
- Easily convert between different data representations
#### Integrate X-Pipe with your favorite tools and workflows
- Easily import and export all kinds of data formats and technologies
- Access data sources from the commandline with the X-Pipe CLI or
your favorite programming languages using the X-Pipe API
- Connect select third party applications directly to X-Pipe through extensions
### Summary
Even though X-Pipe comes with wide variety of features and components, it essentially still only focuses on one goal:
*To get your data from A to B in the easiest way possible while also preserving
compatibility through intermediation.
For that, you can use the medium you like the most, whether that is a GUI, a CLI, or an API.*
Essentially, X-Pipe aims to make the transfer process as quick as possible
so you can spend more time actually working with your
data instead of figuring out how to transfer it.
X-Pipe can therefore be a massive timesaver for
anyone who interacts with a wide range of data.
In case this sounds interesting to you, take a look at the
complete installation instructions and the user guide that can be
found in the [X-Pipe Documentation](https://docs.xpipe.io/guide/installation.html).
## Repository Structure
The following for modules make up the X-Pipe API and a licensed under the MIT license:
- [core](core) - Shared core classes of the X-Pipe Java API, X-Pipe extensions, and the X-Pipe daemon implementation
- [API](api) - The API that can be used to interact with X-Pipe from any JVM-based language.
For setup instructions, see the [X-Pipe Java API Usage](https://xpipe-io.readthedocs.io/en/latest/dev/api/java.html) section.
- [beacon](beacon) - The X-Pipe beacon component is responsible for handling all communications between the X-Pipe daemon
and the client applications, for example the various programming language APIs and the CLI
- Extension - An API to create all different kinds of extensions for the X-Pipe platform
- [extension](extension) - An API to create all different kinds of extensions for the X-Pipe platform
For setup instructions, see the [X-Pipe extension development](https://xpipe-io.readthedocs.io/en/latest/dev/extensions/index.html) section.
## Installation / Usage
The other modules make up the X-Pipe implementation and are licensed under GPL:
- [app](app) - Contains the X-Pipe daemon implementation and the X-Pipe desktop application code
- [cli](cli) - The X-Pipe CLI implementation, a GraalVM native image application
- [dist](dist) - Tools to create a distributable package of X-Pipe
- [ext](ext) - Available X-Pipe extensions. Note that essentially every feature is implemented as an extension
The *core* and *extension* modules are used in X-Pipe extension development.
For setup instructions, see the [X-Pipe extension development](https://xpipe-io.readthedocs.io/en/latest/dev/extensions/index.html) section.
The *beacon* module handles all communication and serves as a
reference when implementing the communication of an API or program that interacts with the X-Pipe daemon.
## Development
The *api* module serves as a reference implementation for other potential X-Pipe APIs
and can also be used to access X-Pipe functionalities from your Java programs.
For setup instructions, see the [X-Pipe Java API Usage](https://xpipe-io.readthedocs.io/en/latest/dev/api/java/index.html) section.
Any contribution is welcomed!
There are no real formal contribution guidelines right now, they will maybe come later.
## Development Notes
### Modularity
All X-Pipe components target [JDK 17](https://openjdk.java.net/projects/jdk/17/) and make full use of the Java Module System (JPMS).
All X-Pipe components target [JDK 19](https://openjdk.java.net/projects/jdk/19/) and make full use of the Java Module System (JPMS).
All components are modularized, including all their dependencies.
As the CLI utilizes the native image capability of [GraalVM](https://github.com/graalvm/graalvm-ce-builds/releases/tag/vm-22.3.0), it is recommended to use GraalVM with Java 19 support.
In case a dependency is (sadly) not modularized yet, module information is manually added using [moditect](https://github.com/moditect/moditect-gradle-plugin).
These dependency generation rules are accumulated in the [X-Pipe dependencies](https://github.com/xpipe-io/xpipe_java_deps)
repository, which is shared between all components and integrated as a git submodule.
Further, note that as this is a pretty complicated Java project that fully utilizes modularity,
many IDEs still have problems building this project properly.
For example, you can't build this project in eclipse or vscode as it will complain about missing modules.
The tested and recommended IDE is intellij.
### Building and Running
You can use the gradle wrapper to build and run the project:
- `gradlew app:run` will run the desktop application. You can set various useful properties in `app/build.gradle`
- `gradlew builtCli` will create a native image for the CLI application
- `gradlew dist` will create a distributable production version in `dist/build/dist/base`.
To include this CLI executable in this build, make sure to run `gradlew builtCli` first
- You can also run the CLI application in development mode with something like `gradlew :cli:clean :cli:run --args="daemon start"`.
Note here that you should always clean the CLI project first, as the native image plugin is a little buggy in that regard.
- `gradlew <project>:test` will run the tests of the specified project.
Some unit tests depend on a connection to an X-Pipe daemon to properly function.
To launch the installed daemon, it is required that you either have X-Pipe
installed or have set the `XPIPE_HOME` environment variable in case you are using a portable version.
You are also able to properly debug the built production application through two different methods:
- The `app/scripts/xpiped_debug` script will launch the application in debug mode and with a console attached to it
- The `app/scripts/xpiped_debug_attach` script attaches a debugger with the help of [AttachMe](https://plugins.jetbrains.com/plugin/13263-attachme).
Just make sure that the attachme process is running within IntelliJ, and the debugger should launch automatically once you start up the application.
Note that when any unit test is run using a debugger, the X-Pipe daemon process that is started will also attempt
to connect to that debugger through [AttachMe](https://plugins.jetbrains.com/plugin/13263-attachme) as well.

View file

@ -13,7 +13,7 @@ The X-Pipe API for Java allows you to use most of the X-Pipe functionality from
Either install the [maven dependency](https://maven-badges.herokuapp.com/maven-central/io.xpipe/xpipe-api) from Maven Central
using your favourite build tool or alternatively download the `xpipe-api.jar`, `xpipe-core.jar`, and `xpipe-beacon.jar`
from the [releases page](https://github.com/xpipe-io/xpipe_java/releases/latest) and add them to the classpath.
from the [releases page](https://github.com/xpipe-io/xpipe/releases/latest) and add them to the classpath.
## Usage

View file

@ -5,13 +5,13 @@ plugins {
id "org.moditect.gradleplugin" version "1.0.0-rc3"
}
apply from: "$projectDir/../gradle_scripts/java.gradle"
apply from: "$projectDir/../gradle_scripts/junit.gradle"
apply from: "$rootDir/gradle/gradle_scripts/java.gradle"
apply from: "$rootDir/gradle/gradle_scripts/junit.gradle"
System.setProperty('excludeExtensionLibrary', 'true')
apply from: "$projectDir/../gradle_scripts/extension_test.gradle"
apply from: "$rootDir/gradle/gradle_scripts/extension_test.gradle"
version = file('../misc/version').text
version = file("$rootDir/dist/version").text
group = 'io.xpipe'
archivesBaseName = 'xpipe-api'
@ -33,6 +33,11 @@ configurations {
testImplementation.extendsFrom(dep)
}
task dist(type: Copy) {
from jar.archiveFile
into "${project(':dist').buildDir}/dist/libraries"
}
apply from: 'publish.gradle'
apply from: "$projectDir/../gradle_scripts/publish-base.gradle"
apply from: "$rootDir/gradle/gradle_scripts/publish-base.gradle"

View file

@ -15,11 +15,11 @@ publishing {
pom {
name = 'X-Pipe Java API'
description = 'Contains everything necessary to interact with X-Pipe from Java applications.'
url = 'https://github.com/xpipe-io/xpipe_java/api'
url = 'https://github.com/xpipe-io/xpipe/api'
licenses {
license {
name = 'The MIT License (MIT)'
url = 'https://github.com/xpipe-io/xpipe_java/LICENSE.md'
url = 'https://github.com/xpipe-io/xpipe/LICENSE.md'
}
}
developers {
@ -30,9 +30,9 @@ publishing {
}
}
scm {
connection = 'scm:git:git://github.com/xpipe-io/xpipe_java.git'
developerConnection = 'scm:git:ssh://github.com/xpipe-io/xpipe_java.git'
url = 'https://github.com/xpipe-io/xpipe_java'
connection = 'scm:git:git://github.com/xpipe-io/xpipe.git'
developerConnection = 'scm:git:ssh://github.com/xpipe-io/xpipe.git'
url = 'https://github.com/xpipe-io/xpipe'
}
}
}

194
app/build.gradle Normal file
View file

@ -0,0 +1,194 @@
import java.util.stream.Collectors
plugins {
id 'application'
id "org.moditect.gradleplugin" version "1.0.0-rc3"
}
repositories {
mavenCentral()
}
def appVersion = file('../dist/version').text
def apiVersion = project(':api').version
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: "$projectDir/gradle_scripts/fxtrayicon.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"
configurations {
implementation.extendsFrom(dep)
}
dependencies {
implementation project(':core')
implementation project(':extension')
implementation project(':beacon')
implementation 'net.java.dev.jna:jna-jpms:5.12.1'
implementation 'net.java.dev.jna:jna-platform-jpms:5.12.1'
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.13.0"
implementation group: 'com.fasterxml.jackson.module', name: 'jackson-module-parameter-names', version: "2.13.0"
implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.13.0"
implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jdk8', version: "2.13.0"
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 name: 'preferencesfx-core-lazy-11.11.0'
implementation name: 'formsfx-core-lazy-11.5.0'
implementation group: 'org.slf4j', name: 'slf4j-api', version: '2.0.0'
implementation 'io.xpipe:modulefs:0.1.4'
implementation 'com.jfoenix:jfoenix:9.0.10'
implementation 'org.controlsfx:controlsfx:11.1.1'
implementation 'net.synedra:validatorfx:0.3.1'
}
apply from: "$rootDir/gradle/gradle_scripts/junit.gradle"
sourceSets {
main {
output.resourcesDir("$buildDir/classes/java/main")
}
}
dependencies {
testImplementation project(':api')
testImplementation project(':core')
testImplementation project(':extension')
}
Arrays.stream(file("$rootDir/ext").list())
.map(l -> project(":$l")).forEach(p -> {
dependencies {
testCompileOnly p
}
})
List<String> 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.extension",
"--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.app",
"--add-opens", "com.dustinredmond.fxtrayicon/com.dustinredmond.fxtrayicon=io.xpipe.app",
"--add-opens", "net.synedra.validatorfx/net.synedra.validatorfx=io.xpipe.extension",
"-Xmx8g",
"--enable-preview",
// "-XX:+ExitOnOutOfMemoryError",
"-Dfile.encoding=UTF-8",
"-Dvisualvm.display.name=X-Pipe"
]
def extensionDirList = Arrays.stream(file("$rootDir/ext").list())
.map(l -> project(":$l").buildDir.toString() + "/libs").collect(Collectors.joining(File.pathSeparator));
test {
jvmArgs += jvmRunArgs
systemProperty 'io.xpipe.app.mode', 'background'
systemProperty 'io.xpipe.app.dataDir', "$projectDir/local_test/"
systemProperty 'io.xpipe.app.writeLogs', "false"
systemProperty 'io.xpipe.app.writeSysOut', "true"
systemProperty 'io.xpipe.app.developerMode', "true"
systemProperty 'io.xpipe.app.logLevel', "trace"
//systemProperty "io.xpipe.beacon.port", "21722"
systemProperty "io.xpipe.app.extensions", extensionDirList
}
def extensionJarDepList = Arrays.stream(file("$rootDir/ext").list())
.map(l -> project(":$l").getTasksByName('jar', true)).toList();
jar {
finalizedBy(extensionJarDepList)
}
application {
mainModule = 'io.xpipe.app'
mainClass = 'io.xpipe.app.Main'
applicationDefaultJvmArgs = jvmRunArgs
}
run {
systemProperty 'io.xpipe.app.mode', 'gui'
systemProperty 'io.xpipe.app.dataDir', "$projectDir/local3/"
systemProperty 'io.xpipe.app.writeLogs', "true"
systemProperty 'io.xpipe.app.writeSysOut', "true"
systemProperty 'io.xpipe.app.developerMode', "true"
systemProperty 'io.xpipe.app.logLevel', "trace"
systemProperty "io.xpipe.beacon.port", "21724"
// systemProperty "io.xpipe.beacon.printMessages", "true"
systemProperty "io.xpipe.app.extensions", extensionDirList
// systemProperty 'io.xpipe.app.debugPlatform', "true"
// systemProperty "io.xpipe.beacon.localProxy", "true"
systemProperties System.getProperties()
systemProperty 'java.library.path', "./lib"
}
task runAttachedDebugger(type: JavaExec) {
classpath = run.classpath
mainModule = 'io.xpipe.app'
mainClass = 'io.xpipe.app.Main'
modularity.inferModulePath = true
jvmArgs += jvmRunArgs
jvmArgs += List.of(
"-javaagent:${System.getProperty("user.home")}/.attachme/attachme-agent-1.2.1.jar=port:7857,host:localhost",
"-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=127.0.0.1:0"
)
systemProperties run.systemProperties
}
task writeBuildProperties(type: DefaultTask) {
doLast {
def resourcesDir = new File(sourceSets.main.output.resourcesDir, "io/xpipe/app/resources")
resourcesDir.mkdirs()
def contents = "version=$appVersion\n" +
"build=$appVersion-${new Date().format('yyyyMMddHHmm')}\n" +
"apiVersion=$apiVersion\n"
new File(resourcesDir, "app.properties").text = contents
}
}
processResources.finalizedBy(writeBuildProperties)
task writeLicenses(type: DefaultTask) {
doLast {
def resourcesDir = new File(sourceSets.main.output.resourcesDir, "io/xpipe/app/resources/third-party")
resourcesDir.mkdirs()
copy {
from "$rootDir/dist/licenses"
into resourcesDir
}
}
}
processResources.finalizedBy(writeLicenses)
distTar {
enabled = false;
}
distZip {
enabled = false;
}

View file

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

View file

@ -0,0 +1,24 @@
dependencies {
implementation files("$buildDir/generated-modules/FXTrayIcon-3.1.2.jar")
}
addDependenciesModuleInfo {
overwriteExistingFiles = true
jdepsExtraArgs = ['-q']
outputDirectory = file("$buildDir/generated-modules")
modules {
module {
artifact 'com.dustinredmond.fxtrayicon:FXTrayIcon:3.1.2'
moduleInfoSource = '''
module com.dustinredmond.fxtrayicon {
exports com.dustinredmond.fxtrayicon;
exports com.dustinredmond.fxtrayicon.annotations;
requires transitive javafx.controls;
requires transitive javafx.base;
requires transitive java.desktop;
}
'''
}
}
}

View file

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

View file

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

View file

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

View file

@ -0,0 +1,17 @@
package io.xpipe.app;
import io.xpipe.app.core.AppProperties;
import io.xpipe.app.core.mode.OperationMode;
public class Main {
public static void main(String[] args) {
if (args.length == 1 && args[0].equals("version")) {
AppProperties.init();
System.out.println(AppProperties.get().getVersion());
return;
}
OperationMode.init(args);
}
}

View file

@ -0,0 +1,100 @@
package io.xpipe.app.comp;
import io.xpipe.app.comp.about.AboutTabComp;
import io.xpipe.app.comp.base.SideMenuBarComp;
import io.xpipe.app.comp.storage.collection.SourceCollectionLayoutComp;
import io.xpipe.app.comp.storage.store.StoreLayoutComp;
import io.xpipe.app.core.AppActionDetector;
import io.xpipe.app.core.AppCache;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppProperties;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.extension.I18n;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.CompStructure;
import io.xpipe.extension.fxcomps.SimpleCompStructure;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.layout.BorderPane;
import java.util.ArrayList;
import java.util.List;
public class AppLayoutComp extends Comp<CompStructure<BorderPane>> {
private final List<SideMenuBarComp.Entry> entries;
private final Property<SideMenuBarComp.Entry> selected;
public AppLayoutComp() {
var firstTime = AppCache.get("firstTimeLayout", Boolean.class, () -> true);
AppCache.update("firstTimeLayout", false);
entries = createEntryList();
selected = new SimpleObjectProperty<>(entries.get(0));
shortcut(new KeyCodeCombination(KeyCode.V, KeyCombination.SHORTCUT_DOWN), structure -> {
AppActionDetector.detectOnPaste();
});
}
private List<SideMenuBarComp.Entry> createEntryList() {
var l = new ArrayList<>(List.of(
new SideMenuBarComp.Entry(
I18n.observable("connections"), "mdi2c-connection", new StoreLayoutComp()),
new SideMenuBarComp.Entry(I18n.observable("data"), "mdsal-dvr", new SourceCollectionLayoutComp()),
new SideMenuBarComp.Entry(
I18n.observable("settings"), "mdsmz-miscellaneous_services", new PrefsComp(this)),
// new SideMenuBarComp.Entry(I18n.observable("help"), "mdi2b-book-open-variant", new
// StorageLayoutComp()),
// new SideMenuBarComp.Entry(I18n.observable("account"), "mdi2a-account", new StorageLayoutComp()),
new SideMenuBarComp.Entry(I18n.observable("about"), "mdi2p-package-variant", new AboutTabComp())));
if (AppProperties.get().isDeveloperMode()) {
// l.add(new SideMenuBarComp.Entry(I18n.observable("developer"), "mdi2b-book-open-variant", new DeveloperTabComp()));
}
// l.add(new SideMenuBarComp.Entry(I18n.observable("abc"), "mdi2b-book-open-variant", Comp.of(() -> {
// var fi = new FontIcon("mdsal-dvr");
// fi.setIconSize(30);
// fi.setIconColor(Color.valueOf("#111C"));
// JfxHelper.addEffect(fi);
// return new StackPane(fi);
// })));
return l;
}
@Override
public CompStructure<BorderPane> createBase() {
var pane = new BorderPane();
var sidebar = new SideMenuBarComp(selected, entries);
pane.setCenter(selected.getValue().comp().createRegion());
pane.setRight(sidebar.createRegion());
selected.addListener((c, o, n) -> {
if (o != null && o.equals(entries.get(2))) {
AppPrefs.get().save();
}
var r = selected.getValue().comp().createRegion();
pane.setCenter(r);
});
pane.setCenter(selected.getValue().comp().createRegion());
pane.setPrefWidth(1200);
pane.setPrefHeight(700);
AppFont.normal(pane);
return new SimpleCompStructure<>(pane);
}
public List<SideMenuBarComp.Entry> getEntries() {
return entries;
}
public SideMenuBarComp.Entry getSelected() {
return selected.getValue();
}
public Property<SideMenuBarComp.Entry> selectedProperty() {
return selected;
}
}

View file

@ -0,0 +1,54 @@
package io.xpipe.app.comp;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.extension.I18n;
import io.xpipe.extension.event.ErrorEvent;
import io.xpipe.extension.fxcomps.SimpleComp;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import java.nio.file.Path;
public class DeveloperTabComp extends SimpleComp {
@Override
protected Region createSimple() {
var button = new ButtonComp(I18n.observable("Throw exception"), null, () -> {
throw new IllegalStateException();
});
var button2 = new ButtonComp(I18n.observable("Throw exception with file"), null, () -> {
try {
throw new IllegalStateException();
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex)
.attachment(Path.of("extensions.txt"))
.build()
.handle();
}
});
var button3 = new ButtonComp(I18n.observable("Exit"), null, () -> {
System.exit(0);
});
var button4 = new ButtonComp(I18n.observable("Throw terminal exception"), null, () -> {
try {
throw new IllegalStateException();
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).terminal(true).build().handle();
}
});
var button5 = new ButtonComp(I18n.observable("Operation mode null"), null, OperationMode::close);
var box = new HBox(
button.createRegion(),
button2.createRegion(),
button3.createRegion(),
button4.createRegion(),
button5.createRegion());
return box;
}
}

View file

@ -0,0 +1,74 @@
package io.xpipe.app.comp;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.prefs.ClearCacheAlert;
import io.xpipe.extension.I18n;
import io.xpipe.extension.fxcomps.SimpleComp;
import javafx.beans.binding.Bindings;
import javafx.geometry.Pos;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import org.controlsfx.control.MasterDetailPane;
public class PrefsComp extends SimpleComp {
private final AppLayoutComp layout;
public PrefsComp(AppLayoutComp layout) {
this.layout = layout;
}
@Override
protected Region createSimple() {
return createButtonOverlay();
}
private Region createButtonOverlay() {
var pfx = AppPrefs.get().createControls().getView();
pfx.getStyleClass().add("prefs");
MasterDetailPane p = (MasterDetailPane) pfx.getCenter();
p.dividerPositionProperty().setValue(0.27);
var cancel = new ButtonComp(I18n.observable("cancel"), null, () -> {
AppPrefs.get().cancel();
layout.selectedProperty().setValue(layout.getEntries().get(0));
})
.createRegion();
var apply = new ButtonComp(I18n.observable("apply"), null, () -> {
AppPrefs.get().save();
layout.selectedProperty().setValue(layout.getEntries().get(0));
})
.createRegion();
var maxWidth = Bindings.max(cancel.widthProperty(), apply.widthProperty());
cancel.minWidthProperty().bind(maxWidth);
apply.minWidthProperty().bind(maxWidth);
var rightButtons = new HBox(apply, cancel);
rightButtons.setSpacing(8);
var rightPane = new AnchorPane(rightButtons);
rightPane.setPickOnBounds(false);
AnchorPane.setBottomAnchor(rightButtons, 15.0);
AnchorPane.setRightAnchor(rightButtons, 55.0);
var clearCaches = new ButtonComp(I18n.observable("clearCaches"), null, ClearCacheAlert::show).createRegion();
// var reload = new ButtonComp(I18n.observable("reload"), null, () -> OperationMode.reload()).createRegion();
var leftButtons = new HBox(clearCaches);
leftButtons.setAlignment(Pos.CENTER);
leftButtons.prefWidthProperty().bind(((Region) p.getDetailNode()).widthProperty());
var leftPane = new AnchorPane(leftButtons);
leftPane.setPickOnBounds(false);
AnchorPane.setBottomAnchor(leftButtons, 15.0);
AnchorPane.setLeftAnchor(leftButtons, 15.0);
var stack = new StackPane(pfx, rightPane, leftPane);
stack.setPickOnBounds(false);
AppFont.medium(stack);
return stack;
}
}

View file

@ -0,0 +1,76 @@
package io.xpipe.app.comp.about;
import io.xpipe.app.util.Hyperlinks;
import io.xpipe.extension.I18n;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.CompStructure;
import io.xpipe.extension.fxcomps.impl.VerticalComp;
import io.xpipe.extension.util.DynamicOptionsBuilder;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
public class AboutTabComp extends Comp<CompStructure<?>> {
private Region createDepsList() {
var deps = new ThirdPartyDependencyListComp().createRegion();
return deps;
}
private Comp<?> hyperlink(String link) {
return Comp.of(() -> {
var hl = new Hyperlink(link);
hl.setOnAction(e -> {
Hyperlinks.open(link);
});
hl.setMaxWidth(250);
return hl;
});
}
private Comp<?> createLinks() {
return new DynamicOptionsBuilder(false)
.addTitle("links")
.addComp(I18n.observable("website"), hyperlink(Hyperlinks.WEBSITE), null)
.addComp(I18n.observable("documentation"), hyperlink(Hyperlinks.DOCUMENTATION), null)
.addComp(I18n.observable("discord"), hyperlink(Hyperlinks.DISCORD), null)
.addComp(I18n.observable("slack"), hyperlink(Hyperlinks.SLACK), null)
.addComp(I18n.observable("github"), hyperlink(Hyperlinks.GITHUB), null)
.buildComp();
}
private Region createThirdPartyDeps() {
var label = new Label(I18n.get("openSourceNotices"), new FontIcon("mdi2o-open-source-initiative"));
label.getStyleClass().add("open-source-header");
var list = createDepsList();
var box = new VBox(label, list);
box.getStyleClass().add("open-source-notices");
return box;
}
@Override
public CompStructure<?> createBase() {
var props = new PropertiesComp();
var update = new UpdateCheckComp();
var box = new VerticalComp(List.of(props, update, createLinks(), new BrowseDirectoryComp()))
.apply(s -> s.get().setFillWidth(true))
.styleClass("information");
return Comp.derive(box, boxS -> {
var bp = new BorderPane();
bp.setLeft(boxS);
var deps = createThirdPartyDeps();
bp.setRight(createThirdPartyDeps());
deps.prefWidthProperty().bind(bp.widthProperty().divide(2));
boxS.prefWidthProperty().bind(bp.widthProperty().divide(2));
bp.getStyleClass().add("about-tab");
return bp;
})
.createStructure();
}
}

View file

@ -0,0 +1,43 @@
package io.xpipe.app.comp.about;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.core.AppLogs;
import io.xpipe.app.issue.UserReportComp;
import io.xpipe.core.util.XPipeInstallation;
import io.xpipe.extension.I18n;
import io.xpipe.extension.event.ErrorEvent;
import io.xpipe.extension.fxcomps.SimpleComp;
import io.xpipe.extension.util.DynamicOptionsBuilder;
import io.xpipe.extension.util.OsHelper;
import javafx.scene.layout.Region;
public class BrowseDirectoryComp extends SimpleComp {
@Override
protected Region createSimple() {
return new DynamicOptionsBuilder(false)
.addComp(
"issueReporter",
new ButtonComp(I18n.observable("reportIssue"), () -> {
var event = ErrorEvent.fromMessage("User Report");
if (AppLogs.get().isWriteToFile()) {
event.attachment(AppLogs.get().getSessionLogsDirectory());
}
UserReportComp.show(event.build());
}),
null)
.addComp(
"logFiles",
new ButtonComp(I18n.observable("openLogsDirectory"), () -> {
OsHelper.browsePath(AppLogs.get().getLogsDirectory());
}),
null)
.addComp(
"installationFiles",
new ButtonComp(I18n.observable("openInstallationDirectory"), () -> {
OsHelper.browsePath(XPipeInstallation.getLocalInstallationBasePath());
}),
null)
.build();
}
}

View file

@ -0,0 +1,47 @@
package io.xpipe.app.comp.about;
import io.xpipe.app.core.App;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppProperties;
import io.xpipe.extension.I18n;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.SimpleComp;
import io.xpipe.extension.fxcomps.impl.LabelComp;
import io.xpipe.extension.util.DynamicOptionsBuilder;
import javafx.scene.control.Label;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Region;
public class PropertiesComp extends SimpleComp {
@Override
protected Region createSimple() {
var title = Comp.of(() -> {
var image = new ImageView(App.getApp().getIcon());
image.setPreserveRatio(true);
image.setFitHeight(40);
var label = new Label(I18n.get("xPipeClient"), image);
label.getStyleClass().add("header");
AppFont.setSize(label, 5);
return label;
});
var section = new DynamicOptionsBuilder(false)
.addComp(title, null)
.addComp(
I18n.observable("version"),
new LabelComp(AppProperties.get().getVersion() + " (x64)"),
null)
.addComp(
I18n.observable("build"),
new LabelComp(AppProperties.get().getBuild()),
null)
.addComp(I18n.observable("runtimeVersion"), new LabelComp(System.getProperty("java.vm.version")), null)
.addComp(
I18n.observable("virtualMachine"),
new LabelComp(System.getProperty("java.vm.vendor") + " " + System.getProperty("java.vm.name")),
null)
.buildComp();
return section.styleClass("properties-comp").createRegion();
}
}

View file

@ -0,0 +1,55 @@
package io.xpipe.app.comp.about;
import io.xpipe.app.core.AppExtensionManager;
import io.xpipe.app.core.AppResources;
import org.apache.commons.io.FilenameUtils;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Properties;
public record ThirdPartyDependency(String name, String version, String licenseName, String licenseText, String link) {
private static final List<ThirdPartyDependency> ALL = new ArrayList<>();
public static void init() {
for (var module : AppExtensionManager.getInstance().getContentModules()) {
AppResources.with(module.getName(), "third-party", path -> {
if (!Files.exists(path)) {
return;
}
try (var list = Files.list(path)) {
for (var p : list
.filter(p -> p.getFileName().toString().endsWith(".properties"))
.sorted(Comparator.comparing(path1 -> path1.toString()))
.toList()) {
var props = new Properties();
try (var in = Files.newInputStream(p)) {
props.load(in);
}
var textFile = p.resolveSibling(
FilenameUtils.getBaseName(p.getFileName().toString()) + ".license");
var text = Files.readString(textFile);
ALL.add(new ThirdPartyDependency(
props.getProperty("name"),
props.getProperty("version"),
props.getProperty("license"),
text,
props.getProperty("link")));
}
}
});
}
}
public static List<ThirdPartyDependency> getAll() {
if (ALL.size() == 0) {
init();
}
return ALL;
}
}

View file

@ -0,0 +1,54 @@
package io.xpipe.app.comp.about;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.util.Hyperlinks;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.CompStructure;
import io.xpipe.extension.fxcomps.SimpleCompStructure;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.layout.StackPane;
public class ThirdPartyDependencyListComp extends Comp<CompStructure<?>> {
private TitledPane createPane(ThirdPartyDependency t) {
var tp = new TitledPane();
tp.setExpanded(false);
var link = new Hyperlink(t.name() + " @ " + t.version());
link.setOnAction(e -> {
Hyperlinks.open(t.link());
});
tp.setGraphic(link);
tp.setAlignment(Pos.CENTER_LEFT);
AppFont.medium(tp);
var licenseName = new Label("(" + t.licenseName() + ")");
var sp = new StackPane(link, licenseName);
StackPane.setAlignment(licenseName, Pos.CENTER_RIGHT);
StackPane.setAlignment(link, Pos.CENTER_LEFT);
sp.prefWidthProperty().bind(tp.widthProperty().subtract(40));
tp.setGraphic(sp);
var text = new TextArea();
text.setEditable(false);
text.setText(t.licenseText());
text.setWrapText(true);
text.setPrefHeight(300);
text.prefWidthProperty().bind(tp.widthProperty());
AppFont.setSize(text, -4);
tp.setContent(text);
AppFont.verySmall(tp);
return tp;
}
@Override
public CompStructure<?> createBase() {
var tps = ThirdPartyDependency.getAll().stream().map(this::createPane).toArray(TitledPane[]::new);
var acc = new Accordion(tps);
acc.getStyleClass().add("third-party-dependency-list-comp");
acc.setPrefWidth(500);
var sp = new ScrollPane(acc);
sp.setFitToWidth(true);
return new SimpleCompStructure<>(sp);
}
}

View file

@ -0,0 +1,156 @@
package io.xpipe.app.comp.about;
import io.xpipe.app.core.AppDistributionType;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.grid.AppUpdater;
import io.xpipe.app.util.Hyperlinks;
import io.xpipe.extension.I18n;
import io.xpipe.extension.fxcomps.SimpleComp;
import io.xpipe.extension.fxcomps.util.PlatformThread;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableBooleanValue;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import org.kordamp.ikonli.javafx.FontIcon;
public class UpdateCheckComp extends SimpleComp {
private final ObservableBooleanValue updateAvailable;
private final ObservableValue<Boolean> updateReady;
public UpdateCheckComp() {
updateAvailable = Bindings.createBooleanBinding(
() -> {
return AppUpdater.get().getLastUpdateCheckResult().getValue() != null
&& AppUpdater.get()
.getLastUpdateCheckResult()
.getValue()
.isUpdate();
},
PlatformThread.sync(AppUpdater.get().getLastUpdateCheckResult()));
updateReady = Bindings.createBooleanBinding(
() -> {
return AppUpdater.get().getDownloadedUpdate().getValue() != null;
},
PlatformThread.sync(AppUpdater.get().getDownloadedUpdate()));
}
private void restart() {
AppUpdater.get().executeUpdateAndClose();
}
private void update() {
AppUpdater.get().downloadUpdateAsync();
}
private void refresh() {
AppUpdater.get().checkForUpdateAsync(true);
}
private ObservableValue<String> descriptionText() {
return PlatformThread.sync(Bindings.createStringBinding(
() -> {
if (AppUpdater.get().getDownloadedUpdate().getValue() != null) {
return I18n.get("updateRestart");
}
if (AppUpdater.get().getLastUpdateCheckResult().getValue() != null
&& AppUpdater.get()
.getLastUpdateCheckResult()
.getValue()
.isUpdate()) {
return I18n.get(
"updateAvailable",
AppUpdater.get()
.getLastUpdateCheckResult()
.getValue()
.getVersion());
}
if (AppUpdater.get().getLastUpdateCheckResult().getValue() != null) {
return AppI18n.readableDuration(
new SimpleObjectProperty<>(AppUpdater.get()
.getLastUpdateCheckResult()
.getValue()
.getCheckTime()),
s -> I18n.get("lastChecked") + " " + s)
.get();
} else {
return null;
}
},
AppUpdater.get().getLastUpdateCheckResult(),
AppUpdater.get().getDownloadedUpdate(),
AppUpdater.get().getBusy()));
}
@Override
protected Region createSimple() {
var button = new Button();
button.disableProperty().bind(PlatformThread.sync(AppUpdater.get().getBusy()));
button.textProperty()
.bind(Bindings.createStringBinding(
() -> {
if (updateReady.getValue()) {
return I18n.get("updateReady");
}
if (updateAvailable.getValue()) {
return AppDistributionType.get().supportsUpdate()
? I18n.get("downloadUpdate")
: I18n.get("checkOutUpdate");
} else {
return I18n.get("checkForUpdates");
}
},
updateAvailable,
updateReady));
button.graphicProperty()
.bind(Bindings.createObjectBinding(
() -> {
if (updateReady.getValue()) {
return new FontIcon("mdi2r-restart");
}
if (updateAvailable.getValue()) {
return new FontIcon("mdi2d-download");
} else {
return new FontIcon("mdi2r-refresh");
}
},
updateAvailable,
updateReady));
button.getStyleClass().add("button-comp");
button.setOnAction(e -> {
AppUpdater.get().refreshUpdateState();
if (updateReady.getValue()) {
restart();
return;
}
if (updateAvailable.getValue() && !AppDistributionType.get().supportsUpdate()) {
Hyperlinks.open(
AppUpdater.get().getLastUpdateCheckResult().getValue().getReleaseUrl());
} else if (updateAvailable.getValue()) {
update();
} else {
refresh();
}
});
var checked = new Label();
checked.textProperty().bind(descriptionText());
var box = new HBox(button, checked);
box.setAlignment(Pos.CENTER_LEFT);
box.setFillHeight(true);
box.getStyleClass().add("update-check");
return box;
}
}

View file

@ -0,0 +1,61 @@
package io.xpipe.app.comp.base;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.CompStructure;
import io.xpipe.extension.fxcomps.SimpleCompStructure;
import javafx.beans.value.ChangeListener;
import javafx.geometry.Rectangle2D;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Pane;
public class BackgroundImageComp extends Comp<CompStructure<Pane>> {
private final Image image;
public BackgroundImageComp(Image image) {
this.image = image;
}
@Override
public CompStructure<Pane> createBase() {
ImageView v = new ImageView(image);
Pane pane = new Pane(v);
v.fitWidthProperty().bind(pane.widthProperty());
v.fitHeightProperty().bind(pane.heightProperty());
if (image == null) {
return new SimpleCompStructure<>(pane);
}
double imageAspect = image.getWidth() / image.getHeight();
ChangeListener<? super Number> cl = (c, o, n) -> {
double paneAspect = pane.getWidth() / pane.getHeight();
double relViewportWidth;
double relViewportHeight;
// Pane width too big for image
if (paneAspect > imageAspect) {
relViewportWidth = 1;
double newImageHeight = pane.getWidth() / imageAspect;
relViewportHeight = Math.min(1, pane.getHeight() / newImageHeight);
}
// Height too big
else {
relViewportHeight = 1;
double newImageWidth = pane.getHeight() * imageAspect;
relViewportWidth = Math.min(1, pane.getWidth() / newImageWidth);
}
v.setViewport(new Rectangle2D(
((1 - relViewportWidth) / 2.0) * image.getWidth(),
((1 - relViewportHeight) / 2.0) * image.getHeight(),
image.getWidth() * relViewportWidth,
image.getHeight() * relViewportHeight));
};
pane.widthProperty().addListener(cl);
pane.heightProperty().addListener(cl);
return new SimpleCompStructure<>(pane);
}
}

View file

@ -0,0 +1,63 @@
package io.xpipe.app.comp.base;
import com.jfoenix.controls.JFXButton;
import io.xpipe.extension.fxcomps.CompStructure;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import lombok.Builder;
import lombok.Value;
public class BigIconButton extends ButtonComp {
public BigIconButton(ObservableValue<String> name, Node graphic, Runnable listener) {
super(name, graphic, listener);
}
@Override
public Structure createBase() {
var vbox = new VBox();
vbox.getStyleClass().add("vbox");
vbox.setAlignment(Pos.CENTER);
var icon = new StackPane(getGraphic());
icon.setAlignment(Pos.CENTER);
icon.getStyleClass().add("icon");
vbox.getChildren().add(icon);
var label = new Label();
label.textProperty().bind(getName());
label.getStyleClass().add("name");
vbox.getChildren().add(label);
var b = new JFXButton(null);
b.setGraphic(vbox);
b.setOnAction(e -> getListener().run());
b.getStyleClass().add("big-icon-button-comp");
return Structure.builder()
.stack(vbox)
.graphic(getGraphic())
.graphicPane(icon)
.text(label)
.button(b)
.build();
}
@Value
@Builder
public static class Structure implements CompStructure<JFXButton> {
JFXButton button;
VBox stack;
Node graphic;
StackPane graphicPane;
Label text;
@Override
public JFXButton get() {
return button;
}
}
}

View file

@ -0,0 +1,69 @@
package io.xpipe.app.comp.base;
import com.jfoenix.controls.JFXButton;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.CompStructure;
import io.xpipe.extension.fxcomps.SimpleCompStructure;
import io.xpipe.extension.fxcomps.util.SimpleChangeListener;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.css.Size;
import javafx.css.SizeUnits;
import javafx.scene.Node;
import org.kordamp.ikonli.javafx.FontIcon;
public class ButtonComp extends Comp<CompStructure<JFXButton>> {
private final ObservableValue<String> name;
private final ObjectProperty<Node> graphic;
private final Runnable listener;
public ButtonComp(ObservableValue<String> name, Runnable listener) {
this.name = name;
this.graphic = new SimpleObjectProperty<>(null);
this.listener = listener;
}
public ButtonComp(ObservableValue<String> name, Node graphic, Runnable listener) {
this.name = name;
this.graphic = new SimpleObjectProperty<>(graphic);
this.listener = listener;
}
public ObservableValue<String> getName() {
return name;
}
public Node getGraphic() {
return graphic.get();
}
public ObjectProperty<Node> graphicProperty() {
return graphic;
}
public Runnable getListener() {
return listener;
}
@Override
public CompStructure<JFXButton> createBase() {
var button = new JFXButton(null);
if (name != null) {
button.textProperty().bind(name);
}
var graphic = getGraphic();
if (graphic instanceof FontIcon f) {
f.iconColorProperty().bind(button.textFillProperty());
SimpleChangeListener.apply(button.fontProperty(), c -> {
f.setIconSize((int) new Size(c.getSize(), SizeUnits.PT).pixels());
});
}
button.setGraphic(getGraphic());
button.setOnAction(e -> getListener().run());
button.getStyleClass().add("button-comp");
return new SimpleCompStructure<>(button);
}
}

View file

@ -0,0 +1,42 @@
package io.xpipe.app.comp.base;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.CompStructure;
import io.xpipe.extension.fxcomps.SimpleCompStructure;
import io.xpipe.extension.fxcomps.util.PlatformThread;
import javafx.beans.binding.Bindings;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.OverrunStyle;
public class CountComp<T> extends Comp<CompStructure<Label>> {
private final ObservableList<T> sub;
private final ObservableList<T> all;
public CountComp(ObservableList<T> sub, ObservableList<T> all) {
this.sub = PlatformThread.sync(sub);
this.all = PlatformThread.sync(all);
}
@Override
public CompStructure<Label> createBase() {
var label = new Label();
label.setTextOverrun(OverrunStyle.CLIP);
label.setAlignment(Pos.CENTER);
label.textProperty()
.bind(Bindings.createStringBinding(
() -> {
if (sub.size() == all.size()) {
return all.size() + "";
} else {
return "" + sub.size() + "/" + all.size();
}
},
sub,
all));
label.getStyleClass().add("count-comp");
return new SimpleCompStructure<>(label);
}
}

View file

@ -0,0 +1,86 @@
package io.xpipe.app.comp.base;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.CompStructure;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.StackPane;
import lombok.Builder;
import lombok.Value;
import org.kordamp.ikonli.javafx.FontIcon;
import java.io.File;
import java.nio.file.Path;
import java.util.List;
import java.util.function.Consumer;
public class FileDropOverlayComp<T extends CompStructure<?>> extends Comp<FileDropOverlayComp.Structure<T>> {
private final Comp<T> comp;
private final Consumer<List<Path>> fileConsumer;
public FileDropOverlayComp(Comp<T> comp, Consumer<List<Path>> fileConsumer) {
this.comp = comp;
this.fileConsumer = fileConsumer;
}
@Override
public Structure<T> createBase() {
var fileDropOverlay = new StackPane(new FontIcon("mdi2f-file-import"));
fileDropOverlay.setOpacity(1.0);
fileDropOverlay.setAlignment(Pos.CENTER);
fileDropOverlay.getStyleClass().add("file-drop-comp");
fileDropOverlay.setVisible(false);
var compBase = comp.createStructure();
var contentStack = new StackPane(compBase.get(), fileDropOverlay);
setupDragAndDrop(contentStack, fileDropOverlay);
return new Structure<>(contentStack, compBase);
}
private void setupDragAndDrop(StackPane stack, Node overlay) {
stack.setOnDragOver(event -> {
if (event.getGestureSource() == null && event.getDragboard().hasFiles()) {
event.acceptTransferModes(TransferMode.COPY);
}
event.consume();
});
stack.setOnDragEntered(event -> {
if (event.getGestureSource() == null && event.getDragboard().hasFiles()) {
overlay.setVisible(true);
}
event.consume();
});
stack.setOnDragExited(event -> {
overlay.setVisible(false);
event.consume();
});
stack.setOnDragDropped(event -> {
// Only accept drops from outside the app window
if (event.getGestureSource() == null && event.getDragboard().hasFiles()) {
event.setDropCompleted(true);
Dragboard db = event.getDragboard();
var list = db.getFiles().stream().map(File::toPath).toList();
fileConsumer.accept(list);
}
event.consume();
});
}
@Value
@Builder
public static class Structure<T extends CompStructure<?>> implements CompStructure<StackPane> {
StackPane value;
T compStructure;
@Override
public StackPane get() {
return value;
}
}
}

View file

@ -0,0 +1,69 @@
package io.xpipe.app.comp.base;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppResources;
import io.xpipe.app.util.Hyperlinks;
import io.xpipe.extension.DownloadModuleInstall;
import io.xpipe.extension.I18n;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.SimpleComp;
import io.xpipe.extension.fxcomps.impl.LabelComp;
import io.xpipe.extension.util.DynamicOptionsBuilder;
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 DynamicOptionsBuilder(false);
builder.addTitle("installRequired");
var header = new LabelComp(I18n.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(I18n.observable("extensionInstallLicenseNote"))
.apply(struc -> struc.get().setWrapText(true));
builder.addComp(changeNotice);
var license = Comp.of(() -> {
var text = new TextArea();
text.setEditable(false);
AppResources.with(install.getModule(), install.getLicenseFile(), file -> {
var s = Files.readString(file);
text.setText(s);
});
text.setWrapText(true);
VBox.setVgrow(text, Priority.ALWAYS);
AppFont.verySmall(text);
return text;
});
builder.addComp(license);
}
return builder.build();
}
}

View file

@ -0,0 +1,114 @@
package io.xpipe.app.comp.base;
import com.jfoenix.controls.JFXTextField;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.CompStructure;
import io.xpipe.extension.fxcomps.util.PlatformThread;
import io.xpipe.extension.fxcomps.util.SimpleChangeListener;
import javafx.animation.Animation;
import javafx.animation.PauseTransition;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleStringProperty;
import javafx.event.EventHandler;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.util.Duration;
import lombok.Builder;
import lombok.Value;
public class LazyTextFieldComp extends Comp<LazyTextFieldComp.Structure> {
private final Property<String> currentValue;
private final Property<String> appliedValue;
public LazyTextFieldComp(Property<String> appliedValue) {
this.appliedValue = appliedValue;
this.currentValue = new SimpleStringProperty(appliedValue.getValue());
}
@Override
public LazyTextFieldComp.Structure createBase() {
var sp = new StackPane();
var r = new JFXTextField();
r.setOnKeyPressed(new EventHandler<KeyEvent>() {
@Override
public void handle(KeyEvent ke) {
if (ke.getCode().equals(KeyCode.ESCAPE)) {
currentValue.setValue(appliedValue.getValue());
}
if (ke.getCode().equals(KeyCode.ENTER) || ke.getCode().equals(KeyCode.ESCAPE)) {
r.getScene().getRoot().requestFocus();
}
ke.consume();
}
});
r.focusedProperty().addListener((c, o, n) -> {
if (!n) {
appliedValue.setValue(currentValue.getValue());
}
});
sp.focusedProperty().addListener((c, o, n) -> {
if (n) {
r.setDisable(false);
r.requestFocus();
}
});
// Handles external updates
PlatformThread.sync(appliedValue).addListener((observable, oldValue, newValue) -> {
r.setText(newValue);
currentValue.setValue(newValue);
});
r.setPrefWidth(0);
sp.getChildren().add(r);
sp.prefWidthProperty().bind(r.prefWidthProperty());
sp.prefHeightProperty().bind(r.prefHeightProperty());
r.setDisable(true);
SimpleChangeListener.apply(currentValue, val -> {
PlatformThread.runLaterIfNeeded(() -> r.setText(val));
});
r.textProperty().addListener((observable, oldValue, newValue) -> {
currentValue.setValue(newValue);
});
Animation delay = new PauseTransition(Duration.millis(800));
delay.setOnFinished(e -> {
r.setDisable(false);
r.requestFocus();
});
sp.addEventFilter(MouseEvent.MOUSE_ENTERED, e -> {
delay.playFromStart();
});
sp.addEventFilter(MouseEvent.MOUSE_EXITED, e -> {
delay.stop();
});
r.focusedProperty().addListener((c, o, n) -> {
if (!n) {
r.setDisable(true);
}
});
r.getStyleClass().add("lazy-text-field-comp");
return new Structure(sp, r);
}
@Value
@Builder
public static class Structure implements CompStructure<StackPane> {
StackPane pane;
JFXTextField textField;
@Override
public StackPane get() {
return pane;
}
}
}

View file

@ -0,0 +1,46 @@
package io.xpipe.app.comp.base;
import com.jfoenix.controls.JFXCheckBox;
import io.xpipe.extension.fxcomps.SimpleComp;
import javafx.beans.property.ListProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.FXCollections;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import lombok.EqualsAndHashCode;
import lombok.Value;
import java.util.List;
import java.util.function.Function;
@Value
@EqualsAndHashCode(callSuper = true)
public class ListSelectorComp<T> extends SimpleComp {
List<T> values;
Function<T, String> toString;
ListProperty<T> selected = new SimpleListProperty<>(FXCollections.observableArrayList());
@Override
protected Region createSimple() {
var vbox = new VBox();
for (var v : values) {
var cb = new JFXCheckBox(null);
cb.selectedProperty().addListener((c, o, n) -> {
if (n) {
selected.add(v);
} else {
selected.remove(v);
}
});
cb.setSelected(true);
var l = new Label(toString.apply(v), cb);
vbox.getChildren().add(l);
}
var sp = new ScrollPane(vbox);
sp.setFitToWidth(true);
return sp;
}
}

View file

@ -0,0 +1,124 @@
package io.xpipe.app.comp.base;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.CompStructure;
import io.xpipe.extension.fxcomps.SimpleCompStructure;
import io.xpipe.extension.fxcomps.util.BindingsHelper;
import io.xpipe.extension.fxcomps.util.PlatformThread;
import io.xpipe.extension.util.PrettyListView;
import javafx.application.Platform;
import javafx.beans.property.Property;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.control.ListView;
import javafx.scene.layout.Region;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
public class ListViewComp<T> extends Comp<CompStructure<ListView<Node>>> {
private final ObservableList<T> shown;
private final ObservableList<T> all;
private final Property<T> selected;
private final Function<T, Comp<?>> compFunction;
public ListViewComp(
ObservableList<T> shown, ObservableList<T> all, Property<T> selected, Function<T, Comp<?>> compFunction) {
this.shown = PlatformThread.sync(shown);
this.all = PlatformThread.sync(all);
this.selected = selected;
this.compFunction = compFunction;
}
@Override
public CompStructure<ListView<Node>> createBase() {
Map<T, Region> cache = new HashMap<>();
PrettyListView<Node> listView = new PrettyListView<>();
listView.setFocusTraversable(false);
if (selected == null) {
listView.disableSelection();
}
refresh(listView, shown, cache, false);
listView.requestLayout();
if (selected != null) {
if (selected.getValue() != null && shown.contains(selected.getValue())) {
listView.getSelectionModel().select(shown.indexOf(selected.getValue()));
}
AtomicBoolean internalSelection = new AtomicBoolean(false);
listView.getSelectionModel().selectedItemProperty().addListener((c, o, n) -> {
// if (true) return;
var item = cache.entrySet().stream()
.filter(e -> e.getValue().equals(n))
.map(e -> e.getKey())
.findAny()
.orElse(null);
internalSelection.set(true);
selected.setValue(item);
internalSelection.set(false);
});
selected.addListener((c, o, n) -> {
if (internalSelection.get()) {
return;
}
var selectedNode = cache.get(n);
PlatformThread.runLaterIfNeeded(() -> {
listView.getSelectionModel().select(selectedNode);
});
});
} else {
listView.getSelectionModel().selectedItemProperty().addListener((c, o, n) -> {
if (n != null) {
listView.getSelectionModel().clearSelection();
listView.getScene().getRoot().requestFocus();
}
});
}
shown.addListener((ListChangeListener<? super T>) (c) -> {
refresh(listView, c.getList(), cache, true);
});
all.addListener((ListChangeListener<? super T>) c -> {
cache.keySet().retainAll(c.getList());
});
return new SimpleCompStructure<>(listView);
}
private void refresh(ListView<Node> listView, List<? extends T> c, Map<T, Region> cache, boolean asynchronous) {
Runnable update = () -> {
var newShown = c.stream()
.map(v -> {
if (!cache.containsKey(v)) {
cache.put(v, compFunction.apply(v).createRegion());
}
return cache.get(v);
})
.toList();
if (!listView.getItems().equals(newShown)) {
BindingsHelper.setContent(listView.getItems(), newShown);
listView.layout();
}
};
if (asynchronous) {
Platform.runLater(update);
} else {
PlatformThread.runLaterIfNeeded(update);
}
}
}

View file

@ -0,0 +1,72 @@
package io.xpipe.app.comp.base;
import com.jfoenix.controls.JFXSpinner;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.CompStructure;
import io.xpipe.extension.fxcomps.SimpleCompStructure;
import io.xpipe.extension.fxcomps.util.PlatformThread;
import io.xpipe.extension.util.ThreadHelper;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.layout.StackPane;
public class LoadingOverlayComp extends Comp<CompStructure<StackPane>> {
private final Comp<?> comp;
private final ObservableValue<Boolean> showLoading;
public LoadingOverlayComp(Comp<?> comp, ObservableValue<Boolean> loading) {
this.comp = comp;
this.showLoading = PlatformThread.sync(loading);
}
@Override
public CompStructure<StackPane> createBase() {
var compStruc = comp.createStructure();
JFXSpinner loading = new JFXSpinner();
loading.getStyleClass().add("spinner");
var loadingBg = new StackPane(loading);
loadingBg.getStyleClass().add("loading-comp");
loadingBg.setVisible(showLoading.getValue());
;
var listener = new ChangeListener<Boolean>() {
@Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean busy) {
if (!busy) {
// Reduce flickering for consecutive loads
Thread t = new Thread(() -> {
try {
Thread.sleep(50);
} catch (InterruptedException ignored) {
}
if (!showLoading.getValue()) {
Platform.runLater(() -> loadingBg.setVisible(false));
}
});
t.setDaemon(true);
t.setName("loading delay");
t.start();
} else {
ThreadHelper.runAsync(() -> {
try {
Thread.sleep(50);
} catch (InterruptedException ignored) {
}
if (showLoading.getValue()) {
Platform.runLater(() -> loadingBg.setVisible(true));
}
});
}
}
};
showLoading.addListener(listener);
var stack = new StackPane(compStruc.get(), loadingBg);
return new SimpleCompStructure<>(stack);
}
}

View file

@ -0,0 +1,91 @@
package io.xpipe.app.comp.base;
import com.vladsch.flexmark.html.HtmlRenderer;
import com.vladsch.flexmark.parser.Parser;
import com.vladsch.flexmark.util.ast.Document;
import com.vladsch.flexmark.util.data.MutableDataSet;
import io.xpipe.app.core.AppResources;
import io.xpipe.extension.event.ErrorEvent;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.CompStructure;
import io.xpipe.extension.fxcomps.SimpleCompStructure;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import lombok.SneakyThrows;
import java.awt.*;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.util.function.UnaryOperator;
public class MarkdownComp extends Comp<CompStructure<StackPane>> {
private final String markdown;
private final UnaryOperator<String> transformation;
public MarkdownComp(String markdown, UnaryOperator<String> transformation) {
this.markdown = markdown;
this.transformation = transformation;
}
private String getHtml() {
MutableDataSet options = new MutableDataSet();
Parser parser = Parser.builder(options).build();
HtmlRenderer renderer = HtmlRenderer.builder(options).build();
Document document = parser.parse(markdown);
var html = renderer.render(document);
var result = transformation.apply(html);
return "<article class=\"markdown-body\">" + result + "</article>";
}
@SneakyThrows
private WebView createWebView() {
var wv = new WebView();
wv.setPageFill(Color.valueOf("#EEE"));
var url = AppResources.getResourceURL(AppResources.XPIPE_MODULE, "web/github-markdown.css")
.orElseThrow();
wv.getEngine().setUserStyleSheetLocation(url.toString());
// Work around for https://bugs.openjdk.org/browse/JDK-8199014
try {
var file = Files.createTempFile(null, ".html");
Files.writeString(file, getHtml());
var contentUrl = file.toUri();
wv.getEngine().load(contentUrl.toString());
} catch (IOException e) {
throw new RuntimeException(e);
}
wv.getStyleClass().add("markdown-comp");
addLinkHandler(wv.getEngine());
return wv;
}
private void addLinkHandler(WebEngine engine) {
engine.getLoadWorker()
.stateProperty()
.addListener((observable, oldValue, newValue) -> Platform.runLater(() -> {
String toBeopen = engine.getLoadWorker().getMessage().trim().replace("Loading ", "");
if (toBeopen.contains("http://") || toBeopen.contains("https://")) {
engine.getLoadWorker().cancel();
try {
Desktop.getDesktop().browse(new URL(toBeopen).toURI());
} catch (Exception e) {
ErrorEvent.fromThrowable(e).omit().handle();
}
}
}));
}
@Override
public CompStructure<StackPane> createBase() {
var sp = new StackPane(createWebView());
sp.setPadding(Insets.EMPTY);
return new SimpleCompStructure<>(sp);
}
}

View file

@ -0,0 +1,76 @@
package io.xpipe.app.comp.base;
import io.xpipe.extension.fxcomps.SimpleComp;
import io.xpipe.extension.fxcomps.util.PlatformThread;
import io.xpipe.extension.fxcomps.util.SimpleChangeListener;
import io.xpipe.extension.util.ThreadHelper;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import lombok.AccessLevel;
import lombok.experimental.FieldDefaults;
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
public class MessageComp extends SimpleComp {
Property<Boolean> shown = new SimpleBooleanProperty();
ObservableValue<String> text;
int msShown;
public MessageComp(ObservableValue<String> text, int msShown) {
this.text = PlatformThread.sync(text);
this.msShown = msShown;
}
public void show() {
shown.setValue(true);
if (msShown != -1) {
ThreadHelper.runAsync(() -> {
try {
Thread.sleep(msShown);
} catch (InterruptedException ignored) {
}
shown.setValue(false);
});
}
}
@Override
protected Region createSimple() {
var l = new Label();
l.textProperty().bind(text);
l.setWrapText(true);
l.getStyleClass().add("message");
var sp = new StackPane(l);
sp.getStyleClass().add("message-comp");
SimpleChangeListener.apply(PlatformThread.sync(shown), n -> {
if (n) {
l.setMinHeight(Region.USE_PREF_SIZE);
l.setPrefHeight(Region.USE_COMPUTED_SIZE);
l.setMaxHeight(Region.USE_PREF_SIZE);
sp.setMinHeight(Region.USE_PREF_SIZE);
sp.setPrefHeight(Region.USE_COMPUTED_SIZE);
sp.setMaxHeight(Region.USE_PREF_SIZE);
} else {
l.setMinHeight(0);
l.setPrefHeight(0);
l.setMaxHeight(0);
sp.setMinHeight(0);
sp.setPrefHeight(0);
sp.setMaxHeight(0);
}
});
return sp;
}
}

View file

@ -0,0 +1,37 @@
package io.xpipe.app.comp.base;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.SimpleComp;
import io.xpipe.extension.fxcomps.util.PlatformThread;
import io.xpipe.extension.fxcomps.util.SimpleChangeListener;
import javafx.beans.value.ObservableBooleanValue;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import java.util.Map;
public class MultiContentComp extends SimpleComp {
private final Map<Comp<?>, ObservableBooleanValue> content;
public MultiContentComp(Map<Comp<?>, ObservableBooleanValue> content) {
this.content = content;
}
@Override
protected Region createSimple() {
var stack = new StackPane();
stack.setPickOnBounds(false);
for (Map.Entry<Comp<?>, ObservableBooleanValue> entry : content.entrySet()) {
var region = entry.getKey().createRegion();
SimpleChangeListener.apply(PlatformThread.sync(entry.getValue()), val -> {
if (val) {
stack.getChildren().add(region);
} else {
stack.getChildren().remove(region);
}
});
}
return stack;
}
}

View file

@ -0,0 +1,303 @@
package io.xpipe.app.comp.base;
import com.jfoenix.controls.JFXTabPane;
import io.xpipe.extension.I18n;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.CompStructure;
import io.xpipe.extension.fxcomps.SimpleCompStructure;
import io.xpipe.extension.fxcomps.util.PlatformThread;
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 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<>();
private List<Entry> entries;
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());
});
}
protected void setStartingIndex(int start) {
if (this.entries != null) {
throw new IllegalStateException();
}
currentIndex = start;
}
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 int getCurrentIndex() {
return 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.getStyleClass().add("buttons");
buttons.setSpacing(5);
var helpButton = new ButtonComp(I18n.observable("help"), null, () -> {
getValue().help.run();
})
.styleClass("help")
.apply(struc -> struc.get()
.visibleProperty()
.bind(Bindings.createBooleanBinding(() -> getValue().help == null, currentStep)));
if (getValue().help != null) {
buttons.getChildren().add(helpButton.createRegion());
}
var spacer = new Region();
buttons.getChildren().add(spacer);
HBox.setHgrow(spacer, Priority.ALWAYS);
buttons.setAlignment(Pos.CENTER_RIGHT);
var nextText = Bindings.createStringBinding(
() -> isLastPage() ? I18n.get("finishStep") : I18n.get("nextStep"), currentStep);
var nextButton = new ButtonComp(nextText, null, comp::next).styleClass("next");
var previousButton = new ButtonComp(I18n.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 entry : 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 List<Entry> getEntries() {
return entries;
}
public ReadOnlyProperty<Boolean> completedProperty() {
return completed;
}
public abstract static class Step<S extends CompStructure<?>> extends Comp<S> {
private final Runnable help;
public Step(Runnable help) {
this.help = help;
}
public void onInit() {}
public void onBack() {}
public void onContinue() {}
public boolean canContinue() {
return true;
}
}
public static record Entry(ObservableValue<String> name, Step<?> step) {}
}

View file

@ -0,0 +1,48 @@
package io.xpipe.app.comp.base;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.CompStructure;
import io.xpipe.extension.fxcomps.SimpleCompStructure;
import io.xpipe.extension.fxcomps.augment.GrowAugment;
import javafx.beans.property.Property;
import javafx.beans.value.ObservableValue;
import javafx.css.PseudoClass;
import javafx.scene.layout.VBox;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
private final Property<SideMenuBarComp.Entry> value;
private final List<Entry> entries;
public SideMenuBarComp(Property<Entry> value, List<Entry> entries) {
this.value = value;
this.entries = entries;
}
@Override
public CompStructure<VBox> createBase() {
var vbox = new VBox();
vbox.setFillWidth(true);
var selected = PseudoClass.getPseudoClass("selected");
entries.forEach(e -> {
var fi = new FontIcon(e.icon());
var b = new BigIconButton(e.name(), fi, () -> value.setValue(e));
b.apply(GrowAugment.create(true, false));
b.apply(struc -> {
struc.get().pseudoClassStateChanged(selected, value.getValue().equals(e));
value.addListener((c, o, n) -> {
struc.get().pseudoClassStateChanged(selected, n.equals(e));
});
});
vbox.getChildren().add(b.createRegion());
});
vbox.getStyleClass().add("sidebar-comp");
return new SimpleCompStructure<>(vbox);
}
public static record Entry(ObservableValue<String> name, String icon, Comp<?> comp) {}
}

View file

@ -0,0 +1,43 @@
package io.xpipe.app.comp.base;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.CompStructure;
import io.xpipe.extension.fxcomps.SimpleCompStructure;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.TitledPane;
import java.util.concurrent.atomic.AtomicInteger;
public class TitledPaneComp extends Comp<CompStructure<TitledPane>> {
private final ObservableValue<String> name;
private final Comp<?> content;
private final int height;
public TitledPaneComp(ObservableValue<String> name, Comp<?> content, int height) {
this.name = name;
this.content = content;
this.height = height;
}
@Override
public CompStructure<TitledPane> createBase() {
var tp = new TitledPane(null, content.createRegion());
tp.textProperty().bind(name);
tp.getStyleClass().add("titled-pane-comp");
tp.setExpanded(false);
tp.setAnimated(false);
AtomicInteger minimizedSize = new AtomicInteger();
tp.expandedProperty().addListener((c, o, n) -> {
if (n) {
if (minimizedSize.get() == 0) {
minimizedSize.set((int) tp.getHeight());
}
tp.setPrefHeight(height);
} else {
tp.setPrefHeight(minimizedSize.get());
}
});
return new SimpleCompStructure<>(tp);
}
}

View file

@ -0,0 +1,105 @@
package io.xpipe.app.comp.source;
import io.xpipe.app.core.AppCache;
import io.xpipe.extension.DataSourceTarget;
import io.xpipe.extension.I18n;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.CompStructure;
import io.xpipe.extension.fxcomps.SimpleCompStructure;
import io.xpipe.extension.util.CustomComboBoxBuilder;
import javafx.beans.property.Property;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
import java.util.function.Predicate;
public class DataSourceTargetChoiceComp extends Comp<CompStructure<ComboBox<Node>>> {
private final Property<DataSourceTarget> selectedApplication;
private final List<DataSourceTarget> all;
private DataSourceTargetChoiceComp(
Property<DataSourceTarget> selectedApplication, List<DataSourceTarget> all) {
this.selectedApplication = selectedApplication;
this.all = all;
}
public static DataSourceTargetChoiceComp create(
Property<DataSourceTarget> selectedApplication,
Predicate<DataSourceTarget> filter) {
selectedApplication.addListener((observable, oldValue, val) -> {
AppCache.update("application-last-used", val != null ? val.getId() : null);
});
var all = DataSourceTarget.getAll().stream()
.filter((p) -> filter.test(p))
.toList();
if (selectedApplication.getValue() == null) {
String selectedId = AppCache.get("application-last-used", String.class, () -> null);
var selectedProvider = selectedId != null
? DataSourceTarget.byId(selectedId)
.filter(filter)
.orElse(null)
: null;
selectedApplication.setValue(selectedProvider);
}
return new DataSourceTargetChoiceComp(selectedApplication, all);
}
private String getIconCode(DataSourceTarget p) {
return p.getGraphicIcon() != null
? p.getGraphicIcon()
: p.getCategory().equals(DataSourceTarget.Category.PROGRAMMING_LANGUAGE)
? "mdi2c-code-tags"
: "mdral-indeterminate_check_box";
}
private Region createLabel(DataSourceTarget p) {
var g = new FontIcon(getIconCode(p));
var l = new Label(p.getName().getValue(), g);
l.setAlignment(Pos.CENTER);
g.iconColorProperty().bind(l.textFillProperty());
return l;
}
@Override
public CompStructure<ComboBox<Node>> createBase() {
var addMoreLabel = new Label(I18n.get("addMore"), new FontIcon("mdmz-plus"));
var builder = new CustomComboBoxBuilder<DataSourceTarget>(
selectedApplication, app -> createLabel(app), new Label(""), v -> true);
// builder.addFilter((v, s) -> v.getName().getValue().toLowerCase().contains(s));
builder.addHeader(I18n.get("programmingLanguages"));
all.stream()
.filter(p -> p.getCategory().equals(DataSourceTarget.Category.PROGRAMMING_LANGUAGE))
.forEach(builder::add);
builder.addHeader(I18n.get("applications"));
all.stream()
.filter(p -> p.getCategory().equals(DataSourceTarget.Category.APPLICATION))
.forEach(builder::add);
builder.addHeader(I18n.get("other"));
all.stream()
.filter(p -> p.getCategory().equals(DataSourceTarget.Category.OTHER))
.forEach(builder::add);
// builder.addSeparator();
// builder.addAction(addMoreLabel, () -> {
//
// });
var cb = builder.build();
cb.getStyleClass().add("application-choice-comp");
cb.setMaxWidth(2000);
return new SimpleCompStructure<>(cb);
}
}

View file

@ -0,0 +1,61 @@
package io.xpipe.app.comp.source;
import io.xpipe.core.source.CollectionReadConnection;
import io.xpipe.extension.event.ErrorEvent;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.CompStructure;
import io.xpipe.extension.fxcomps.SimpleCompStructure;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import java.util.ArrayList;
public class DsCollectionComp extends Comp<CompStructure<TreeView<String>>> {
private final ObservableValue<CollectionReadConnection> con;
private final ObservableValue<String> value;
public DsCollectionComp(ObservableValue<CollectionReadConnection> con) {
this.con = con;
this.value = new SimpleObjectProperty<>("/");
}
private TreeItem<String> createTree() {
var c = new ArrayList<TreeItem<String>>();
if (con.getValue() != null) {
try {
con.getValue().listEntries().forEach(e -> {
var item = new TreeItem<String>(e.getFileName());
c.add(item);
});
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
}
}
var ar = new TreeItem<String>(value.getValue());
ar.getChildren().setAll(c);
return ar;
}
private void setupListener(TreeView<String> tv) {
ChangeListener<CollectionReadConnection> listener = (c, o, n) -> {
var nt = createTree();
tv.setRoot(nt);
};
con.addListener(listener);
listener.changed(con, null, con.getValue());
}
@Override
public CompStructure<TreeView<String>> createBase() {
var table = new TreeView<String>();
setupListener(table);
return new SimpleCompStructure<>(table);
}
}

View file

@ -0,0 +1,207 @@
package io.xpipe.app.comp.source;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.comp.base.MultiStepComp;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppWindowHelper;
import io.xpipe.app.storage.DataSourceEntry;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.Hyperlinks;
import io.xpipe.core.source.DataSourceId;
import io.xpipe.extension.I18n;
import io.xpipe.extension.DataSourceTarget;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.CompStructure;
import io.xpipe.extension.fxcomps.SimpleComp;
import io.xpipe.extension.fxcomps.impl.DynamicOptionsComp;
import io.xpipe.extension.fxcomps.impl.HorizontalComp;
import io.xpipe.extension.fxcomps.util.PlatformThread;
import io.xpipe.extension.fxcomps.util.SimpleChangeListener;
import io.xpipe.extension.util.BusyProperty;
import io.xpipe.extension.util.ThreadHelper;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Pos;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.experimental.FieldDefaults;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.ArrayList;
import java.util.List;
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
@AllArgsConstructor
@Getter
public class DsDataTransferComp extends SimpleComp {
private final Property<DataSourceEntry> dataSourceEntry;
Property<DataSourceTarget> selectedTarget = new SimpleObjectProperty<>();
Property<DataSourceTarget.InstructionsDisplay> selectedDisplay = new SimpleObjectProperty<>();
List<DataSourceTarget> excludedTargets = new ArrayList<>();
public DsDataTransferComp selectApplication(DataSourceTarget t) {
selectedTarget.setValue(t);
return this;
}
public DsDataTransferComp exclude(DataSourceTarget t) {
excludedTargets.add(t);
return this;
}
public static void showPipeWindow(DataSourceEntry e) {
Platform.runLater(() -> {
var loading = new SimpleBooleanProperty();
AppWindowHelper.sideWindow(
I18n.get("pipeDataSource"),
window -> {
var ms = new DsDataTransferComp(new SimpleObjectProperty<>(e)).exclude(DataSourceTarget.byId("base.saveSource").orElseThrow());
var multi = new MultiStepComp() {
@Override
protected List<Entry> setup() {
return List.of(new Entry(null, new Step<>(null) {
@Override
public CompStructure<?> createBase() {
return ms.createStructure();
}
@Override
public boolean canContinue() {
var selected = ms.selectedTarget.getValue();
if (selected == null) {
return false;
}
var validator = ms.selectedDisplay
.getValue()
.getValidator();
if (validator == null) {
return true;
}
return validator.validate();
}
}));
}
@Override
protected void finish() {
var onFinish = ms.getSelectedDisplay()
.getValue()
.getOnFinish();
if (onFinish != null) {
ThreadHelper.runAsync(() -> {
try (var busy = new BusyProperty(loading)) {
onFinish.run();
PlatformThread.runLaterIfNeeded(() -> window.close());
}
});
}
}
};
return multi.apply(s -> {
SimpleChangeListener.apply(ms.getSelectedTarget(), (c) -> {
if (c != null
&& c.getAccessType()
== DataSourceTarget.AccessType.PASSIVE) {
((Region) s.get().getChildren().get(2)).setMaxHeight(0);
((Region) s.get().getChildren().get(2)).setMinHeight(0);
((Region) s.get().getChildren().get(2)).setVisible(false);
} else {
((Region) s.get().getChildren().get(2)).setMaxHeight(Region.USE_PREF_SIZE);
((Region) s.get().getChildren().get(2)).setMinHeight(Region.USE_PREF_SIZE);
((Region) s.get().getChildren().get(2)).setVisible(true);
}
});
s.get().setPrefWidth(600);
s.get().setPrefHeight(700);
AppFont.medium(s.get());
});
},
false,
loading)
.show();
});
}
@Override
public Region createSimple() {
ObservableValue<DataSourceId> id = Bindings.createObjectBinding(
() -> {
if (!DataStorage.get().getSourceEntries().contains(dataSourceEntry.getValue())) {
return null;
}
return DataStorage.get().getId(dataSourceEntry.getValue());
},
dataSourceEntry);
var chooser = DataSourceTargetChoiceComp.create(
selectedTarget,
a -> !excludedTargets.contains(a) && a.isApplicable(dataSourceEntry.getValue().getSource()) &&
a.createRetrievalInstructions(
dataSourceEntry.getValue().getSource(), id)
!= null);
var setupGuideButton = new ButtonComp(
I18n.observable("setupGuide"), new FontIcon("mdoal-integration_instructions"), () -> {
Hyperlinks.open(selectedTarget.getValue().getSetupGuideURL());
})
.apply(s -> s.get()
.visibleProperty()
.bind(Bindings.createBooleanBinding(
() -> {
return selectedTarget.getValue() != null
&& selectedTarget.getValue().getSetupGuideURL() != null;
},
selectedTarget
)));
var top = new HorizontalComp(List.<Comp<?>>of(
chooser.apply(struc -> HBox.setHgrow(struc.get(), Priority.ALWAYS)),
setupGuideButton))
.apply(struc -> {
struc.get().setAlignment(Pos.CENTER);
struc.get().setSpacing(12);
struc.get().getStyleClass().add("top");
});
// setupGuideButton.prefHeightProperty().bind(chooserR.heightProperty());
var content = new VBox(
new DynamicOptionsComp(
List.of(new DynamicOptionsComp.Entry(null, null, top)), false)
.createRegion(),
new Region());
SimpleChangeListener.apply(selectedTarget, c -> {
if (selectedTarget.getValue() == null) {
content.getChildren().set(1, new Region());
selectedDisplay.setValue(null);
return;
}
var instructions = selectedTarget
.getValue()
.createRetrievalInstructions(
dataSourceEntry.getValue().getSource(), id);
content.getChildren().set(1, instructions.getRegion());
VBox.setVgrow(instructions.getRegion(), Priority.ALWAYS);
selectedDisplay.setValue(instructions);
});
content.setSpacing(15);
var r = content;
r.getStyleClass().add("data-source-retrieve");
return r;
}
}

View file

@ -0,0 +1,90 @@
package io.xpipe.app.comp.source;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.JfxHelper;
import io.xpipe.core.source.DataSourceType;
import io.xpipe.extension.DataSourceProvider;
import io.xpipe.extension.DataSourceProviders;
import io.xpipe.extension.I18n;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.CompStructure;
import io.xpipe.extension.fxcomps.SimpleCompStructure;
import io.xpipe.extension.util.CustomComboBoxBuilder;
import io.xpipe.extension.util.SimpleValidator;
import io.xpipe.extension.util.Validatable;
import io.xpipe.extension.util.Validator;
import javafx.beans.property.Property;
import javafx.scene.Node;
import javafx.scene.control.ComboBox;
import javafx.scene.layout.Region;
import lombok.Getter;
import net.synedra.validatorfx.Check;
import java.util.List;
public class DsProviderChoiceComp extends Comp<CompStructure<ComboBox<Node>>> implements Validatable {
private final DataSourceProvider.Category type;
private final Property<DataSourceProvider<?>> provider;
@Getter
private final Validator validator = new SimpleValidator();
private final Check check;
private final DataSourceType filter;
public DsProviderChoiceComp(
DataSourceProvider.Category type, Property<DataSourceProvider<?>> provider, DataSourceType filter) {
this.type = type;
this.provider = provider;
check = Validator.nonNull(validator, I18n.observable("provider"), provider);
this.filter = filter;
}
private Region createDefaultNode() {
return switch (type) {
case STREAM -> JfxHelper.createNamedEntry(
I18n.get("anyStream"), I18n.get("anyStreamDescription"), "file_icon.png");
case DATABASE -> JfxHelper.createNamedEntry(
I18n.get("selectQueryType"), I18n.get("selectQueryTypeDescription"), "db_icon.png");
};
}
private List<DataSourceProvider<?>> getProviders() {
return switch (type) {
case STREAM -> DataSourceProviders.getAll().stream()
.filter(p -> AppPrefs.get().developerShowHiddenProviders().get()
|| p.getCategory() == DataSourceProvider.Category.STREAM)
.filter(p -> p.shouldShow(filter))
.toList();
case DATABASE -> DataSourceProviders.getAll().stream()
.filter(p -> p.getCategory() == DataSourceProvider.Category.DATABASE)
.filter(p -> AppPrefs.get().developerShowHiddenProviders().get() || p.shouldShow(filter))
.toList();
};
}
private Region createGraphic(DataSourceProvider<?> provider) {
if (provider == null) {
return createDefaultNode();
}
var graphic = provider.getDisplayIconFileName();
return JfxHelper.createNamedEntry(provider.getDisplayName(), provider.getDisplayDescription(), graphic);
}
@Override
public CompStructure<ComboBox<Node>> createBase() {
var comboBox = new CustomComboBoxBuilder<>(provider, this::createGraphic, createDefaultNode(), v -> true);
comboBox.add(null);
comboBox.addSeparator();
comboBox.addFilter((v, s) -> v.getDisplayName().toLowerCase().contains(s.toLowerCase()));
getProviders().forEach(comboBox::add);
ComboBox<Node> cb = comboBox.build();
check.decorates(cb);
cb.getStyleClass().add("data-source-type");
cb.getStyleClass().add("choice-comp");
return new SimpleCompStructure<>(cb);
}
}

View file

@ -0,0 +1,35 @@
package io.xpipe.app.comp.source;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.CompStructure;
import io.xpipe.extension.fxcomps.SimpleCompStructure;
import io.xpipe.extension.fxcomps.util.PlatformThread;
import io.xpipe.extension.fxcomps.util.SimpleChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.TextArea;
import java.util.HexFormat;
public class DsRawComp extends Comp<CompStructure<TextArea>> {
private final ObservableValue<byte[]> value;
public DsRawComp(ObservableValue<byte[]> value) {
this.value = value;
}
private void setupListener(TextArea ta) {
var format = HexFormat.of().withDelimiter(" ").withUpperCase();
SimpleChangeListener.apply(PlatformThread.sync(value), val -> {
ta.textProperty().setValue(format.formatHex(val));
});
}
@Override
public CompStructure<TextArea> createBase() {
var ta = new TextArea();
ta.setWrapText(true);
setupListener(ta);
return new SimpleCompStructure<>(ta);
}
}

View file

@ -0,0 +1,43 @@
package io.xpipe.app.comp.source;
import io.xpipe.app.storage.DataSourceCollection;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.extension.fxcomps.SimpleComp;
import io.xpipe.extension.util.CustomComboBoxBuilder;
import javafx.beans.property.Property;
import javafx.scene.Node;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
public class DsStorageGroupSelector extends SimpleComp {
private final Property<DataSourceCollection> selected;
public DsStorageGroupSelector(Property<DataSourceCollection> selected) {
this.selected = selected;
}
private static Region createGraphic(DataSourceCollection group) {
if (group == null) {
return new Label("<>");
}
var l = new Label(group.getName());
return l;
}
@Override
protected ComboBox<Node> createSimple() {
var comboBox = new CustomComboBoxBuilder<DataSourceCollection>(
selected, DsStorageGroupSelector::createGraphic, createGraphic(null), v -> true);
DataStorage.get().getSourceCollections().stream()
.filter(dataSourceCollection ->
!dataSourceCollection.equals(DataStorage.get().getInternalCollection()))
.forEach(comboBox::add);
ComboBox<Node> cb = comboBox.build();
cb.getStyleClass().add("storage-group-selector");
return cb;
}
}

View file

@ -0,0 +1,75 @@
package io.xpipe.app.comp.source;
import com.jfoenix.controls.JFXTextField;
import io.xpipe.app.comp.storage.DataSourceTypeComp;
import io.xpipe.app.storage.DataSourceCollection;
import io.xpipe.app.storage.DataSourceEntry;
import io.xpipe.core.source.DataSourceId;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.SimpleComp;
import io.xpipe.extension.fxcomps.impl.HorizontalComp;
import javafx.beans.property.Property;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import java.util.List;
public class DsStorageTargetComp extends SimpleComp {
private final Property<DataSourceEntry> dataSourceEntry;
private final Property<DataSourceCollection> storageGroup;
private final Property<Boolean> nameValid;
public DsStorageTargetComp(
Property<DataSourceEntry> dataSourceEntry,
Property<DataSourceCollection> storageGroup,
Property<Boolean> nameValid) {
this.dataSourceEntry = dataSourceEntry;
this.storageGroup = storageGroup;
this.nameValid = nameValid;
}
@Override
protected Region createSimple() {
var type = new DataSourceTypeComp(
dataSourceEntry.getValue().getDataSourceType(),
dataSourceEntry.getValue().getSource().getFlow());
type.apply(struc -> struc.get().prefWidthProperty().bind(struc.get().prefHeightProperty()));
type.apply(struc -> struc.get().setPrefHeight(60));
var storageGroupSelector = new DsStorageGroupSelector(storageGroup).apply(s -> {
s.get().setMaxWidth(1000);
HBox.setHgrow(s.get(), Priority.ALWAYS);
});
var splitter = Comp.of(() -> new Label("" + DataSourceId.SEPARATOR)).apply(s -> {});
var name = Comp.of(() -> {
var nameField = new JFXTextField(dataSourceEntry.getValue().getName());
dataSourceEntry.addListener((c, o, n) -> {
nameField.setText(n.getName());
nameValid.setValue(n.getName().trim().length() > 0);
});
nameField.textProperty().addListener((c, o, n) -> {
dataSourceEntry.getValue().setName(n);
});
return nameField;
})
.apply(s -> HBox.setHgrow(s.get(), Priority.ALWAYS));
var right = new HorizontalComp(List.of(storageGroupSelector, splitter, name))
.apply(struc -> {
struc.get().setAlignment(Pos.CENTER);
HBox.setHgrow(struc.get(), Priority.ALWAYS);
})
.styleClass("data-source-id");
return new HorizontalComp(List.of(type, right))
.apply(s -> s.get().setFillHeight(true))
.styleClass("data-source-preview")
.createRegion();
}
}

View file

@ -0,0 +1,66 @@
package io.xpipe.app.comp.source;
import io.xpipe.core.data.node.DataStructureNode;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.CompStructure;
import io.xpipe.extension.fxcomps.SimpleCompStructure;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicInteger;
public class DsStructureComp extends Comp<CompStructure<TreeView<String>>> {
private final ObservableValue<DataStructureNode> value;
public DsStructureComp(ObservableValue<DataStructureNode> value) {
this.value = value;
}
private TreeItem<String> createTree(DataStructureNode n, AtomicInteger counter, int max) {
if (n.isArray()) {
var c = new ArrayList<TreeItem<String>>();
for (int i = 0; i < Math.min(n.size(), max - counter.get()); i++) {
var item = createTree(n.at(i), counter, max);
item.setValue("[" + i + "] = " + item.getValue());
c.add(item);
}
var ar = new TreeItem<String>("[" + n.size() + "... ]");
ar.getChildren().setAll(c);
return ar;
} else if (n.isTuple()) {
var c = new ArrayList<TreeItem<String>>();
for (int i = 0; i < Math.min(n.size(), max - counter.get()); i++) {
var item = createTree(n.at(i), counter, max);
var key = n.asTuple().getKeyNames().get(i);
item.setValue((key != null ? key : "" + i) + " = " + item.getValue());
c.add(item);
}
var ar = new TreeItem<String>("( " + n.size() + "... )");
ar.getChildren().setAll(c);
return ar;
} else {
var ar = new TreeItem<String>(n.asValue().asString());
return ar;
}
}
private void setupListener(TreeView<String> tv) {
ChangeListener<DataStructureNode> listener = (c, o, n) -> {
var nt = createTree(n, new AtomicInteger(0), 100);
tv.setRoot(nt);
};
value.addListener(listener);
listener.changed(value, null, value.getValue());
}
@Override
public CompStructure<TreeView<String>> createBase() {
var table = new TreeView<String>();
setupListener(table);
return new SimpleCompStructure<>(table);
}
}

View file

@ -0,0 +1,93 @@
package io.xpipe.app.comp.source;
import io.xpipe.core.data.node.ArrayNode;
import io.xpipe.core.data.node.DataStructureNode;
import io.xpipe.core.data.type.DataTypeVisitors;
import io.xpipe.core.data.type.TupleType;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.CompStructure;
import io.xpipe.extension.fxcomps.SimpleCompStructure;
import io.xpipe.extension.fxcomps.util.PlatformThread;
import io.xpipe.extension.fxcomps.util.SimpleChangeListener;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import java.util.ArrayList;
import java.util.Stack;
public class DsTableComp extends Comp<CompStructure<TableView<DsTableComp.RowWrapper>>> {
private final ObservableValue<ArrayNode> value;
public DsTableComp(ObservableValue<ArrayNode> value) {
this.value = value;
}
private TupleType determineDataType(ArrayNode table) {
if (table == null || table.size() == 0) {
return TupleType.empty();
}
var first = table.at(0);
return (TupleType) first.determineDataType();
}
private void setupListener(TableView<RowWrapper> table) {
SimpleChangeListener.apply(PlatformThread.sync(value), n -> {
table.getItems().clear();
table.getColumns().clear();
var t = determineDataType(n);
var stack = new Stack<ObservableList<TableColumn<RowWrapper, ?>>>();
stack.push(table.getColumns());
t.visit(DataTypeVisitors.table(
tupleName -> {
var current = stack.peek();
stack.push(current.get(current.size() - 1).getColumns());
},
stack::pop,
(name, pointer) -> {
TableColumn<RowWrapper, String> col = new TableColumn<>(name);
col.setCellValueFactory(cellData -> {
var node = pointer.get(n.at(cellData.getValue().rowIndex()));
return new SimpleStringProperty(nodeToString(node));
});
var current = stack.peek();
current.add(col);
}));
var list = new ArrayList<RowWrapper>(n.size());
for (int i = 0; i < n.size(); i++) {
list.add(new RowWrapper(n, i));
}
table.setItems(FXCollections.observableList(list));
});
}
private String nodeToString(DataStructureNode node) {
if (node.isValue()) {
return node.asString();
}
if (node.isArray()) {
return "[...]";
}
if (node.isTuple()) {
return "{...}";
}
return null;
}
@Override
public CompStructure<TableView<RowWrapper>> createBase() {
var table = new TableView<RowWrapper>();
setupListener(table);
return new SimpleCompStructure<>(table);
}
public record RowWrapper(ArrayNode table, int rowIndex) {}
}

View file

@ -0,0 +1,49 @@
package io.xpipe.app.comp.source;
import io.xpipe.core.source.TableMapping;
import io.xpipe.extension.I18n;
import io.xpipe.extension.fxcomps.SimpleComp;
import io.xpipe.extension.fxcomps.impl.LabelComp;
import io.xpipe.extension.fxcomps.util.PlatformThread;
import io.xpipe.extension.fxcomps.util.SimpleChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Region;
import lombok.EqualsAndHashCode;
import lombok.Value;
@Value
@EqualsAndHashCode(callSuper = true)
public class DsTableMappingComp extends SimpleComp {
ObservableValue<TableMapping> mapping;
public DsTableMappingComp(ObservableValue<TableMapping> mapping) {
this.mapping = PlatformThread.sync(mapping);
}
@Override
protected Region createSimple() {
var grid = new GridPane();
grid.getStyleClass().add("table-mapping-comp");
SimpleChangeListener.apply(mapping, val -> {
for (int i = 0; i < val.getInputType().getSize(); i++) {
var input = new LabelComp(val.getInputType().getNames().get(i));
grid.add(input.createRegion(), 0, i);
grid.add(new LabelComp("->").createRegion(), 1, i);
var map = val.map(i).orElse(-1);
var output =
new LabelComp(map != -1 ? val.getOutputType().getNames().get(map) : I18n.get("discarded"));
grid.add(output.createRegion(), 2, i);
if (i % 2 != 0) {
grid.getChildren().stream().skip((i * 3)).forEach(node -> node.getStyleClass()
.add("odd"));
}
}
});
return grid;
}
}

View file

@ -0,0 +1,34 @@
package io.xpipe.app.comp.source;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.CompStructure;
import io.xpipe.extension.fxcomps.SimpleCompStructure;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.TextArea;
import java.util.List;
public class DsTextComp extends Comp<CompStructure<TextArea>> {
private final ObservableValue<List<String>> value;
public DsTextComp(ObservableValue<List<String>> value) {
this.value = value;
}
private void setupListener(TextArea ta) {
ChangeListener<List<String>> listener = (c, o, n) -> {
ta.textProperty().setValue(String.join("\n", n));
};
value.addListener(listener);
listener.changed(value, null, value.getValue());
}
@Override
public CompStructure<TextArea> createBase() {
var ta = new TextArea();
setupListener(ta);
return new SimpleCompStructure<>(ta);
}
}

View file

@ -0,0 +1,82 @@
package io.xpipe.app.comp.source;
import io.xpipe.app.comp.storage.DataSourceTypeComp;
import io.xpipe.core.source.DataSource;
import io.xpipe.core.source.DataSourceType;
import io.xpipe.extension.DataSourceProvider;
import io.xpipe.extension.I18n;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.CompStructure;
import io.xpipe.extension.fxcomps.SimpleCompStructure;
import io.xpipe.extension.util.CustomComboBoxBuilder;
import javafx.beans.property.Property;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.Arrays;
public class DsTypeChoiceComp extends Comp<CompStructure<StackPane>> {
private final ObservableValue<? extends DataSource<?>> baseSource;
private final ObservableValue<DataSourceProvider<?>> provider;
private final Property<DataSourceType> selectedType;
public DsTypeChoiceComp(
ObservableValue<? extends DataSource<?>> baseSource,
ObservableValue<DataSourceProvider<?>> provider,
Property<DataSourceType> selectedType) {
this.baseSource = baseSource;
this.provider = provider;
this.selectedType = selectedType;
}
private Region createLabel(DataSourceType p) {
var l = new Label(I18n.get(p.name().toLowerCase()), new FontIcon(DataSourceTypeComp.ICONS.get(p)));
l.setAlignment(Pos.CENTER);
return l;
}
@Override
public CompStructure<StackPane> createBase() {
var sp = new StackPane();
Runnable update = () -> {
sp.getChildren().clear();
if (provider.getValue() == null || baseSource.getValue() == null) {
return;
}
var builder = new CustomComboBoxBuilder<>(selectedType, app -> createLabel(app), new Label(""), v -> true);
builder.add(provider.getValue().getPrimaryType());
var list = Arrays.stream(DataSourceType.values())
.filter(t -> t != provider.getValue().getPrimaryType()
&& provider.getValue()
.supportsConversion(baseSource.getValue().asNeeded(), t))
.toList();
if (list.size() == 0) {
return;
}
list.forEach(t -> builder.add(t));
var cb = builder.build();
cb.getStyleClass().add("data-source-type-choice-comp");
sp.getChildren().add(cb);
};
baseSource.addListener((c, o, n) -> {
update.run();
});
provider.addListener((c, o, n) -> {
update.run();
});
update.run();
return new SimpleCompStructure<>(sp);
}
}

View file

@ -0,0 +1,279 @@
package io.xpipe.app.comp.source;
import io.xpipe.app.comp.base.MultiStepComp;
import io.xpipe.app.core.AppFont;
import io.xpipe.core.data.node.ArrayNode;
import io.xpipe.core.data.node.TupleNode;
import io.xpipe.core.source.*;
import io.xpipe.core.store.DataFlow;
import io.xpipe.core.store.DataStore;
import io.xpipe.extension.DataSourceProvider;
import io.xpipe.extension.event.ErrorEvent;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.CompStructure;
import io.xpipe.extension.fxcomps.impl.HorizontalComp;
import io.xpipe.extension.fxcomps.impl.IconButtonComp;
import io.xpipe.extension.fxcomps.impl.VerticalComp;
import io.xpipe.extension.util.BusyProperty;
import io.xpipe.extension.util.ThreadHelper;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Pos;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class GuiDsConfigStep extends MultiStepComp.Step<CompStructure<?>> {
private final Property<? extends DataStore> input;
private final ObservableValue<? extends DataSource<?>> baseSource;
private final Property<? extends DataSource<?>> currentSource;
private final Property<DataSourceProvider<?>> provider;
private final Property<DataSourceType> type;
private final Map<DataSourceType, Comp<?>> previewComps;
private final BooleanProperty loading;
public GuiDsConfigStep(
Property<DataSourceProvider<?>> provider,
Property<? extends DataStore> input,
Property<? extends DataSource<?>> baseSource,
Property<? extends DataSource<?>> currentSource,
Property<DataSourceType> type,
BooleanProperty loading) {
super(null);
this.input = input;
this.baseSource = baseSource;
this.currentSource = currentSource;
this.type = type;
this.provider = provider;
this.loading = loading;
this.previewComps = new HashMap<>();
Arrays.stream(DataSourceType.values()).forEach(t -> previewComps.put(t, createPreviewComp(t)));
}
@Override
public CompStructure<?> createBase() {
var hide = Bindings.createBooleanBinding(
() -> {
return currentSource.getValue() != null
&& currentSource.getValue().getFlow() == DataFlow.OUTPUT;
},
currentSource);
var top = new HorizontalComp(List.of(createTypeSelectorComp(), Comp.of(Region::new), createRefreshComp()))
.apply(s -> {
HBox.setHgrow(s.get().getChildren().get(1), Priority.ALWAYS);
s.get().setAlignment(Pos.CENTER);
s.get().setSpacing(7);
})
.hide(hide);
var preview = Bindings.createObjectBinding(
() -> {
return previewComps.get(type.getValue()).hide(hide);
},
type);
var layout = new VerticalComp(List.of(top, preview.getValue(), Comp.of(this::createConfigOptions)));
layout.apply(vbox -> {
vbox.get().setAlignment(Pos.CENTER);
VBox.setVgrow(vbox.get().getChildren().get(1), Priority.ALWAYS);
AppFont.small(vbox.get());
currentSource.addListener((c, o, n) -> {
vbox.get().getChildren().set(1, preview.getValue().createRegion());
VBox.setVgrow(vbox.get().getChildren().get(1), Priority.ALWAYS);
});
provider.addListener((c, o, n) -> {
vbox.get().getChildren().set(2, createConfigOptions());
});
})
.styleClass("data-source-config");
provider.addListener((c, o, n) -> {});
return layout.createStructure();
}
@SuppressWarnings("unchecked")
private <T extends DataSource<?>> Region createConfigOptions() {
if (currentSource.getValue() == null || provider.getValue() == null) {
return new Region();
}
Region r = null;
try {
r = ((DataSourceProvider<T>) provider.getValue()).configGui((Property<T>) baseSource, false);
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
}
return r != null ? r : new Region();
}
private Comp<?> createReportComp() {
return new IconButtonComp("mdi2a-alert-circle-outline", () -> {});
}
@SuppressWarnings("unchecked")
private <T extends DataSource<?>> Comp<?> createRefreshComp() {
return new IconButtonComp("mdmz-refresh", () -> {
T src = currentSource.getValue() != null
? currentSource.getValue().asNeeded()
: null;
currentSource.setValue(null);
((Property<T>) currentSource).setValue(src);
})
.shortcut(new KeyCodeCombination(KeyCode.F5));
}
private Comp<?> createTypeSelectorComp() {
return new DsTypeChoiceComp(baseSource, provider, type);
}
private Comp<?> createPreviewComp(DataSourceType t) {
return switch (t) {
case TABLE -> createTablePreviewComp();
case STRUCTURE -> createStructurePreviewComp();
case TEXT -> createTextPreviewComp();
case RAW -> createRawPreviewComp();
case COLLECTION -> createCollectionPreviewComp();
};
}
@SuppressWarnings("unchecked")
private <DI extends DataStore, DS extends TextDataSource<DI>> Comp<?> createTextPreviewComp() {
var text = Bindings.createObjectBinding(
() -> {
if (currentSource.getValue() == null || type.getValue() != DataSourceType.TEXT) {
return List.of("");
}
try (var con = ((DS) currentSource.getValue()).openReadConnection()) {
con.init();
return con.lines().limit(10).toList();
} catch (Exception e) {
ErrorEvent.fromThrowable(e).build().handle();
return List.of("");
}
},
currentSource);
return new DsTextComp(text);
}
@SuppressWarnings("unchecked")
private <DI extends DataStore, DS extends StructureDataSource<DI>> Comp<?> createStructurePreviewComp() {
var structure = Bindings.createObjectBinding(
() -> {
if (currentSource.getValue() == null || type.getValue() != DataSourceType.STRUCTURE) {
return TupleNode.builder().build();
}
try (var con = ((DS) currentSource.getValue()).openReadConnection()) {
con.init();
return con.read();
} catch (Exception e) {
ErrorEvent.fromThrowable(e).build().handle();
return TupleNode.builder().build();
}
},
currentSource);
return new DsStructureComp(structure);
}
@Override
public void onInit() {
// DataSource<?> src = currentSource.getValue() != null ? currentSource.getValue().asNeeded() : null;
// currentSource.setValue(null);
// currentSource.setValue(src != null ? src.asNeeded() : null);
}
private Comp<?> createTablePreviewComp() {
var table = new SimpleObjectProperty<>(ArrayNode.of());
currentSource.addListener((c, o, val) -> {
ThreadHelper.runAsync(() -> {
if (val == null
|| type.getValue() != DataSourceType.TABLE
|| !val.getFlow().hasInput()) {
return;
}
try (var ignored = new BusyProperty(loading);
var con = (TableReadConnection) val.openReadConnection()) {
con.init();
table.set(con.readRows(50));
} catch (Exception e) {
ErrorEvent.fromThrowable(e).build().handle();
table.set(ArrayNode.of());
}
});
});
return new DsTableComp(table);
}
@SuppressWarnings("unchecked")
private <DI extends DataStore, DS extends RawDataSource<DI>> Comp<?> createRawPreviewComp() {
var bytes = Bindings.createObjectBinding(
() -> {
if (currentSource.getValue() == null || type.getValue() != DataSourceType.RAW) {
return new byte[0];
}
try (var con = ((DS) currentSource.getValue()).openReadConnection()) {
con.init();
return con.readBytes(1000);
} catch (Exception e) {
ErrorEvent.fromThrowable(e).build().handle();
return new byte[0];
}
},
currentSource);
return new DsRawComp(bytes);
}
@SuppressWarnings("unchecked")
private <DI extends DataStore, DS extends CollectionDataSource<DI>> Comp<?> createCollectionPreviewComp() {
var con = Bindings.createObjectBinding(
() -> {
if (currentSource.getValue() == null || type.getValue() != DataSourceType.COLLECTION) {
return null;
}
/*
TODO: Fix
*/
try {
return ((DS) currentSource.getValue()).openReadConnection();
} catch (Exception e) {
ErrorEvent.fromThrowable(e).build().handle();
return null;
}
},
currentSource,
input);
con.addListener((c, o, n) -> {
if (o == null) {
return;
}
try {
o.close();
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
}
});
return new DsCollectionComp(con);
}
}

View file

@ -0,0 +1,296 @@
package io.xpipe.app.comp.source;
import io.xpipe.app.comp.base.MultiStepComp;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppWindowHelper;
import io.xpipe.app.storage.DataSourceCollection;
import io.xpipe.app.storage.DataSourceEntry;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.core.source.DataSource;
import io.xpipe.core.source.DataSourceType;
import io.xpipe.core.store.DataStore;
import io.xpipe.extension.DataSourceProvider;
import io.xpipe.extension.DataSourceProviders;
import io.xpipe.extension.I18n;
import io.xpipe.extension.event.ErrorEvent;
import javafx.application.Platform;
import javafx.beans.property.*;
import javafx.scene.control.Alert;
import javafx.scene.layout.Region;
import javafx.stage.Stage;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
public class GuiDsCreatorMultiStep<DI extends DataStore, DS extends DataSource<DI>> extends MultiStepComp {
private final Stage window;
private final DataSourceEntry editing;
private final DataSourceCollection targetGroup;
private final Property<DataSourceType> dataSourceType;
private final DataSourceProvider.Category category;
private final Property<DataSourceProvider<?>> provider;
private final Property<DI> store;
private final ObjectProperty<DS> baseSource;
private final ObjectProperty<DS> source;
private final BooleanProperty loading = new SimpleBooleanProperty();
private final State state;
private GuiDsCreatorMultiStep(
Stage window,
DataSourceEntry editing, DataSourceCollection targetGroup,
DataSourceProvider.Category category,
DataSourceProvider<?> provider,
DI store,
DS source,
State state) {
this.window = window;
this.editing = editing;
this.targetGroup = targetGroup;
this.category = category;
this.provider = new SimpleObjectProperty<>(provider);
this.store = new SimpleObjectProperty<>(store);
this.dataSourceType = new SimpleObjectProperty<>(provider != null ? provider.getPrimaryType() : null);
this.baseSource = new SimpleObjectProperty<>(source);
this.source = new SimpleObjectProperty<>(source);
this.state = state;
addListeners();
this.apply(r -> {
r.get().setPrefWidth(AppFont.em(30));
r.get().setPrefHeight(AppFont.em(35));
});
}
public static void showCreation(DataSourceProvider.Category category, DataSourceCollection sourceCollection) {
Platform.runLater(() -> {
var loading = new SimpleBooleanProperty();
var stage = AppWindowHelper.sideWindow(
I18n.get("newDataSource"),
window -> {
var ms = new GuiDsCreatorMultiStep<>(
window,
null, sourceCollection,
category,
null,
null,
null,
State.CREATE);
loading.bind(ms.loading);
window.setOnCloseRequest(e -> {
if (ms.state == State.CREATE && ms.source.getValue() != null) {
e.consume();
showCloseConfirmAlert(ms, window);
}
});
return ms;
},
false,
loading);
stage.show();
});
}
public static void showEdit(DataSourceEntry entry) {
Platform.runLater(() -> {
var loading = new SimpleBooleanProperty();
var stage = AppWindowHelper.sideWindow(
I18n.get("editDataSource"),
window -> {
var ms = new GuiDsCreatorMultiStep<>(
window,
entry, DataStorage.get()
.getCollectionForSourceEntry(entry)
.orElse(null),
entry.getProvider().getCategory(),
entry.getProvider(),
entry.getStore().asNeeded(),
entry.getSource().asNeeded(),
State.EDIT);
loading.bind(ms.loading);
return ms.apply(struc -> ms.next());
},
false,
loading);
stage.show();
});
}
public static Future<Boolean> showForStore(
DataSourceProvider.Category category, DataStore store, DataSourceCollection sourceCollection) {
CompletableFuture<Boolean> completableFuture = new CompletableFuture<>();
var provider = DataSourceProviders.byPreferredStore(store, null);
Platform.runLater(() -> {
var stage = AppWindowHelper.sideWindow(
I18n.get("newDataSource"),
window -> {
var gui = new GuiDsCreatorMultiStep<>(
window,
null, sourceCollection,
category,
provider.orElse(null),
store,
null,
State.CREATE);
gui.completedProperty().addListener((c, o, n) -> {
if (n) {
completableFuture.complete(true);
}
});
window.setOnCloseRequest(e -> {
if (gui.state == State.CREATE && gui.source.getValue() != null) {
e.consume();
showCloseConfirmAlert(gui, window);
}
});
return gui;
},
false,
null);
stage.show();
stage.setOnHiding(e -> {
completableFuture.complete(false);
});
});
return completableFuture;
}
private static void showCloseConfirmAlert(GuiDsCreatorMultiStep<?, ?> ms, Stage s) {
AppWindowHelper.showBlockingAlert(alert -> {
alert.setTitle(I18n.get("confirmDsCreationAbortTitle"));
alert.setHeaderText(I18n.get("confirmDsCreationAbortHeader"));
alert.setContentText(I18n.get("confirmDsCreationAbortContent"));
alert.setAlertType(Alert.AlertType.CONFIRMATION);
})
.filter(b -> b.getButtonData().isDefaultButton())
.ifPresent(t -> {
s.close();
});
}
@SuppressWarnings("unchecked")
private void addListeners() {
this.provider.addListener((c, o, n) -> {
if (n == null) {
this.dataSourceType.setValue(null);
return;
}
if (baseSource.getValue() != null
&& !n.getSourceClass().equals(baseSource.get().getClass())) {
this.baseSource.setValue(null);
}
this.dataSourceType.setValue(n.getPrimaryType());
});
this.store.addListener((c, o, n) -> {
if (n == null) {
return;
}
if (this.provider.getValue() == null) {
this.provider.setValue(
DataSourceProviders.byPreferredStore(n, null).orElse(null));
if (this.provider.getValue() != null) {
try {
this.baseSource.set((DS) provider.getValue().createDefaultSource(n));
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
}
}
}
});
this.baseSource.addListener((c, o, n) -> {
if (n == null) {
this.source.set(null);
return;
}
try {
var converted = dataSourceType.getValue() != provider.getValue().getPrimaryType()
? (provider.getValue()).convert(n.asNeeded(), dataSourceType.getValue())
: n;
source.setValue((DS) converted);
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
}
});
this.dataSourceType.addListener((c, o, n) -> {
if (n == null || source.get() == null) {
return;
}
if (n == this.provider.getValue().getPrimaryType()) {
this.source.set(baseSource.getValue());
return;
}
try {
var conv = this.provider.getValue().convert(baseSource.get().asNeeded(), n);
this.source.set(conv.asNeeded());
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
}
});
}
@Override
protected Region createStepOverview(Region content) {
var r = super.createStepOverview(content);
AppFont.small(r);
return r;
}
@Override
protected Region createStepNavigation() {
var r = super.createStepNavigation();
AppFont.small(r);
return r;
}
@Override
protected List<Entry> setup() {
var list = new ArrayList<Entry>();
list.add(new Entry(I18n.observable("selectInput"), createInputStep()));
list.add(new Entry(
I18n.observable("configure"),
new GuiDsConfigStep(provider, store, baseSource, source, dataSourceType, loading)));
switch (state) {
case EDIT -> {}
case CREATE -> {
list.add(new Entry(I18n.observable("target"), new GuiDsCreatorTransferStep(targetGroup, store, source)));
}
}
return list;
}
@SuppressWarnings("unchecked")
private MultiStepComp.Step<?> createInputStep() {
return new GuiDsStoreSelectStep(
this, provider, (ObjectProperty<DataStore>) store, category, baseSource, loading);
}
@Override
protected void finish() {
switch (state) {
case EDIT -> {
editing.setSource(source.get());
}
case CREATE -> {}
}
window.close();
}
public static enum State {
EDIT,
CREATE
}
}

View file

@ -0,0 +1,73 @@
package io.xpipe.app.comp.source;
import com.jfoenix.controls.JFXCheckBox;
import io.xpipe.app.comp.base.MultiStepComp;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.storage.DataSourceCollection;
import io.xpipe.app.storage.DataSourceEntry;
import io.xpipe.extension.I18n;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.CompStructure;
import io.xpipe.extension.fxcomps.impl.VerticalComp;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.control.Label;
import java.util.ArrayList;
import java.util.List;
public class GuiDsCreatorSaveStep extends MultiStepComp.Step<CompStructure<?>> {
private final Property<DataSourceCollection> storageGroup;
private final Property<DataSourceEntry> dataSourceEntry;
private final Property<Boolean> nameValid = new SimpleObjectProperty<>(true);
private final Property<Boolean> storeForLaterUse = new SimpleBooleanProperty(true);
public GuiDsCreatorSaveStep(
Property<DataSourceCollection> storageGroup, Property<DataSourceEntry> dataSourceEntry) {
super(null);
this.storageGroup = storageGroup;
this.dataSourceEntry = dataSourceEntry;
}
@Override
public CompStructure<?> createBase() {
var storeSwitch = Comp.of(() -> {
var cb = new JFXCheckBox();
cb.selectedProperty().bindBidirectional(storeForLaterUse);
var label = new Label(I18n.get("storeForLaterUse"));
label.setGraphic(cb);
return label;
});
var storeSettings = new VerticalComp(List.of(new DsStorageTargetComp(dataSourceEntry, storageGroup, nameValid)))
.apply(struc -> {
var elems = new ArrayList<>(struc.get().getChildren());
if (!storeForLaterUse.getValue()) {
struc.get().getChildren().clear();
}
storeForLaterUse.addListener((c, o, n) -> {
if (n) {
struc.get().getChildren().addAll(elems);
} else {
struc.get().getChildren().clear();
}
});
})
.styleClass("store-options");
var vert = new VerticalComp(List.of(storeSwitch, storeSettings));
vert.styleClass("data-source-save-step");
vert.apply(r -> AppFont.small(r.get()));
return vert.createStructure();
}
@Override
public boolean canContinue() {
return nameValid.getValue();
}
}

View file

@ -0,0 +1,112 @@
package io.xpipe.app.comp.source;
import io.xpipe.app.comp.base.MultiStepComp;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.storage.DataSourceCollection;
import io.xpipe.app.storage.DataSourceEntry;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.core.source.DataSource;
import io.xpipe.core.store.DataStore;
import io.xpipe.extension.DataSourceTarget;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.CompStructure;
import io.xpipe.extension.fxcomps.impl.VerticalComp;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import java.util.List;
import java.util.UUID;
public class GuiDsCreatorTransferStep extends MultiStepComp.Step<CompStructure<?>> {
private final DataSourceCollection targetGroup;
private final Property<? extends DataStore> store;
private final ObjectProperty<? extends DataSource<?>> source;
private final Property<DataSourceEntry> entry = new SimpleObjectProperty<>();
private DsDataTransferComp comp;
public GuiDsCreatorTransferStep(
DataSourceCollection targetGroup,
Property<? extends DataStore> store,
ObjectProperty<? extends DataSource<?>> source) {
super(null);
this.targetGroup = targetGroup;
this.store = store;
this.source = source;
entry.bind(Bindings.createObjectBinding(
() -> {
if (this.store.getValue() == null || this.source.get() == null) {
return null;
}
var name = DataStorage.get().createUniqueSourceEntryName(DataStorage.get().getInternalCollection(), source.get());
var entry = DataSourceEntry.createNew(UUID.randomUUID(), name, this.source.get());
return entry;
},
this.store,
this.source));
}
@Override
public CompStructure<?> createBase() {
comp = new DsDataTransferComp(entry)
.selectApplication(
targetGroup != null
? DataSourceTarget.byId("base.saveSource").orElseThrow()
: null);
var vert = new VerticalComp(List.of(comp.apply(s -> VBox.setVgrow(s.get(), Priority.ALWAYS))));
vert.styleClass("data-source-finish-step");
vert.apply(r -> AppFont.small(r.get()));
return Comp.derive(vert, vBox -> {
var r = new ScrollPane(vBox);
r.setFitToWidth(true);
return r;
})
.createStructure();
}
@Override
public void onInit() {
var e = entry.getValue();
DataStorage.get().add(e, DataStorage.get().getInternalCollection());
}
@Override
public void onBack() {
var e = entry.getValue();
DataStorage.get().deleteEntry(e);
}
@Override
public void onContinue() {
var onFinish = comp.getSelectedDisplay().getValue().getOnFinish();
if (onFinish != null) {
onFinish.run();
}
var e = entry.getValue();
DataStorage.get().deleteEntry(e);
}
@Override
public boolean canContinue() {
var selected = comp.getSelectedTarget().getValue();
if (selected == null) {
return false;
}
var validator = comp.getSelectedDisplay().getValue().getValidator();
if (validator == null) {
return true;
}
return validator.validate();
}
}

View file

@ -0,0 +1,124 @@
package io.xpipe.app.comp.source;
import io.xpipe.app.comp.base.MultiStepComp;
import io.xpipe.app.comp.source.store.DsDbStoreChooserComp;
import io.xpipe.app.comp.source.store.DsStreamStoreChoiceComp;
import io.xpipe.app.util.Hyperlinks;
import io.xpipe.core.source.DataSource;
import io.xpipe.core.store.DataStore;
import io.xpipe.extension.DataSourceProvider;
import io.xpipe.extension.event.ErrorEvent;
import io.xpipe.extension.event.TrackEvent;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.CompStructure;
import io.xpipe.extension.fxcomps.augment.GrowAugment;
import io.xpipe.extension.fxcomps.impl.StackComp;
import io.xpipe.extension.fxcomps.util.PlatformThread;
import io.xpipe.extension.util.BusyProperty;
import io.xpipe.extension.util.ThreadHelper;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.Property;
import javafx.scene.control.Separator;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import java.util.List;
public class GuiDsStoreSelectStep extends MultiStepComp.Step<CompStructure<? extends Region>> {
private final MultiStepComp parent;
private final Property<DataSourceProvider<?>> provider;
private final Property<DataStore> input;
private final DataSourceProvider.Category category;
private final ObjectProperty<? extends DataSource<?>> baseSource;
private final BooleanProperty loading;
public GuiDsStoreSelectStep(
MultiStepComp parent,
Property<DataSourceProvider<?>> provider,
Property<DataStore> input,
DataSourceProvider.Category category,
ObjectProperty<? extends DataSource<?>> baseSource,
BooleanProperty loading) {
super(Hyperlinks.openLink(Hyperlinks.DOCS_DATA_INPUT));
this.parent = parent;
this.provider = provider;
this.input = input;
this.category = category;
this.baseSource = baseSource;
this.loading = loading;
}
private Region createLayout() {
var layout = new BorderPane();
var providerChoice = new DsProviderChoiceComp(category, provider, null);
providerChoice.apply(GrowAugment.create(true, false));
layout.setCenter(createCategoryChooserComp());
var top = new VBox(providerChoice.createRegion(), new Separator());
top.getStyleClass().add("top");
layout.setTop(top);
layout.getStyleClass().add("data-input-creation-step");
return layout;
}
private Region createCategoryChooserComp() {
if (category == DataSourceProvider.Category.STREAM) {
return new DsStreamStoreChoiceComp(input, provider, true, true, DsStreamStoreChoiceComp.Mode.OPEN).createRegion();
}
if (category == DataSourceProvider.Category.DATABASE) {
return new DsDbStoreChooserComp(input, provider).createRegion();
}
throw new AssertionError();
}
@Override
public CompStructure<? extends Region> createBase() {
// var bgImg = AppImages.image("plus_bg.jpg");
// var background = new BackgroundImageComp(bgImg)
// .apply(struc -> struc.get().setOpacity(0.1));
var layered = new StackComp(List.of(Comp.of(this::createLayout)));
return layered.createStructure();
}
@Override
public void onContinue() {}
@Override
public boolean canContinue() {
if (input.getValue() == null || provider.getValue() == null) {
return false;
}
if (baseSource.getValue() != null) {
return true;
}
ThreadHelper.runAsync(() -> {
try (var ignored = new BusyProperty(loading)) {
var n = this.input.getValue();
var ds = this.provider.getValue().createDefaultSource(n);
if (ds == null) {
TrackEvent.warn("Default data source is null");
return;
}
PlatformThread.runLaterBlocking(() -> {
baseSource.setValue(ds.asNeeded());
parent.next();
});
} catch (Exception e) {
ErrorEvent.fromThrowable(e).build().handle();
PlatformThread.runLaterBlocking(() -> {
baseSource.setValue(null);
});
}
});
return false;
}
}

View file

@ -0,0 +1,107 @@
package io.xpipe.app.comp.source;
import io.xpipe.app.comp.base.MultiStepComp;
import io.xpipe.app.comp.storage.source.SourceEntryWrapper;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppWindowHelper;
import io.xpipe.core.source.TableMapping;
import io.xpipe.extension.I18n;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.CompStructure;
import io.xpipe.extension.fxcomps.SimpleComp;
import io.xpipe.extension.fxcomps.impl.LabelComp;
import io.xpipe.extension.fxcomps.impl.VerticalComp;
import io.xpipe.extension.fxcomps.util.PlatformThread;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Separator;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import lombok.EqualsAndHashCode;
import lombok.Value;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
@Value
@EqualsAndHashCode(callSuper = true)
public class GuiDsTableMappingConfirmation extends SimpleComp {
ObservableValue<TableMapping> mapping;
public GuiDsTableMappingConfirmation(ObservableValue<TableMapping> mapping) {
this.mapping = PlatformThread.sync(mapping);
}
public static boolean showWindowAndWait(SourceEntryWrapper source, TableMapping mapping) {
var latch = new CountDownLatch(1);
var confirmed = new AtomicBoolean();
AppWindowHelper.showAndWaitForWindow(() -> {
var stage = AppWindowHelper.sideWindow(
I18n.get("confirmTableMappingTitle"),
window -> {
var ms = new GuiDsTableMappingConfirmation(new SimpleObjectProperty<>(mapping));
var multi = new MultiStepComp() {
@Override
protected List<Entry> setup() {
return List.of(new Entry(null, new Step<>(null) {
@Override
public CompStructure<?> createBase() {
return ms.createStructure();
}
}));
}
@Override
protected void finish() {
confirmed.set(true);
window.close();
}
};
return multi.apply(s -> {
s.get().setPrefWidth(400);
s.get().setPrefHeight(500);
AppFont.medium(s.get());
});
},
false,
null);
stage.setOnHidden(event -> latch.countDown());
return stage;
});
return confirmed.get();
}
@Override
protected Region createSimple() {
var header = new LabelComp(I18n.observable("confirmTableMapping"))
.apply(struc -> struc.get().setWrapText(true));
var content = Comp.derive(new DsTableMappingComp(mapping), region -> {
var box = new HBox(region);
box.setAlignment(Pos.CENTER);
box.getStyleClass().add("grid-container");
return box;
})
.apply(struc -> AppFont.normal(struc.get()));
var changeNotice = new LabelComp(I18n.observable("changeTableMapping"))
.apply(struc -> struc.get().setWrapText(true));
var changeButton = Comp.of(() -> {
var hl = new Hyperlink("Customizing Data Flows");
hl.setOnAction(e -> {});
hl.setMaxWidth(250);
return hl;
});
return new VerticalComp(List.of(
header,
content,
Comp.of(() -> new Separator(Orientation.HORIZONTAL)),
changeNotice,
changeButton))
.styleClass("table-mapping-confirmation-comp")
.createRegion();
}
}

View file

@ -0,0 +1,146 @@
package io.xpipe.app.comp.source;
import io.xpipe.app.comp.base.ListViewComp;
import io.xpipe.app.storage.DataSourceEntry;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.JfxHelper;
import io.xpipe.core.source.DataSource;
import io.xpipe.extension.DataSourceProvider;
import io.xpipe.extension.DataStoreProviders;
import io.xpipe.extension.I18n;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.SimpleComp;
import io.xpipe.extension.fxcomps.impl.FilterComp;
import io.xpipe.extension.fxcomps.impl.LabelComp;
import io.xpipe.extension.fxcomps.impl.StackComp;
import io.xpipe.extension.fxcomps.impl.VerticalComp;
import io.xpipe.extension.fxcomps.util.BindingsHelper;
import io.xpipe.extension.util.SimpleValidator;
import io.xpipe.extension.util.Validatable;
import io.xpipe.extension.util.Validator;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import lombok.Getter;
import net.synedra.validatorfx.Check;
import java.util.Collection;
import java.util.List;
import java.util.function.Predicate;
public class NamedSourceChoiceComp extends SimpleComp implements Validatable {
private final ObservableValue<Predicate<DataSource<?>>> filter;
private final DataSourceProvider.Category category;
private final Property<? extends DataSource<?>> selected;
private final StringProperty filterString = new SimpleStringProperty();
@Getter
private final Validator validator = new SimpleValidator();
private final Check check;
public NamedSourceChoiceComp(
ObservableValue<Predicate<DataSource<?>>> filter,
Property<? extends DataSource<?>> selected,
DataSourceProvider.Category category) {
this.filter = filter;
this.selected = selected;
this.category = category;
check = Validator.nonNull(validator, I18n.observable("source"), selected);
}
@SuppressWarnings("unchecked")
private <T extends DataSource<?>> void setUpListener(ObservableValue<T> prop) {
prop.addListener((c, o, n) -> {
((Property<T>) selected).setValue((T) n);
});
}
private <T extends DataSource<?>> void refreshShown(ObservableList<T> list, ObservableList<T> shown) {
var filtered = list.filtered(source -> {
if (!filter.getValue().test(source)) {
return false;
}
var e = DataStorage.get().getEntryBySource(source).orElseThrow();
return filterString.get() == null
|| e.getName().toLowerCase().contains(filterString.get().toLowerCase());
});
shown.removeIf(store -> !filtered.contains(store));
filtered.forEach(store -> {
if (!shown.contains(store)) {
shown.add(store);
}
});
}
@SuppressWarnings("unchecked")
private <T extends DataSource<?>> Region create() {
var list = FXCollections.observableList(DataStorage.get().getSourceCollections().stream()
.map(dataSourceCollection -> dataSourceCollection.getEntries())
.flatMap(Collection::stream)
.filter(entry -> entry.getState().isUsable())
.map(DataSourceEntry::getSource)
.map(source -> (T) source)
.toList());
var shown = FXCollections.<T>observableArrayList();
refreshShown(list, shown);
filter.addListener((observable, oldValue, newValue) -> {
refreshShown(list, shown);
});
filterString.addListener((observable, oldValue, newValue) -> {
refreshShown(list, shown);
});
var prop = new SimpleObjectProperty<T>();
setUpListener(prop);
var filterComp = new FilterComp(filterString).hide(Bindings.greaterThan(5, Bindings.size(shown)));
var view = new ListViewComp<>(shown, list, prop, (T s) -> {
var e = DataStorage.get().getEntryBySource(s).orElseThrow();
var provider = e.getProvider();
var graphic = provider.getDisplayIconFileName();
var top = String.format("%s (%s)", e.getName(), provider.getDisplayName());
var bottom = DataStoreProviders.byStore(e.getStore()).toSummaryString(e.getStore(), 100);
var el = JfxHelper.createNamedEntry(top, bottom, graphic);
VBox.setVgrow(el, Priority.ALWAYS);
return Comp.of(() -> el);
})
.apply(struc -> {
struc.get().setMaxHeight(3500);
check.decorates(struc.get());
});
var box = new VerticalComp(List.of(filterComp, view));
var text = new LabelComp(I18n.observable("noMatchingSourceFound"))
.apply(struc -> VBox.setVgrow(struc.get(), Priority.ALWAYS));
var notice = new VerticalComp(List.of(text))
.apply(struc -> {
struc.get().setSpacing(10);
struc.get().setAlignment(Pos.CENTER);
})
.hide(BindingsHelper.persist(Bindings.notEqual(0, Bindings.size(shown))));
return new StackComp(List.of(box, notice))
.styleClass("named-source-choice")
.createRegion();
}
@Override
protected Region createSimple() {
return create();
}
}

View file

@ -0,0 +1,68 @@
package io.xpipe.app.comp.source.store;
import com.jfoenix.controls.JFXButton;
import io.xpipe.app.util.JfxHelper;
import io.xpipe.core.impl.FileStore;
import io.xpipe.core.store.DataStore;
import io.xpipe.extension.DataStoreProvider;
import io.xpipe.extension.DataStoreProviders;
import io.xpipe.extension.I18n;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.CompStructure;
import io.xpipe.extension.fxcomps.SimpleCompStructure;
import io.xpipe.extension.fxcomps.util.PlatformThread;
import javafx.beans.property.Property;
import javafx.scene.control.Button;
import javafx.scene.layout.Region;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.experimental.FieldDefaults;
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
@AllArgsConstructor
public class DataStoreSelectorComp extends Comp<CompStructure<Button>> {
DataStoreProvider.Category category;
Property<DataStore> chosenStore;
@Override
public CompStructure<Button> createBase() {
var button = new JFXButton();
button.setGraphic(getGraphic());
button.setOnAction(e -> {
GuiDsStoreCreator.show("inProgress", null, null, category, entry -> {
chosenStore.setValue(entry.getStore());
});
e.consume();
});
Runnable update = () -> {
PlatformThread.runLaterIfNeeded(() -> {
var newGraphic = getGraphic();
button.setGraphic(newGraphic);
button.layout();
});
};
chosenStore.addListener((c, o, n) -> {
update.run();
});
return new SimpleCompStructure<>(button);
}
private Region getGraphic() {
var provider = chosenStore.getValue() != null
? DataStoreProviders.byStoreClass(chosenStore.getValue().getClass())
.orElse(null)
: null;
var graphic = provider != null ? provider.getDisplayIconFileName() : "file_icon.png";
if (chosenStore.getValue() == null || !(chosenStore.getValue() instanceof FileStore f)) {
return JfxHelper.createNamedEntry(
I18n.get("selectStreamStore"), I18n.get("openStreamStoreWizard"), graphic);
} else {
return JfxHelper.createNamedEntry(
f.getFileName().toString(), f.getFile().toString(), graphic);
}
}
}

View file

@ -0,0 +1,52 @@
package io.xpipe.app.comp.source.store;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.core.store.DataStore;
import io.xpipe.extension.DataSourceProvider;
import io.xpipe.extension.DataStoreProvider;
import io.xpipe.extension.I18n;
import io.xpipe.extension.fxcomps.SimpleComp;
import io.xpipe.extension.fxcomps.impl.TabPaneComp;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.layout.Region;
import java.util.List;
import java.util.function.Predicate;
public class DsDbStoreChooserComp extends SimpleComp {
private final Property<DataStore> input;
private final ObservableValue<DataSourceProvider<?>> provider;
public DsDbStoreChooserComp(Property<DataStore> input, ObservableValue<DataSourceProvider<?>> provider) {
this.input = input;
this.provider = provider;
}
@Override
protected Region createSimple() {
var filter = Bindings.createObjectBinding(
() -> (Predicate<DataStoreEntry>) e -> {
if (provider.getValue() == null) {
return e.getProvider().getCategory() == DataStoreProvider.Category.DATABASE;
}
return provider.getValue().couldSupportStore(e.getStore());
},
provider);
var connections = new TabPaneComp.Entry(
I18n.observable("savedConnections"),
"mdi2m-monitor",
NamedStoreChoiceComp.create(filter, input, DataStoreProvider.Category.DATABASE)
.styleClass("store-local-file-chooser"));
var pane = new TabPaneComp(new SimpleObjectProperty<>(connections), List.of(connections));
pane.apply(s -> AppFont.normal(s.get()));
return pane.createRegion();
}
}

View file

@ -0,0 +1,61 @@
package io.xpipe.app.comp.source.store;
import com.jfoenix.controls.JFXButton;
import io.xpipe.app.core.AppCache;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.util.JfxHelper;
import io.xpipe.core.impl.FileStore;
import io.xpipe.extension.DataSourceProvider;
import io.xpipe.extension.I18n;
import io.xpipe.extension.fxcomps.SimpleComp;
import javafx.beans.property.Property;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import org.apache.commons.io.FilenameUtils;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
public class DsFileHistoryComp extends SimpleComp {
private final DataSourceProvider<?> provider;
private final Property<FileStore> file;
public DsFileHistoryComp(DataSourceProvider<?> provider, Property<FileStore> file) {
this.provider = provider;
this.file = file;
}
@Override
public Region createSimple() {
var previous = new VBox();
List<String> cached = AppCache.get("csv-data-sources", List.class, ArrayList::new);
if (cached.size() == 0) {
return previous;
}
previous.setFillWidth(true);
var label = new Label(I18n.get("recentFiles"));
AppFont.header(label);
previous.getChildren().add(label);
cached.forEach(s -> {
var graphic = provider.getDisplayIconFileName();
var el = JfxHelper.createNamedEntry(FilenameUtils.getName(s), s, graphic);
var b = new JFXButton();
b.setGraphic(el);
b.prefWidthProperty().bind(previous.widthProperty());
b.setOnAction(e -> {
file.setValue(FileStore.local(Path.of(s)));
});
previous.getChildren().add(b);
});
var pane = new ScrollPane(previous);
pane.setFitToWidth(true);
return previous;
}
}

View file

@ -0,0 +1,66 @@
package io.xpipe.app.comp.source.store;
import com.jfoenix.controls.JFXButton;
import io.xpipe.app.util.JfxHelper;
import io.xpipe.extension.DataSourceProvider;
import io.xpipe.extension.I18n;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.CompStructure;
import io.xpipe.extension.fxcomps.SimpleCompStructure;
import io.xpipe.extension.fxcomps.util.PlatformThread;
import javafx.beans.property.Property;
import javafx.scene.control.Button;
import javafx.scene.layout.Region;
import javafx.stage.DirectoryChooser;
import java.io.File;
import java.nio.file.Path;
public class DsLocalDirectoryBrowseComp extends Comp<CompStructure<Button>> {
private final DataSourceProvider provider;
private final Property<Path> chosenDir;
public DsLocalDirectoryBrowseComp(DataSourceProvider provider, Property<Path> chosenDir) {
this.provider = provider;
this.chosenDir = chosenDir;
}
@Override
public CompStructure<Button> createBase() {
var button = new JFXButton();
button.setGraphic(getGraphic());
button.setOnAction(e -> {
var dirChooser = new DirectoryChooser();
dirChooser.setTitle(
I18n.get("browseDirectoryTitle", provider.getFileProvider().getFileName()));
File file = dirChooser.showDialog(button.getScene().getWindow());
if (file != null && file.exists()) {
chosenDir.setValue(file.toPath());
}
e.consume();
});
chosenDir.addListener((c, o, n) -> {
PlatformThread.runLaterIfNeeded(() -> {
var newGraphic = getGraphic();
button.setGraphic(newGraphic);
button.layout();
});
});
return new SimpleCompStructure<>(button);
}
private Region getGraphic() {
var graphic = provider.getDisplayIconFileName();
if (chosenDir.getValue() == null) {
return JfxHelper.createNamedEntry(I18n.get("browse"), I18n.get("selectDirectoryFromComputer"), graphic);
} else {
return JfxHelper.createNamedEntry(
chosenDir.getValue().getFileName().toString(),
chosenDir.getValue().toString(),
graphic);
}
}
}

View file

@ -0,0 +1,109 @@
package io.xpipe.app.comp.source.store;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.util.JfxHelper;
import io.xpipe.core.impl.FileStore;
import io.xpipe.core.store.DataStore;
import io.xpipe.extension.DataSourceProvider;
import io.xpipe.extension.I18n;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.CompStructure;
import io.xpipe.extension.fxcomps.SimpleCompStructure;
import io.xpipe.extension.fxcomps.util.PlatformThread;
import javafx.beans.property.Property;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.Button;
import javafx.scene.layout.Region;
import javafx.stage.FileChooser;
import lombok.AllArgsConstructor;
import java.io.File;
import java.util.concurrent.atomic.AtomicReference;
@AllArgsConstructor
public class DsLocalFileBrowseComp extends Comp<CompStructure<Button>> {
private final ObservableValue<DataSourceProvider<?>> provider;
private final Property<DataStore> chosenFile;
private final DsStreamStoreChoiceComp.Mode mode;
@Override
public CompStructure<Button> createBase() {
var button = new AtomicReference<Button>();
button.set(new ButtonComp(null, getGraphic(), () -> {
var fileChooser = createChooser();
File file = mode == DsStreamStoreChoiceComp.Mode.OPEN
? fileChooser.showOpenDialog(button.get().getScene().getWindow())
: fileChooser.showSaveDialog(button.get().getScene().getWindow());
if (file != null && file.exists()) {
chosenFile.setValue(FileStore.local(file.toPath()));
}
})
.createStructure()
.get());
Runnable update = () -> {
PlatformThread.runLaterIfNeeded(() -> {
var newGraphic = getGraphic();
button.get().setGraphic(newGraphic);
button.get().layout();
});
};
chosenFile.addListener((c, o, n) -> {
update.run();
});
if (provider != null) {
provider.addListener((c, o, n) -> {
update.run();
});
}
return new SimpleCompStructure<>(button.get());
}
private boolean hasProvider() {
return provider != null && provider.getValue() != null;
}
private FileChooser createChooser() {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle(I18n.get("browseFileTitle"));
if (!hasProvider()) {
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(I18n.get("anyFile"), "*"));
return fileChooser;
}
if (hasProvider()) {
provider.getValue().getFileProvider().getFileExtensions().forEach((key, value) -> {
var name = I18n.get(key);
if (value != null) {
fileChooser
.getExtensionFilters()
.add(new FileChooser.ExtensionFilter(
name, value.stream().map(v -> "*." + v).toArray(String[]::new)));
} else {
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(name, "*"));
}
});
if (!provider.getValue().getFileProvider().getFileExtensions().containsValue(null)) {
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(I18n.get("anyFile"), "*"));
}
}
return fileChooser;
}
private Region getGraphic() {
var graphic = hasProvider() ? provider.getValue().getDisplayIconFileName() : "file_icon.png";
if (chosenFile.getValue() == null || !(chosenFile.getValue() instanceof FileStore f) || f.getFile() == null) {
return JfxHelper.createNamedEntry(I18n.get("browse"), I18n.get("selectFileFromComputer"), graphic);
} else {
return JfxHelper.createNamedEntry(
f.getFileName().toString(), f.getFile().toString(), graphic);
}
}
}

View file

@ -0,0 +1,44 @@
package io.xpipe.app.comp.source.store;
import io.xpipe.core.impl.FileStore;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.FileSystemStore;
import io.xpipe.extension.I18n;
import io.xpipe.extension.fxcomps.SimpleComp;
import io.xpipe.extension.fxcomps.impl.FileSystemStoreChoiceComp;
import io.xpipe.extension.util.DynamicOptionsBuilder;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.scene.layout.Region;
import lombok.EqualsAndHashCode;
import lombok.Value;
@Value
@EqualsAndHashCode(callSuper = true)
public class DsRemoteFileChoiceComp extends SimpleComp {
Property<DataStore> store;
@Override
protected Region createSimple() {
var machine = new SimpleObjectProperty<FileSystemStore>();
var fileName = new SimpleStringProperty();
return new DynamicOptionsBuilder(false)
.addComp(I18n.observable("machine"), new FileSystemStoreChoiceComp(machine), machine)
.addString(I18n.observable("file"), fileName, true)
.bind(
() -> {
if (fileName.get() == null || machine.get() == null) {
return null;
}
return FileStore.builder()
.fileSystem(machine.get())
.file(fileName.get())
.build();
},
store)
.build();
}
}

View file

@ -0,0 +1,68 @@
package io.xpipe.app.comp.source.store;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.JfxHelper;
import io.xpipe.extension.DataStoreProvider;
import io.xpipe.extension.DataStoreProviders;
import io.xpipe.extension.I18n;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.CompStructure;
import io.xpipe.extension.fxcomps.SimpleCompStructure;
import io.xpipe.extension.util.CustomComboBoxBuilder;
import javafx.beans.property.Property;
import javafx.scene.Node;
import javafx.scene.control.ComboBox;
import javafx.scene.layout.Region;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.experimental.FieldDefaults;
import java.util.List;
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
@AllArgsConstructor
public class DsStoreProviderChoiceComp extends Comp<CompStructure<ComboBox<Node>>> {
DataStoreProvider.Category type;
Property<DataStoreProvider> provider;
private Region createDefaultNode() {
return switch (type) {
case STREAM -> JfxHelper.createNamedEntry(
I18n.get("selectStreamType"), I18n.get("selectStreamTypeDescription"), "file_icon.png");
case SHELL -> JfxHelper.createNamedEntry(
I18n.get("selectShellType"), I18n.get("selectShellTypeDescription"), "machine_icon.png");
case DATABASE -> JfxHelper.createNamedEntry(
I18n.get("selectDatabaseType"), I18n.get("selectDatabaseTypeDescription"), "db_icon.png");
};
}
private List<DataStoreProvider> getProviders() {
return DataStoreProviders.getAll().stream()
.filter(p -> p.getCategory() == type)
.toList();
}
private Region createGraphic(DataStoreProvider provider) {
if (provider == null) {
return createDefaultNode();
}
var graphic = provider.getDisplayIconFileName();
return JfxHelper.createNamedEntry(provider.getDisplayName(), provider.getDisplayDescription(), graphic);
}
@Override
public CompStructure<ComboBox<Node>> createBase() {
var comboBox = new CustomComboBoxBuilder<>(provider, this::createGraphic, createDefaultNode(), v -> true);
comboBox.add(null);
comboBox.addSeparator();
getProviders().stream()
.filter(p -> AppPrefs.get().developerShowHiddenProviders().get() || p.shouldShow())
.forEach(comboBox::add);
ComboBox<Node> cb = comboBox.build();
cb.getStyleClass().add("data-source-type");
cb.getStyleClass().add("choice-comp");
return new SimpleCompStructure<>(cb);
}
}

View file

@ -0,0 +1,180 @@
package io.xpipe.app.comp.source.store;
import io.xpipe.app.comp.base.FileDropOverlayComp;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.core.impl.FileStore;
import io.xpipe.core.impl.LocalStore;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.StreamDataStore;
import io.xpipe.extension.DataSourceProvider;
import io.xpipe.extension.DataSourceProviders;
import io.xpipe.extension.DataStoreProvider;
import io.xpipe.extension.I18n;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.SimpleComp;
import io.xpipe.extension.fxcomps.augment.GrowAugment;
import io.xpipe.extension.fxcomps.impl.TabPaneComp;
import io.xpipe.extension.fxcomps.impl.VerticalComp;
import io.xpipe.extension.fxcomps.util.SimpleChangeListener;
import io.xpipe.extension.util.SimpleValidator;
import io.xpipe.extension.util.Validatable;
import io.xpipe.extension.util.Validator;
import io.xpipe.extension.util.XPipeDaemon;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import lombok.EqualsAndHashCode;
import lombok.Value;
import net.synedra.validatorfx.Check;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
@Value
@EqualsAndHashCode(callSuper = true)
public class DsStreamStoreChoiceComp extends SimpleComp implements Validatable {
public static enum Mode {
OPEN,
WRITE
}
Property<DataStore> selected;
Property<DataSourceProvider<?>> provider;
boolean showAnonymous;
boolean showSaved;
Validator validator;
Check check;
DsStreamStoreChoiceComp.Mode mode;
public DsStreamStoreChoiceComp(
Property<DataStore> selected,
Property<DataSourceProvider<?>> provider,
boolean showAnonymous,
boolean showSaved, Mode mode
) {
this.selected = selected;
this.provider = provider;
this.showAnonymous = showAnonymous;
this.showSaved = showSaved;
this.mode = mode;
validator = new SimpleValidator();
check = Validator.nonNull(validator, I18n.observable("streamStore"), selected);
}
@Override
protected Region createSimple() {
var isNamedStore = XPipeDaemon.getInstance().getStoreName(selected.getValue()).isPresent();
var localStore = new SimpleObjectProperty<DataStore>(
!isNamedStore && selected.getValue() instanceof FileStore fileStore && fileStore.getFileSystem() instanceof LocalStore
? selected.getValue()
: null);
var browseComp = new DsLocalFileBrowseComp(provider, localStore, mode).apply(GrowAugment.create(true, false));
var dragAndDropLabel = Comp.of(() -> new Label(I18n.get("dragAndDropFilesHere")))
.apply(s -> s.get().setAlignment(Pos.CENTER))
.apply(struc -> AppFont.small(struc.get()));
// var historyComp = new DsFileHistoryComp(provider, chosenFile);
var local = new TabPaneComp.Entry(
I18n.observable("localFile"),
"mdi2m-monitor",
new VerticalComp(List.of(browseComp, dragAndDropLabel))
.styleClass("store-local-file-chooser")
.apply(s -> s.get().setFillWidth(true))
.apply(s -> s.get().setSpacing(30))
.apply(s -> s.get().setAlignment(Pos.TOP_CENTER)));
var filter = Bindings.createObjectBinding(
() -> (Predicate<DataStoreEntry>) e -> {
if (provider == null || provider.getValue() == null) {
return e.getStore() instanceof StreamDataStore;
}
return provider.getValue().couldSupportStore(e.getStore());
},
provider != null ? provider : new SimpleObjectProperty<>());
var remoteStore = new SimpleObjectProperty<DataStore>(
isNamedStore && selected.getValue() instanceof FileStore fileStore && !(fileStore.getFileSystem() instanceof LocalStore)
? selected.getValue()
: null);
var remote = new TabPaneComp.Entry(
I18n.observable("remote"), "mdi2e-earth", new DsRemoteFileChoiceComp(remoteStore));
var namedStore = new SimpleObjectProperty<DataStore>(isNamedStore ? selected.getValue() : null);
var named = new TabPaneComp.Entry(
I18n.observable("stored"),
"mdrmz-storage",
NamedStoreChoiceComp.create(filter, namedStore, DataStoreProvider.Category.STREAM));
var otherStore = new SimpleObjectProperty<DataStore>(localStore.get() == null && remoteStore.get() == null && !isNamedStore ? selected.getValue() : null);
var other = new TabPaneComp.Entry(
I18n.observable("other"),
"mdrmz-web_asset",
new DataStoreSelectorComp(DataStoreProvider.Category.STREAM, otherStore));
var selectedTab = new SimpleObjectProperty<TabPaneComp.Entry>();
if (localStore.get() != null) {
selectedTab.set(local);
} else if (remoteStore.get() != null) {
selectedTab.set(remote);
} else if (namedStore.get() != null) {
selectedTab.set(named);
} else if (otherStore.get() != null) {
selectedTab.set(other);
} else {
selectedTab.set(local);
}
selected.addListener((observable, oldValue, newValue) -> {
if (provider != null && provider.getValue() == null) {
provider.setValue(DataSourceProviders.byPreferredStore(newValue, null).orElse(null));
}
});
SimpleChangeListener.apply(selectedTab, c -> {
if (c == local) {
this.selected.bind(localStore);
}
if (c == remote) {
this.selected.bind(remoteStore);
}
if (c == named) {
this.selected.bind(namedStore);
}
if (c == other) {
this.selected.bind(otherStore);
}
});
var entries = new ArrayList<>(List.of(local, remote));
if (showSaved) {
entries.add(named);
}
if (showAnonymous) {
entries.add(other);
}
var pane = new TabPaneComp(selectedTab, entries);
pane.apply(s -> AppFont.normal(s.get()));
var fileDrop = new FileDropOverlayComp<>(pane, files -> {
if (files.size() != 1) {
return;
}
var f = files.get(0);
var store = FileStore.local(f);
selectedTab.set(local);
localStore.set(store);
});
var region = fileDrop.createRegion();
check.decorates(region);
return region;
}
}

View file

@ -0,0 +1,295 @@
package io.xpipe.app.comp.source.store;
import io.xpipe.app.comp.base.InstallExtensionComp;
import io.xpipe.app.comp.base.LoadingOverlayComp;
import io.xpipe.app.comp.base.MessageComp;
import io.xpipe.app.comp.base.MultiStepComp;
import io.xpipe.app.core.AppExtensionManager;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppWindowHelper;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.core.store.DataStore;
import io.xpipe.extension.DataStoreProvider;
import io.xpipe.extension.DownloadModuleInstall;
import io.xpipe.extension.I18n;
import io.xpipe.extension.event.ErrorEvent;
import io.xpipe.extension.event.ExceptionConverter;
import io.xpipe.extension.event.TrackEvent;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.CompStructure;
import io.xpipe.extension.fxcomps.augment.GrowAugment;
import io.xpipe.extension.fxcomps.util.PlatformThread;
import io.xpipe.extension.fxcomps.util.SimpleChangeListener;
import io.xpipe.extension.util.*;
import javafx.application.Platform;
import javafx.beans.property.*;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.Alert;
import javafx.scene.control.Separator;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import lombok.AccessLevel;
import lombok.experimental.FieldDefaults;
import java.util.List;
import java.util.UUID;
import java.util.function.Consumer;
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
public class GuiDsStoreCreator extends MultiStepComp.Step<CompStructure<?>> {
MultiStepComp parent;
Property<DataStoreProvider> provider;
Property<DataStore> input;
DataStoreProvider.Category generalType;
BooleanProperty busy = new SimpleBooleanProperty();
Property<Validator> validator = new SimpleObjectProperty<>(new SimpleValidator());
Property<String> messageProp = new SimpleStringProperty();
MessageComp message = new MessageComp(messageProp, 10000);
BooleanProperty finished = new SimpleBooleanProperty();
Property<DataStoreEntry> entry = new SimpleObjectProperty<>();
BooleanProperty changedSinceError = new SimpleBooleanProperty();
StringProperty name;
public GuiDsStoreCreator(
MultiStepComp parent,
Property<DataStoreProvider> provider,
Property<DataStore> input,
DataStoreProvider.Category generalType,
String initialName) {
super(null);
this.parent = parent;
this.provider = provider;
this.input = input;
this.generalType = generalType;
this.name = new SimpleStringProperty(initialName);
this.input.addListener((c, o, n) -> {
changedSinceError.setValue(true);
});
this.name.addListener((c, o, n) -> {
changedSinceError.setValue(true);
});
this.provider.addListener((c, o, n) -> {
input.unbind();
input.setValue(null);
if (n != null) {
input.setValue(n.defaultStore());
}
});
this.apply(r -> {
r.get().setPrefWidth(AppFont.em(30));
r.get().setPrefHeight(AppFont.em(35));
});
}
public static void showEdit(DataStoreEntry e) {
show(e.getName(), e.getProvider(), e.getStore(), e.getProvider().getCategory(), newE -> {
ThreadHelper.runAsync(() -> {
e.applyChanges(newE);
if (!DataStorage.get().getStores().contains(e)) {
DataStorage.get().addStore(e);
}
DataStorage.get().refresh();
});
});
}
public static void showCreation(DataStoreProvider.Category cat) {
show(null, null, null, cat, e -> {
try {
DataStorage.get().addStore(e);
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
}
});
}
public static void show(
String initialName,
DataStoreProvider provider,
DataStore s,
DataStoreProvider.Category cat,
Consumer<DataStoreEntry> con) {
var prop = new SimpleObjectProperty<DataStoreProvider>(provider);
var store = new SimpleObjectProperty<DataStore>(s);
var name = cat == DataStoreProvider.Category.SHELL
? "addShellTitle"
: cat == DataStoreProvider.Category.DATABASE ? "addDatabaseTitle" : "addStreamTitle";
Platform.runLater(() -> {
var stage = AppWindowHelper.sideWindow(
I18n.get(name),
window -> {
return new MultiStepComp() {
private final GuiDsStoreCreator creator =
new GuiDsStoreCreator(this, prop, store, cat, initialName);
@Override
protected List<Entry> setup() {
return List.of(new Entry(I18n.observable("a"), creator));
}
@Override
protected void finish() {
window.close();
if (creator.entry.getValue() != null) {
con.accept(creator.entry.getValue());
}
}
};
},
false,
null);
stage.show();
});
}
private static boolean showInvalidConfirmAlert() {
return AppWindowHelper.showBlockingAlert(alert -> {
alert.setTitle(I18n.get("confirmInvalidStoreTitle"));
alert.setHeaderText(I18n.get("confirmInvalidStoreHeader"));
alert.setContentText(I18n.get("confirmInvalidStoreContent"));
alert.setAlertType(Alert.AlertType.CONFIRMATION);
})
.map(b -> b.getButtonData().isDefaultButton())
.orElse(false);
}
private Region createStoreProperties(Comp<?> comp, Validator propVal) {
return new DynamicOptionsBuilder(false)
.addComp((ObservableValue<String>) null, comp, input)
.addTitle(I18n.observable("properties"))
.addString(I18n.observable("name"), name, false)
.nonNull(propVal)
.bind(
() -> {
if (name.getValue() == null || input.getValue() == null) {
return null;
}
return DataStoreEntry.createNew(UUID.randomUUID(), name.getValue(), input.getValue());
},
entry)
.build();
}
@Override
public CompStructure<? extends Region> createBase() {
var layout = new BorderPane();
var providerChoice = new DsStoreProviderChoiceComp(generalType, provider);
providerChoice.apply(GrowAugment.create(true, false));
SimpleChangeListener.apply(provider, n -> {
if (n != null) {
var install = n.getRequiredAdditionalInstallation();
if (install != null && AppExtensionManager.getInstance().isInstalled(install)) {
layout.setCenter(new InstallExtensionComp((DownloadModuleInstall) install).createRegion());
validator.setValue(new SimpleValidator());
return;
}
var d = n.guiDialog(input);
if (d == null || d.getComp() == null) {
layout.setCenter(null);
validator.setValue(new SimpleValidator());
return;
}
var propVal = new SimpleValidator();
var propR = createStoreProperties(d.getComp(), propVal);
var box = new VBox(propR);
box.setSpacing(7);
layout.setCenter(box);
validator.setValue(new ChainedValidator(List.of(d.getValidator(), propVal)));
} else {
layout.setCenter(null);
validator.setValue(new SimpleValidator());
}
});
layout.setBottom(message.createRegion());
var sep = new Separator();
sep.getStyleClass().add("spacer");
var top = new VBox(providerChoice.createRegion(), sep);
top.getStyleClass().add("top");
layout.setTop(top);
// layout.getStyleClass().add("data-input-creation-step");
return new LoadingOverlayComp(Comp.of(() -> layout), busy).createStructure();
}
@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 BusyProperty(busy)) {
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 (input.getValue() == null) {
return false;
}
if (messageProp.getValue() != null && !changedSinceError.get()) {
if (showInvalidConfirmAlert()) {
return true;
}
}
if (!validator.getValue().validate()) {
var msg = validator
.getValue()
.getValidationResult()
.getMessages()
.get(0)
.getText();
TrackEvent.info(msg);
messageProp.setValue(msg);
message.show();
changedSinceError.setValue(false);
return false;
}
ThreadHelper.runAsync(() -> {
try (var b = new BusyProperty(busy)) {
entry.getValue().setStore(input.getValue());
entry.getValue().refresh(true);
finished.setValue(true);
PlatformThread.runLaterIfNeeded(parent::next);
} catch (Exception ex) {
messageProp.setValue(ExceptionConverter.convertMessage(ex));
message.show();
changedSinceError.setValue(false);
ErrorEvent.fromThrowable(ex).omit().reportable(false).handle();
}
});
return false;
}
}

View file

@ -0,0 +1,158 @@
package io.xpipe.app.comp.source.store;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.comp.base.ListViewComp;
import io.xpipe.app.comp.storage.store.StoreViewState;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.util.JfxHelper;
import io.xpipe.core.store.DataStore;
import io.xpipe.extension.DataStoreProvider;
import io.xpipe.extension.I18n;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.SimpleComp;
import io.xpipe.extension.fxcomps.impl.FilterComp;
import io.xpipe.extension.fxcomps.impl.LabelComp;
import io.xpipe.extension.fxcomps.impl.StackComp;
import io.xpipe.extension.fxcomps.impl.VerticalComp;
import io.xpipe.extension.fxcomps.util.BindingsHelper;
import io.xpipe.extension.util.SimpleValidator;
import io.xpipe.extension.util.Validatable;
import io.xpipe.extension.util.Validator;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import lombok.Getter;
import net.synedra.validatorfx.Check;
import java.util.List;
import java.util.function.Predicate;
public class NamedStoreChoiceComp extends SimpleComp implements Validatable {
private final ObservableValue<Predicate<DataStore>> filter;
private final DataStoreProvider.Category category;
private final Property<? extends DataStore> selected;
private final StringProperty filterString = new SimpleStringProperty();
@Getter
private final Validator validator = new SimpleValidator();
private final Check check;
public NamedStoreChoiceComp(
ObservableValue<Predicate<DataStore>> filter,
Property<? extends DataStore> selected,
DataStoreProvider.Category category) {
this.filter = filter;
this.selected = selected;
this.category = category;
check = Validator.nonNull(validator, I18n.observable("store"), selected);
}
public static NamedStoreChoiceComp create(
ObservableValue<Predicate<DataStoreEntry>> filter,
Property<? extends DataStore> selected,
DataStoreProvider.Category category) {
return new NamedStoreChoiceComp(
Bindings.createObjectBinding(
() -> {
return store -> {
if (store == null) {
return false;
}
var e = DataStorage.get().getStore(store);
return filter.getValue().test(e);
};
},
filter),
selected,
category);
}
private void setUpListener(ObservableValue<DataStoreEntry> prop) {
prop.addListener((c, o, n) -> {
selected.setValue(n != null ? n.getStore().asNeeded() : null);
});
}
private void refreshShown(ObservableList<DataStoreEntry> list, ObservableList<DataStoreEntry> shown) {
var filtered = list.filtered(e -> filter.getValue().test(e.getStore())).filtered(e -> {
return filterString.get() == null || e.matches(filterString.get());
});
shown.removeIf(store -> !filtered.contains(store));
filtered.forEach(store -> {
if (!shown.contains(store)) {
shown.add(store);
}
});
}
@Override
protected Region createSimple() {
var list = FXCollections.<DataStoreEntry>observableArrayList();
BindingsHelper.bindMappedContent(list, StoreViewState.get().getAllEntries(), v -> v.getEntry());
var shown = FXCollections.<DataStoreEntry>observableArrayList();
refreshShown(list, shown);
list.addListener((ListChangeListener<? super DataStoreEntry>) c -> {
refreshShown(list, shown);
});
filter.addListener((observable, oldValue, newValue) -> {
refreshShown(list, shown);
});
filterString.addListener((observable, oldValue, newValue) -> {
refreshShown(list, shown);
});
var prop = new SimpleObjectProperty<>(
DataStorage.get().getEntryByStore(selected.getValue()).orElse(null));
setUpListener(prop);
var filterComp = new FilterComp(filterString)
.hide(BindingsHelper.persist(Bindings.greaterThan(5, Bindings.size(shown))));
var view = new ListViewComp<>(shown, list, prop, (DataStoreEntry e) -> {
var provider = e.getProvider();
var graphic = provider.getDisplayIconFileName();
var top = String.format("%s (%s)", e.getName(), provider.getDisplayName());
var bottom = provider.toSummaryString(e.getStore(), 50);
var el = JfxHelper.createNamedEntry(top, bottom, graphic);
VBox.setVgrow(el, Priority.ALWAYS);
return Comp.of(() -> el);
})
.apply(struc -> {
struc.get().setMaxHeight(2000);
check.decorates(struc.get());
});
var box = new VerticalComp(List.of(filterComp, view));
var text = new LabelComp(I18n.observable("noMatchingStoreFound"))
.apply(struc -> VBox.setVgrow(struc.get(), Priority.ALWAYS));
var addButton = new ButtonComp(I18n.observable("addStore"), null, () -> {
GuiDsStoreCreator.showCreation(category);
});
var notice = new VerticalComp(List.of(text, addButton))
.apply(struc -> {
struc.get().setSpacing(10);
struc.get().setAlignment(Pos.CENTER);
})
.hide(BindingsHelper.persist(Bindings.notEqual(0, Bindings.size(shown))));
return new StackComp(List.of(box, notice))
.styleClass("named-store-choice")
.createRegion();
}
}

View file

@ -0,0 +1,98 @@
package io.xpipe.app.comp.storage;
import io.xpipe.core.source.DataSourceType;
import io.xpipe.core.store.DataFlow;
import io.xpipe.extension.fxcomps.SimpleComp;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.Map;
public class DataSourceTypeComp extends SimpleComp {
public static final Map<DataSourceType, String> ICONS = Map.of(
DataSourceType.TABLE, "mdi2t-table-large",
DataSourceType.STRUCTURE, "mdi2b-beaker-outline",
DataSourceType.TEXT, "mdi2t-text-box",
DataSourceType.RAW, "mdi2c-card-outline",
DataSourceType.COLLECTION, "mdi2b-briefcase-outline");
private static final String MISSING_ICON = "mdi2c-comment-question-outline";
private static final Color MISSING_COLOR = Color.RED;
private static final Map<DataSourceType, Color> COLORS = Map.of(
DataSourceType.TABLE, Color.rgb(0, 160, 0, 0.5),
DataSourceType.STRUCTURE, Color.ORANGERED,
DataSourceType.TEXT, Color.LIGHTBLUE,
DataSourceType.RAW, Color.GREY,
DataSourceType.COLLECTION, Color.ORCHID.deriveColor(0, 1.0, 0.85, 1.0));
private final DataSourceType type;
private final DataFlow flow;
public DataSourceTypeComp(DataSourceType type, DataFlow flow) {
this.type = type;
this.flow = flow;
}
@Override
protected Region createSimple() {
var bg = new Region();
bg.setBackground(new Background(new BackgroundFill(
type != null ? COLORS.get(type) : MISSING_COLOR, new CornerRadii(12), Insets.EMPTY)));
bg.getStyleClass().add("background");
var sp = new StackPane(bg);
sp.setAlignment(Pos.CENTER);
sp.getStyleClass().add("data-source-type-comp");
var icon = new FontIcon(type != null ? ICONS.get(type) : MISSING_ICON);
icon.iconSizeProperty().bind(Bindings.divide(sp.heightProperty(), 2));
sp.getChildren().add(icon);
if (flow == DataFlow.INPUT || flow == DataFlow.INPUT_OUTPUT) {
var flowIcon = createInputFlowType();
sp.getChildren().add(flowIcon);
}
if (flow == DataFlow.OUTPUT || flow == DataFlow.INPUT_OUTPUT) {
var flowIcon = createOutputFlowType();
sp.getChildren().add(flowIcon);
}
if (flow == DataFlow.TRANSFORMER) {
var flowIcon = createTransformerFlowType();
sp.getChildren().add(flowIcon);
}
return sp;
}
private Region createInputFlowType() {
var icon = new FontIcon("mdi2c-chevron-double-left");
icon.setIconColor(Color.WHITE);
var anchorPane = new AnchorPane(icon);
AnchorPane.setLeftAnchor(icon, 3.0);
AnchorPane.setBottomAnchor(icon, 3.0);
return anchorPane;
}
private Region createOutputFlowType() {
var icon = new FontIcon("mdi2c-chevron-double-right");
icon.setIconColor(Color.WHITE);
var anchorPane = new AnchorPane(icon);
AnchorPane.setRightAnchor(icon, 3.0);
AnchorPane.setBottomAnchor(icon, 3.0);
return anchorPane;
}
private Region createTransformerFlowType() {
var icon = new FontIcon("mdi2t-transfer");
icon.setIconColor(Color.WHITE);
var anchorPane = new AnchorPane(icon);
AnchorPane.setRightAnchor(icon, 3.0);
AnchorPane.setBottomAnchor(icon, 3.0);
return anchorPane;
}
}

View file

@ -0,0 +1,28 @@
package io.xpipe.app.comp.storage;
import io.xpipe.core.source.DataSource;
import io.xpipe.extension.fxcomps.SimpleComp;
import javafx.beans.binding.Bindings;
import javafx.geometry.Pos;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import lombok.EqualsAndHashCode;
import lombok.Value;
import org.kordamp.ikonli.javafx.FontIcon;
@Value
@EqualsAndHashCode(callSuper = true)
public class DataStoreTypeComp extends SimpleComp {
private final DataSource<?> source;
@Override
protected Region createSimple() {
var icon = new FontIcon("mdoal-insert_drive_file");
var sp = new StackPane(icon);
sp.setAlignment(Pos.CENTER);
icon.iconSizeProperty().bind(Bindings.divide(sp.heightProperty(), 1));
sp.getStyleClass().add("data-store-type-comp");
return sp;
}
}

View file

@ -0,0 +1,56 @@
package io.xpipe.app.comp.storage;
import io.xpipe.extension.fxcomps.util.SimpleChangeListener;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import java.util.ArrayList;
import java.util.Comparator;
public class StorageFilter {
private final StringProperty filter = new SimpleStringProperty("");
public <T extends Filterable> void createFilterBinding(
ObservableList<T> all, ObservableList<T> shown, ObservableValue<Comparator<T>> order) {
all.addListener((ListChangeListener<? super T>) lc -> {
update(all, shown, order.getValue());
});
SimpleChangeListener.apply(filter, n -> {
update(all, shown, order.getValue());
});
order.addListener((observable, oldValue, newValue) -> {
update(all, shown, newValue);
});
}
private <T extends Filterable> void update(ObservableList<T> all, ObservableList<T> shown, Comparator<T> order) {
var updatedShown = new ArrayList<>(shown);
updatedShown.removeIf(e -> !all.contains(e) || !e.shouldShow(filter.get()));
for (var e : all) {
if (!updatedShown.contains(e) && e.shouldShow(filter.get())) {
updatedShown.add(e);
}
}
updatedShown.sort(order);
shown.setAll(updatedShown);
}
public String getFilter() {
return filter.get();
}
public StringProperty filterProperty() {
return filter;
}
public static interface Filterable {
boolean shouldShow(String filter);
}
}

View file

@ -0,0 +1,198 @@
package io.xpipe.app.comp.storage.collection;
import com.jfoenix.controls.JFXTextField;
import io.xpipe.app.comp.base.CountComp;
import io.xpipe.app.comp.base.LazyTextFieldComp;
import io.xpipe.app.comp.storage.source.SourceEntryWrapper;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n;
import io.xpipe.extension.I18n;
import io.xpipe.extension.event.TrackEvent;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.SimpleComp;
import io.xpipe.extension.fxcomps.SimpleCompStructure;
import io.xpipe.extension.fxcomps.impl.FancyTooltipAugment;
import io.xpipe.extension.fxcomps.impl.IconButtonComp;
import io.xpipe.extension.fxcomps.impl.PrettyImageComp;
import io.xpipe.extension.fxcomps.util.PlatformThread;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Label;
import javafx.scene.effect.Glow;
import javafx.scene.input.Dragboard;
import javafx.scene.input.MouseButton;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.*;
import org.kordamp.ikonli.javafx.FontIcon;
import java.io.File;
public class SourceCollectionComp extends SimpleComp {
private final SourceCollectionWrapper group;
public SourceCollectionComp(SourceCollectionWrapper group) {
this.group = group;
}
@Override
protected Region createSimple() {
var r = createContent();
var sp = new StackPane(r);
sp.setAlignment(Pos.CENTER);
sp.setOnMouseClicked(e -> {
if (e.getButton() != MouseButton.PRIMARY) {
return;
}
TrackEvent.withDebug("Storage group clicked")
.tag("uuid", group.getCollection().getUuid().toString())
.tag("name", group.getName())
.build()
.handle();
// StorageViewState.get().selectedGroupProperty().set(group);
e.consume();
});
setupDragAndDrop(sp);
return sp;
}
private void setupDragAndDrop(Region r) {
r.setOnDragOver(event -> {
// Moving storage entries
if (event.getGestureSource() != null
&& event.getGestureSource() != r
&& event.getSource() instanceof Node) {
event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
r.setEffect(new Glow(0.5));
}
// Files from the outside
else if (event.getGestureSource() == null && event.getDragboard().hasFiles()) {
event.acceptTransferModes(TransferMode.COPY);
r.setEffect(new Glow(0.5));
}
event.consume();
});
r.setOnDragExited(event -> {
r.setEffect(null);
event.consume();
});
r.setOnDragDropped(event -> {
// Moving storage entries
if (event.getGestureSource() != null
&& event.getGestureSource() != r
&& event.getGestureSource() instanceof Node n) {
var entry = n.getProperties().get("entry");
if (entry != null) {
var cast = (SourceEntryWrapper) entry;
cast.moveTo(this.group);
}
}
// Files from the outside
else if (event.getGestureSource() == null && event.getDragboard().hasFiles()) {
event.setDropCompleted(true);
Dragboard db = event.getDragboard();
db.getFiles().stream().map(File::toPath).forEach(group::dropFile);
}
event.consume();
});
}
private Label createDate() {
var date = new Label();
date.textProperty().bind(AppI18n.readableDuration("usedDate", PlatformThread.sync(group.lastAccessProperty())));
date.getStyleClass().add("date");
return date;
}
private Region createContent() {
Region nameR;
if (!group.isRenameable()) {
Region textFieldR = new JFXTextField(group.getName());
textFieldR.setDisable(true);
var tempNote = Comp.of(() -> {
var infoIcon = new FontIcon("mdi2i-information-outline");
infoIcon.setOpacity(0.75);
return new StackPane(infoIcon);
})
.apply(new FancyTooltipAugment<>(I18n.observable("temporaryCollectionNote")))
.createRegion();
var label = new Label(group.getName(), tempNote);
label.getStyleClass().add("temp");
label.setAlignment(Pos.CENTER);
label.setContentDisplay(ContentDisplay.RIGHT);
nameR = new HBox(label);
} else {
var text = new LazyTextFieldComp(group.nameProperty());
nameR = text.createRegion();
}
var options = new IconButtonComp("mdomz-settings");
var cm = new SourceCollectionContextMenu<>(true, group, nameR);
options.apply(new SourceCollectionContextMenu<>(true, group, nameR))
.apply(r -> {
AppFont.setSize(r.get(), -1);
r.get().setPadding(new Insets(3, 5, 3, 5));
})
.apply(new FancyTooltipAugment<>("collectionOptions"));
var count = new CountComp<>(
SourceCollectionViewState.get().getFilteredEntries(this.group), this.group.entriesProperty());
var spacer = new Region();
var optionsR = options.createRegion();
var top = new HBox(nameR, optionsR);
HBox.setHgrow(nameR, Priority.ALWAYS);
top.setSpacing(8);
var countR = count.createRegion();
countR.prefWidthProperty().bind(optionsR.widthProperty());
var bottom = new HBox(createDate(), spacer, countR);
bottom.setAlignment(Pos.CENTER);
bottom.setSpacing(8);
HBox.setHgrow(spacer, Priority.ALWAYS);
var right = new VBox(top, bottom);
right.setSpacing(8);
AppFont.header(top);
AppFont.small(bottom);
var svgContent = Bindings.createObjectBinding(
() -> {
if (SourceCollectionViewState.get().getSelectedGroup() == group) {
return "folder_open.svg";
} else {
return "folder_closed.svg";
}
},
SourceCollectionViewState.get().selectedGroupProperty());
var svg = new PrettyImageComp(svgContent, 55, 55).createRegion();
svg.getStyleClass().add("icon");
if (group.isInternal()) {
svg.setOpacity(0.3);
}
var hbox = new HBox(svg, right);
HBox.setHgrow(right, Priority.ALWAYS);
hbox.setAlignment(Pos.CENTER);
// svg.prefHeightProperty().bind(right.heightProperty());
// svg.prefWidthProperty().bind(right.heightProperty());
hbox.setSpacing(5);
hbox.getStyleClass().add("storage-group-entry");
cm = new SourceCollectionContextMenu<>(false, group, nameR);
cm.augment(new SimpleCompStructure<>(hbox));
hbox.setMinWidth(0);
return hbox;
}
}

View file

@ -0,0 +1,111 @@
package io.xpipe.app.comp.storage.collection;
import io.xpipe.app.core.AppWindowHelper;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.extension.I18n;
import io.xpipe.extension.fxcomps.CompStructure;
import io.xpipe.extension.fxcomps.augment.PopupMenuAugment;
import io.xpipe.extension.util.OsHelper;
import javafx.scene.control.Alert;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.layout.Region;
import org.kordamp.ikonli.javafx.FontIcon;
public class SourceCollectionContextMenu<S extends CompStructure<?>> extends PopupMenuAugment<S> {
private final SourceCollectionWrapper group;
private final Region renameTextField;
public SourceCollectionContextMenu(
boolean showOnPrimaryButton, SourceCollectionWrapper group, Region renameTextField) {
super(showOnPrimaryButton);
this.group = group;
this.renameTextField = renameTextField;
}
private void onDelete() {
if (group.getEntries().size() > 0) {
AppWindowHelper.showBlockingAlert(alert -> {
alert.setTitle(I18n.get("confirmCollectionDeletionTitle"));
alert.setHeaderText(I18n.get("confirmCollectionDeletionHeader", group.getName()));
alert.setContentText(I18n.get(
"confirmCollectionDeletionContent",
group.getEntries().size()));
alert.setAlertType(Alert.AlertType.CONFIRMATION);
})
.filter(b -> b.getButtonData().isDefaultButton())
.ifPresent(t -> {
group.delete();
});
} else {
group.delete();
}
}
private void onClean() {
if (group.getEntries().size() > 0) {
AppWindowHelper.showBlockingAlert(alert -> {
alert.setTitle(I18n.get("confirmCollectionDeletionTitle"));
alert.setHeaderText(I18n.get("confirmCollectionDeletionHeader", group.getName()));
alert.setContentText(I18n.get(
"confirmCollectionDeletionContent",
group.getEntries().size()));
alert.setAlertType(Alert.AlertType.CONFIRMATION);
})
.filter(b -> b.getButtonData().isDefaultButton())
.ifPresent(t -> {
group.clean();
});
} else {
group.clean();
}
}
@Override
protected ContextMenu createContextMenu() {
var cm = new ContextMenu();
var name = new MenuItem(group.getName());
name.setDisable(true);
name.getStyleClass().add("header-menu-item");
cm.getItems().add(name);
cm.getItems().add(new SeparatorMenuItem());
{
var properties = new MenuItem(I18n.get("properties"), new FontIcon("mdi2a-application-cog"));
properties.setOnAction(e -> {});
// cm.getItems().add(properties);
}
if (group.isRenameable()) {
var rename = new MenuItem(I18n.get("rename"), new FontIcon("mdal-edit"));
rename.setOnAction(e -> {
renameTextField.requestFocus();
});
cm.getItems().add(rename);
}
if (AppPrefs.get().developerMode().getValue()) {
var openDir = new MenuItem(I18n.get("openDir"), new FontIcon("mdal-edit"));
openDir.setOnAction(e -> {
OsHelper.browseFileInDirectory(group.getCollection().getDirectory());
});
cm.getItems().add(openDir);
}
if (group.isDeleteable()) {
var del = new MenuItem(I18n.get("delete"), new FontIcon("mdal-delete_outline"));
del.setOnAction(e -> {
onDelete();
});
cm.getItems().add(del);
} else {
var del = new MenuItem(I18n.get("clean"), new FontIcon("mdal-delete_outline"));
del.setOnAction(e -> {
onClean();
});
cm.getItems().add(del);
}
return cm;
}
}

View file

@ -0,0 +1,81 @@
package io.xpipe.app.comp.storage.collection;
import io.xpipe.app.comp.storage.DataSourceTypeComp;
import io.xpipe.app.core.AppFont;
import io.xpipe.core.source.DataSourceType;
import io.xpipe.extension.I18n;
import io.xpipe.extension.fxcomps.SimpleComp;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.Separator;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.kordamp.ikonli.javafx.FontIcon;
public class SourceCollectionEmptyIntroComp extends SimpleComp {
@Override
protected Region createSimple() {
var title = new Label(I18n.get("dataSourceIntroTitle"));
AppFont.setSize(title, 7);
title.getStyleClass().add("title-header");
var descFi = new FontIcon("mdi2i-information-outline");
var introDesc = new Label(I18n.get("dataSourceIntroDescription"));
introDesc.heightProperty().addListener((c, o, n) -> {
descFi.iconSizeProperty().set(n.intValue());
});
var tableFi = new DataSourceTypeComp(DataSourceType.TABLE, null).createRegion();
var table = new Label(I18n.get("dataSourceIntroTable"), tableFi);
tableFi.prefWidthProperty().bind(table.heightProperty());
tableFi.prefHeightProperty().bind(table.heightProperty());
var structureFi = new DataSourceTypeComp(DataSourceType.STRUCTURE, null).createRegion();
var structure = new Label(I18n.get("dataSourceIntroStructure"), structureFi);
structureFi.prefWidthProperty().bind(structure.heightProperty());
structureFi.prefHeightProperty().bind(structure.heightProperty());
var textFi = new DataSourceTypeComp(DataSourceType.TEXT, null).createRegion();
var text = new Label(I18n.get("dataSourceIntroText"), textFi);
textFi.prefWidthProperty().bind(text.heightProperty());
textFi.prefHeightProperty().bind(text.heightProperty());
var binaryFi = new DataSourceTypeComp(DataSourceType.RAW, null).createRegion();
var binary = new Label(I18n.get("dataSourceIntroBinary"), binaryFi);
binaryFi.prefWidthProperty().bind(binary.heightProperty());
binaryFi.prefHeightProperty().bind(binary.heightProperty());
var collectionFi = new DataSourceTypeComp(DataSourceType.COLLECTION, null).createRegion();
var collection = new Label(I18n.get("dataSourceIntroCollection"), collectionFi);
collectionFi.prefWidthProperty().bind(collection.heightProperty());
collectionFi.prefHeightProperty().bind(collection.heightProperty());
var v = new VBox(
title,
introDesc,
new Separator(Orientation.HORIZONTAL),
table,
new Separator(Orientation.HORIZONTAL),
structure,
new Separator(Orientation.HORIZONTAL),
text,
new Separator(Orientation.HORIZONTAL),
binary,
new Separator(Orientation.HORIZONTAL),
collection);
v.setMinWidth(Region.USE_PREF_SIZE);
v.setMaxWidth(Region.USE_PREF_SIZE);
v.setMinHeight(Region.USE_PREF_SIZE);
v.setMaxHeight(Region.USE_PREF_SIZE);
v.setSpacing(10);
v.getStyleClass().add("intro");
var sp = new StackPane(v);
sp.setAlignment(Pos.CENTER);
return sp;
}
}

View file

@ -0,0 +1,60 @@
package io.xpipe.app.comp.storage.collection;
import io.xpipe.app.comp.base.CountComp;
import io.xpipe.app.core.AppFont;
import io.xpipe.extension.fxcomps.SimpleComp;
import io.xpipe.extension.fxcomps.impl.FancyTooltipAugment;
import io.xpipe.extension.fxcomps.impl.FilterComp;
import io.xpipe.extension.fxcomps.impl.IconButtonComp;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.layout.*;
public class SourceCollectionFilterBarComp extends SimpleComp {
private Region createGroupListHeader() {
var label = new Label("Collections");
label.getStyleClass().add("name");
var count = new CountComp<>(
SourceCollectionViewState.get().getShownGroups(),
SourceCollectionViewState.get().getAllGroups());
var newFolder = new IconButtonComp("mdi2f-folder-plus-outline", () -> {
SourceCollectionViewState.get().addNewCollection();
})
.shortcut(new KeyCodeCombination(KeyCode.N, KeyCombination.SHORTCUT_DOWN))
.apply(new FancyTooltipAugment<>("addCollectionFolder"));
var spacer = new Region();
var topBar = new HBox(label, count.createRegion(), spacer, newFolder.createRegion());
AppFont.header(topBar);
topBar.setAlignment(Pos.CENTER);
HBox.setHgrow(spacer, Priority.ALWAYS);
topBar.getStyleClass().add("top");
return topBar;
}
private Region createGroupListFilter() {
var filter = new FilterComp(SourceCollectionViewState.get().getFilter().filterProperty());
filter.shortcut(new KeyCodeCombination(KeyCode.F, KeyCombination.SHORTCUT_DOWN), s -> {
s.getText().requestFocus();
});
var r = new StackPane(filter.createRegion());
r.setAlignment(Pos.CENTER);
r.getStyleClass().add("filter-bar");
AppFont.medium(r);
return r;
}
@Override
public Region createSimple() {
var content = new VBox(createGroupListHeader(), createGroupListFilter());
content.getStyleClass().add("bar");
content.getStyleClass().add("collections-bar");
return content;
}
}

View file

@ -0,0 +1,105 @@
package io.xpipe.app.comp.storage.collection;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.comp.storage.source.SourceEntryListComp;
import io.xpipe.app.comp.storage.source.SourceEntryListHeaderComp;
import io.xpipe.extension.I18n;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.SimpleComp;
import io.xpipe.extension.fxcomps.impl.FancyTooltipAugment;
import io.xpipe.extension.fxcomps.impl.StackComp;
import io.xpipe.extension.fxcomps.impl.VerticalComp;
import io.xpipe.extension.fxcomps.util.BindingsHelper;
import io.xpipe.extension.fxcomps.util.PlatformThread;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.layout.*;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.ArrayList;
import java.util.List;
public class SourceCollectionLayoutComp extends SimpleComp {
private Comp<?> createEntries(SourceCollectionWrapper group, Region groupHeader) {
var entryList = new SourceEntryListComp(group);
var entriesHeader = new SourceEntryListHeaderComp(group);
entriesHeader.apply(r -> r.get().minHeightProperty().bind(groupHeader.heightProperty()));
var entriesHeaderWrapped = Comp.derive(entriesHeader, r -> {
var sp = new StackPane(r);
sp.setPadding(new Insets(0, 5, 5, 5));
return sp;
});
var list = new ArrayList<Comp<?>>(List.of(entriesHeaderWrapped));
list.add(entryList);
entryList.apply(s -> VBox.setVgrow(s.get(), Priority.ALWAYS));
return new VerticalComp(list);
}
private Comp<?> createCollectionList() {
var listComp = new SourceCollectionListComp();
listComp.apply(s -> s.get().setPrefHeight(Region.USE_COMPUTED_SIZE));
return listComp;
}
private Comp<?> createFiller() {
var filler = Comp.of(() -> new Region());
filler.styleClass("bar");
filler.styleClass("filler-bar");
var button = new ButtonComp(I18n.observable("addCollection"), new FontIcon("mdi2f-folder-plus-outline"), () -> {
SourceCollectionViewState.get().addNewCollection();
})
.apply(new FancyTooltipAugment<>("addCollectionFolder"));
button.styleClass("intro-add-collection-button");
var pane = Comp.derive(button, r -> {
var sp = new StackPane(r);
sp.setAlignment(Pos.CENTER);
sp.setPickOnBounds(false);
return sp;
});
pane.apply(r -> {
r.get().visibleProperty().bind(SourceCollectionViewState.get().getStorageEmpty());
r.get()
.mouseTransparentProperty()
.bind(BindingsHelper.persist(
Bindings.not(SourceCollectionViewState.get().getStorageEmpty())));
});
var stack = new StackComp(List.of(filler, pane));
stack.apply(s -> {
s.get().setMinHeight(0);
s.get().setPrefHeight(0);
});
return stack;
}
@Override
protected Region createSimple() {
var listComp = createCollectionList();
var r = new BorderPane();
var listR = listComp.createRegion();
var groupHeader = new SourceCollectionFilterBarComp().createRegion();
var filler = createFiller().createRegion();
var groups = new VBox(groupHeader, listR);
groups.getStyleClass().add("sidebar");
VBox.setVgrow(filler, Priority.SOMETIMES);
VBox.setVgrow(listR, Priority.SOMETIMES);
r.setLeft(groups);
Runnable update = () -> {
r.setCenter(createEntries(SourceCollectionViewState.get().getSelectedGroup(), groupHeader)
.createRegion());
};
update.run();
SourceCollectionViewState.get().selectedGroupProperty().addListener((c, o, n) -> {
PlatformThread.runLaterIfNeeded(update);
});
return r;
}
}

View file

@ -0,0 +1,17 @@
package io.xpipe.app.comp.storage.collection;
import io.xpipe.app.comp.base.ListViewComp;
public class SourceCollectionListComp extends ListViewComp<SourceCollectionWrapper> {
public SourceCollectionListComp() {
super(
SourceCollectionViewState.get().getShownGroups(),
SourceCollectionViewState.get().getAllGroups(),
SourceCollectionViewState.get().selectedGroupProperty(),
SourceCollectionComp::new);
styleClass("storage-group-list-comp");
styleClass("bar");
apply(s -> s.get().layout());
}
}

View file

@ -0,0 +1,66 @@
package io.xpipe.app.comp.storage.collection;
import io.xpipe.app.comp.storage.source.SourceEntryWrapper;
import java.time.Instant;
import java.util.Comparator;
public interface SourceCollectionSortMode {
static SourceCollectionSortMode ALPHABETICAL_DESC = new SourceCollectionSortMode() {
@Override
public String getId() {
return "alphabetical-desc";
}
@Override
public Comparator<SourceEntryWrapper> comparator() {
return Comparator.<SourceEntryWrapper, String>comparing(
e -> e.getName().getValue())
.reversed();
}
};
static SourceCollectionSortMode ALPHABETICAL_ASC = new SourceCollectionSortMode() {
@Override
public String getId() {
return "alphabetical-asc";
}
@Override
public Comparator<SourceEntryWrapper> comparator() {
return Comparator.<SourceEntryWrapper, String>comparing(
e -> e.getName().getValue());
}
};
static SourceCollectionSortMode DATE_DESC = new SourceCollectionSortMode() {
@Override
public String getId() {
return "date-desc";
}
@Override
public Comparator<SourceEntryWrapper> comparator() {
return Comparator.<SourceEntryWrapper, Instant>comparing(
e -> e.getLastUsed().getValue())
.reversed();
}
};
static SourceCollectionSortMode DATE_ASC = new SourceCollectionSortMode() {
@Override
public String getId() {
return "date-asc";
}
@Override
public Comparator<SourceEntryWrapper> comparator() {
return Comparator.comparing(e -> e.getLastUsed().getValue());
}
};
String getId();
Comparator<SourceEntryWrapper> comparator();
}

View file

@ -0,0 +1,221 @@
package io.xpipe.app.comp.storage.collection;
import io.xpipe.app.comp.storage.StorageFilter;
import io.xpipe.app.comp.storage.source.SourceEntryWrapper;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataSourceCollection;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.storage.StorageListener;
import io.xpipe.extension.I18n;
import io.xpipe.extension.fxcomps.util.BindingsHelper;
import io.xpipe.extension.fxcomps.util.PlatformThread;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableBooleanValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import java.time.Instant;
import java.util.Comparator;
import java.util.concurrent.CopyOnWriteArrayList;
public class SourceCollectionViewState {
private static SourceCollectionViewState INSTANCE;
private final StorageFilter filter = new StorageFilter();
private final ObservableList<SourceCollectionWrapper> allGroups =
FXCollections.observableList(new CopyOnWriteArrayList<>());
private final ObservableList<SourceCollectionWrapper> shownGroups =
FXCollections.observableList(new CopyOnWriteArrayList<>());
private final SimpleObjectProperty<SourceCollectionWrapper> selectedGroup = new SimpleObjectProperty<>();
private final SimpleObjectProperty<SourceCollectionSortMode> sortMode = new SimpleObjectProperty<>();
private final ObservableList<SourceEntryWrapper> allEntries =
FXCollections.observableList(new CopyOnWriteArrayList<>());
private final ObservableList<SourceEntryWrapper> shownEntries =
FXCollections.observableList(new CopyOnWriteArrayList<>());
private final ObservableBooleanValue storageEmpty =
BindingsHelper.persist(Bindings.size(allGroups).isEqualTo(0));
private SourceCollectionViewState() {
addCollectionListChangeListeners();
addEntryListListeners();
addSortModeListeners();
}
public static void init() {
INSTANCE = new SourceCollectionViewState();
}
public static void reset() {
INSTANCE = null;
}
public static SourceCollectionViewState get() {
return INSTANCE;
}
private void addSortModeListeners() {
ChangeListener<SourceCollectionSortMode> listener = (observable1, oldValue1, newValue1) -> {
sortMode.set(newValue1);
};
selectedGroup.addListener((observable, oldValue, newValue) -> {
sortMode.set(newValue != null ? newValue.getSortMode() : null);
if (newValue != null) {
newValue.sortModeProperty().addListener(listener);
}
if (oldValue != null) {
oldValue.sortModeProperty().removeListener(listener);
}
});
}
public void addNewCollection() {
PlatformThread.runLaterIfNeeded(() -> {
var col = DataSourceCollection.createNew(I18n.get("newCollection"));
DataStorage.get().addCollection(col);
allGroups.stream()
.filter(g -> g.getCollection().equals(col))
.findAny()
.ifPresent(selectedGroup::set);
});
}
public ObservableList<SourceEntryWrapper> getAllEntries() {
return allEntries;
}
public ObservableList<SourceEntryWrapper> getShownEntries() {
return shownEntries;
}
public ObservableBooleanValue getStorageEmpty() {
return storageEmpty;
}
public SourceCollectionWrapper getSelectedGroup() {
return selectedGroup.get();
}
public SimpleObjectProperty<SourceCollectionWrapper> selectedGroupProperty() {
return selectedGroup;
}
private void addCollectionListChangeListeners() {
allGroups.setAll(filter(FXCollections.observableList(DataStorage.get().getSourceCollections().stream()
.map(SourceCollectionWrapper::new)
.toList())));
filter.createFilterBinding(
filter(allGroups),
shownGroups,
new SimpleObjectProperty<>(
Comparator.<SourceCollectionWrapper, Instant>comparing(e -> e.getLastAccess())
.reversed()));
DataStorage.get().addListener(new StorageListener() {
@Override
public void onStoreAdd(DataStoreEntry entry) {}
@Override
public void onStoreRemove(DataStoreEntry entry) {}
@Override
public void onCollectionAdd(DataSourceCollection collection) {
PlatformThread.runLaterIfNeeded(() -> {
var sg = new SourceCollectionWrapper(collection);
allGroups.add(sg);
});
}
@Override
public void onCollectionRemove(DataSourceCollection collection) {
PlatformThread.runLaterIfNeeded(() -> {
allGroups.removeIf(g -> g.getCollection().equals(collection));
});
}
});
shownGroups.addListener((ListChangeListener<? super SourceCollectionWrapper>) (c) -> {
if (selectedGroup.get() != null && !shownGroups.contains(selectedGroup.get())) {
selectedGroup.set(null);
}
});
shownGroups.addListener((ListChangeListener<? super SourceCollectionWrapper>) c -> {
if (c.getList().size() == 1) {
selectedGroup.set(c.getList().get(0));
}
});
}
private ObservableList<SourceCollectionWrapper> filter(ObservableList<SourceCollectionWrapper> list) {
return list.filtered(storeEntryWrapper -> {
if (AppPrefs.get().developerMode().getValue() && AppPrefs.get().developerShowHiddenEntries().get()) {
return true;
} else {
return !storeEntryWrapper.isInternal();
}
});
}
public SourceCollectionWrapper getGroup(SourceEntryWrapper e) {
return allGroups.stream()
.filter(g -> g.getEntries().contains(e))
.findFirst()
.orElseThrow();
}
public ObservableList<SourceEntryWrapper> getFilteredEntries(SourceCollectionWrapper g) {
var filtered = FXCollections.<SourceEntryWrapper>observableArrayList();
filter.createFilterBinding(
g.entriesProperty(),
filtered,
new SimpleObjectProperty<>(Comparator.<SourceEntryWrapper, Instant>comparing(
e -> e.getEntry().getLastAccess())
.reversed()));
return filtered;
}
private void addEntryListListeners() {
filter.createFilterBinding(
allEntries,
shownEntries,
Bindings.createObjectBinding(
() -> {
return sortMode.getValue() != null
? sortMode.getValue().comparator()
: Comparator.<SourceEntryWrapper>comparingInt(o -> o.hashCode());
},
sortMode));
selectedGroup.addListener((c, o, n) -> {
if (o != null) {
Bindings.unbindContent(allEntries, o.getEntries());
}
if (n != null) {
Bindings.bindContent(allEntries, n.getEntries());
}
});
}
public ObservableList<SourceCollectionWrapper> getShownGroups() {
return shownGroups;
}
public ObservableList<SourceCollectionWrapper> getAllGroups() {
return allGroups;
}
public StorageFilter getFilter() {
return filter;
}
}

View file

@ -0,0 +1,165 @@
package io.xpipe.app.comp.storage.collection;
import io.xpipe.app.comp.source.GuiDsCreatorMultiStep;
import io.xpipe.app.comp.storage.StorageFilter;
import io.xpipe.app.comp.storage.source.SourceEntryDisplayMode;
import io.xpipe.app.comp.storage.source.SourceEntryWrapper;
import io.xpipe.app.storage.CollectionListener;
import io.xpipe.app.storage.DataSourceCollection;
import io.xpipe.app.storage.DataSourceEntry;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.core.impl.FileStore;
import io.xpipe.extension.DataSourceProvider;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import java.nio.file.Path;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class SourceCollectionWrapper implements StorageFilter.Filterable {
private final Property<String> name;
private final IntegerProperty size;
private final ListProperty<SourceEntryWrapper> entries;
private final DataSourceCollection collection;
private final Property<Instant> lastAccess;
private final Property<SourceCollectionSortMode> sortMode =
new SimpleObjectProperty<>(SourceCollectionSortMode.DATE_DESC);
private final Property<SourceEntryDisplayMode> displayMode =
new SimpleObjectProperty<>(SourceEntryDisplayMode.LIST);
public SourceCollectionWrapper(DataSourceCollection collection) {
this.collection = collection;
this.entries =
new SimpleListProperty<SourceEntryWrapper>(FXCollections.observableList(collection.getEntries().stream()
.map(SourceEntryWrapper::new)
.collect(Collectors.toCollection(ArrayList::new))));
this.size = new SimpleIntegerProperty(collection.getEntries().size());
this.name = new SimpleStringProperty(collection.getName());
this.lastAccess = new SimpleObjectProperty<>(collection.getLastAccess().minus(Duration.ofMillis(500)));
setupListeners();
}
public ReadOnlyBooleanProperty emptyProperty() {
return entries.emptyProperty();
}
public boolean isDeleteable() {
return !isInternal();
}
public boolean isRenameable() {
return !isInternal();
}
public boolean isInternal() {
return collection.equals(DataStorage.get().getInternalCollection());
}
public void dropFile(Path file) {
var store = FileStore.local(file);
GuiDsCreatorMultiStep.showForStore(DataSourceProvider.Category.STREAM, store, this.getCollection());
}
public void delete() {
DataStorage.get().deleteCollection(this.collection);
}
public void clean() {
var entries = List.copyOf(collection.getEntries());
entries.forEach(e -> DataStorage.get().deleteEntry(e));
}
private void setupListeners() {
name.addListener((c, o, n) -> {
collection.setName(n);
});
collection.addListener(new CollectionListener() {
@Override
public void onUpdate() {
lastAccess.setValue(collection.getLastAccess().minus(Duration.ofMillis(500)));
name.setValue(collection.getName());
}
@Override
public void onEntryAdd(DataSourceEntry entry) {
var e = new SourceEntryWrapper(entry);
entries.add(e);
}
@Override
public void onEntryRemove(DataSourceEntry entry) {
entries.removeIf(e -> e.getEntry().equals(entry));
}
});
}
public DataSourceCollection getCollection() {
return collection;
}
public String getName() {
return name.getValue();
}
public Property<String> nameProperty() {
return name;
}
public int getSize() {
return size.get();
}
public IntegerProperty sizeProperty() {
return size;
}
public ObservableList<SourceEntryWrapper> getEntries() {
return entries.get();
}
public ListProperty<SourceEntryWrapper> entriesProperty() {
return entries;
}
@Override
public boolean shouldShow(String filter) {
if (isInternal()) {
// return getEntries().stream().anyMatch(e -> e.shouldShow(filter));
}
return getName().toLowerCase().contains(filter.toLowerCase())
|| entries.stream().anyMatch(e -> e.shouldShow(filter));
}
public Instant getLastAccess() {
return lastAccess.getValue();
}
public Property<Instant> lastAccessProperty() {
return lastAccess;
}
public SourceCollectionSortMode getSortMode() {
return sortMode.getValue();
}
public Property<SourceCollectionSortMode> sortModeProperty() {
return sortMode;
}
public SourceEntryDisplayMode getDisplayMode() {
return displayMode.getValue();
}
public Property<SourceEntryDisplayMode> displayModeProperty() {
return displayMode;
}
}

View file

@ -0,0 +1,224 @@
package io.xpipe.app.comp.storage.source;
import io.xpipe.app.comp.base.LazyTextFieldComp;
import io.xpipe.app.comp.base.LoadingOverlayComp;
import io.xpipe.app.comp.source.DsDataTransferComp;
import io.xpipe.app.comp.storage.DataSourceTypeComp;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppResources;
import io.xpipe.app.storage.DataSourceEntry;
import io.xpipe.core.store.DataFlow;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.SimpleComp;
import io.xpipe.extension.fxcomps.augment.GrowAugment;
import io.xpipe.extension.fxcomps.impl.FancyTooltipAugment;
import io.xpipe.extension.fxcomps.impl.IconButtonComp;
import io.xpipe.extension.fxcomps.impl.LabelComp;
import io.xpipe.extension.fxcomps.impl.PrettyImageComp;
import io.xpipe.extension.fxcomps.util.PlatformThread;
import io.xpipe.extension.fxcomps.util.SimpleChangeListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty;
import javafx.css.PseudoClass;
import javafx.geometry.HPos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.*;
import lombok.SneakyThrows;
public class SourceEntryComp extends SimpleComp {
private static final double SOURCE_TYPE_WIDTH = 0.09;
private static final double NAME_WIDTH = 0.3;
private static final double DETAILS_WIDTH = 0.43;
private static final PseudoClass FAILED = PseudoClass.getPseudoClass("failed");
private static final PseudoClass INCOMPLETE = PseudoClass.getPseudoClass("incomplete");
private static Image DND_IMAGE = null;
private final SourceEntryWrapper entry;
public SourceEntryComp(SourceEntryWrapper entry) {
this.entry = entry;
}
private Label createSize() {
var size = new Label();
size.textProperty().bind(PlatformThread.sync(entry.getStoreSummary()));
size.getStyleClass().add("size");
AppFont.small(size);
return size;
}
private LazyTextFieldComp createName() {
var name = new LazyTextFieldComp(entry.getName());
name.apply(s -> AppFont.header(s.get()));
return name;
}
private void applyState(Node node) {
SimpleChangeListener.apply(PlatformThread.sync(entry.getState()), val -> {
switch (val) {
case LOAD_FAILED -> {
node.pseudoClassStateChanged(FAILED, true);
node.pseudoClassStateChanged(INCOMPLETE, false);
}
case INCOMPLETE -> {
node.pseudoClassStateChanged(FAILED, false);
node.pseudoClassStateChanged(INCOMPLETE, true);
}
default -> {
node.pseudoClassStateChanged(FAILED, false);
node.pseudoClassStateChanged(INCOMPLETE, false);
}
}
});
}
@SneakyThrows
@Override
protected Region createSimple() {
var loading = new LoadingOverlayComp(Comp.of(() -> createContent()), entry.getLoading());
var region = loading.createRegion();
return region;
}
protected Region createContent() {
var name = createName().createRegion();
var size = createSize();
var img = entry.getState().getValue() == DataSourceEntry.State.LOAD_FAILED
? "disabled_icon.png"
: entry.getEntry().getProvider().getDisplayIconFileName();
var storeIcon = new PrettyImageComp(new SimpleStringProperty(img), 60, 50).createRegion();
var desc = new LabelComp(entry.getInformation()).createRegion();
desc.getStyleClass().add("description");
AppFont.header(desc);
var date = new Label();
date.textProperty().bind(AppI18n.readableDuration("usedDate", PlatformThread.sync(entry.getLastUsed())));
date.getStyleClass().add("date");
AppFont.small(date);
var grid = new GridPane();
grid.getColumnConstraints()
.addAll(
createShareConstraint(grid, SOURCE_TYPE_WIDTH),
createShareConstraint(grid, NAME_WIDTH),
new ColumnConstraints(-1));
var typeLogo = new DataSourceTypeComp(
entry.getEntry().getDataSourceType(),
entry.getDataFlow().getValue())
.createRegion();
typeLogo.maxWidthProperty().bind(typeLogo.heightProperty());
grid.add(typeLogo, 0, 0, 1, 2);
GridPane.setHalignment(typeLogo, HPos.CENTER);
grid.add(name, 1, 0);
grid.add(date, 1, 1);
grid.add(storeIcon, 2, 0, 1, 2);
grid.add(size, 3, 1);
grid.add(desc, 3, 0);
grid.setVgap(5);
AppFont.small(size);
AppFont.small(date);
grid.prefHeightProperty()
.bind(Bindings.createDoubleBinding(
() -> {
return name.getHeight() + date.getHeight() + 5;
},
name.heightProperty(),
date.heightProperty()));
grid.getStyleClass().add("content");
grid.setMaxHeight(100);
grid.setHgap(8);
var buttons = new HBox();
buttons.setFillHeight(true);
buttons.getChildren().add(createPipeButton().createRegion());
// buttons.getChildren().add(createUpdateButton().createRegion());
buttons.getChildren().add(createSettingsButton(name).createRegion());
buttons.setMinWidth(Region.USE_PREF_SIZE);
var hbox = new HBox(grid, buttons);
hbox.getStyleClass().add("storage-entry-comp");
HBox.setHgrow(grid, Priority.ALWAYS);
buttons.prefHeightProperty().bind(hbox.heightProperty());
hbox.getProperties().put("entry", this.entry);
hbox.setOnDragDetected(e -> {
if (!entry.getUsable().get()) {
return;
}
if (DND_IMAGE == null) {
var url = AppResources.getResourceURL(AppResources.XPIPE_MODULE, "img/file_drag_icon.png")
.orElseThrow();
DND_IMAGE = new Image(url.toString(), 80, 80, true, false);
}
Dragboard db = hbox.startDragAndDrop(TransferMode.MOVE);
var cc = new ClipboardContent();
cc.putString("");
db.setContent(cc);
db.setDragView(DND_IMAGE, 30, 60);
e.consume();
});
applyState(hbox);
return hbox;
}
private Comp<?> createSettingsButton(Region nameField) {
var settingsButton = new IconButtonComp("mdi2v-view-headline");
settingsButton.styleClass("settings");
settingsButton.apply(new SourceEntryContextMenu<>(true, entry, nameField));
settingsButton.apply(GrowAugment.create(false, true));
settingsButton.apply(s -> {
s.get().prefWidthProperty().bind(Bindings.divide(s.get().heightProperty(), 1.35));
});
settingsButton.apply(new FancyTooltipAugment<>("entrySettings"));
return settingsButton;
}
private Comp<?> createPipeButton() {
var pipeButton = new IconButtonComp("mdi2p-pipe-disconnected", () -> {
DsDataTransferComp.showPipeWindow(this.entry.getEntry());
});
pipeButton.styleClass("retrieve");
pipeButton.apply(GrowAugment.create(false, true));
pipeButton.apply(s -> {
s.get().prefWidthProperty().bind(Bindings.divide(s.get().heightProperty(), 1.35));
});
var disabled = Bindings.createBooleanBinding(
() -> {
if (entry.getDataFlow().getValue() == null) {
return true;
}
return entry.getDataFlow().getValue() == DataFlow.OUTPUT
|| entry.getDataFlow().getValue() == DataFlow.TRANSFORMER;
},
entry.getDataFlow());
pipeButton.disable(disabled).apply(s -> s.get());
pipeButton.apply(new FancyTooltipAugment<>("retrieve"));
return pipeButton;
}
private ColumnConstraints createShareConstraint(Region r, double share) {
var cc = new ColumnConstraints();
cc.prefWidthProperty().bind(Bindings.createDoubleBinding(() -> r.getWidth() * share, r.widthProperty()));
cc.setMaxWidth(750 * share);
return cc;
}
}

View file

@ -0,0 +1,96 @@
package io.xpipe.app.comp.storage.source;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataSourceEntry;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.extension.I18n;
import io.xpipe.extension.event.ErrorEvent;
import io.xpipe.extension.fxcomps.CompStructure;
import io.xpipe.extension.fxcomps.augment.PopupMenuAugment;
import io.xpipe.extension.util.OsHelper;
import javafx.beans.binding.Bindings;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.layout.Region;
import org.kordamp.ikonli.javafx.FontIcon;
public class SourceEntryContextMenu<S extends CompStructure<?>> extends PopupMenuAugment<S> {
private final SourceEntryWrapper entry;
private final Region renameTextField;
public SourceEntryContextMenu(boolean showOnPrimaryButton, SourceEntryWrapper entry, Region renameTextField) {
super(showOnPrimaryButton);
this.entry = entry;
this.renameTextField = renameTextField;
}
@Override
protected ContextMenu createContextMenu() {
var cm = new ContextMenu();
AppFont.normal(cm.getStyleableNode());
for (var actionProvider : entry.getActionProviders()) {
var name = actionProvider.getName(entry.getEntry().getSource().asNeeded());
var icon = actionProvider.getIcon(entry.getEntry().getSource().asNeeded());
var item = new MenuItem(null, new FontIcon(icon));
item.setOnAction(event -> {
try {
actionProvider.execute(entry.getEntry().getSource().asNeeded());
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
}
});
item.textProperty().bind(name);
// item.setDisable(!entry.getState().getValue().isUsable());
cm.getItems().add(item);
// actionProvider.applyToRegion(entry.getEntry().getStore().asNeeded(), region);
}
if (entry.getActionProviders().size() > 0) {
cm.getItems().add(new SeparatorMenuItem());
}
var properties = new MenuItem(I18n.get("properties"), new FontIcon("mdi2a-application-cog"));
properties.setOnAction(e -> {});
// cm.getItems().add(properties);
var rename = new MenuItem(I18n.get("rename"), new FontIcon("mdi2r-rename-box"));
rename.setOnAction(e -> {
renameTextField.requestFocus();
});
cm.getItems().add(rename);
var validate = new MenuItem(I18n.get("refresh"), new FontIcon("mdal-360"));
validate.setOnAction(event -> {
DataStorage.get().refreshAsync(entry.getEntry(), true);
});
cm.getItems().add(validate);
var edit = new MenuItem(I18n.get("edit"), new FontIcon("mdal-edit"));
edit.setOnAction(event -> entry.editDialog());
edit.disableProperty().bind(Bindings.equal(DataSourceEntry.State.LOAD_FAILED, entry.getState()));
cm.getItems().add(edit);
var del = new MenuItem(I18n.get("delete"), new FontIcon("mdal-delete_outline"));
del.setOnAction(e -> {
entry.delete();
});
cm.getItems().add(del);
if (AppPrefs.get().developerMode().getValue()) {
cm.getItems().add(new SeparatorMenuItem());
var openDir = new MenuItem(I18n.get("browseInternal"), new FontIcon("mdi2f-folder-open-outline"));
openDir.setOnAction(e -> {
OsHelper.browsePath(entry.getEntry().getDirectory());
});
cm.getItems().add(openDir);
}
return cm;
}
}

View file

@ -0,0 +1,44 @@
package io.xpipe.app.comp.storage.source;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import java.util.List;
import java.util.stream.Collectors;
public interface SourceEntryDisplayMode {
SourceEntryDisplayMode LIST = new ListMode();
SourceEntryDisplayMode TILES = new ListMode();
public Region create(List<SourceEntryWrapper> entries);
static class ListMode implements SourceEntryDisplayMode {
private static final double SOURCE_TYPE_WIDTH = 0.15;
private static final double NAME_WIDTH = 0.4;
private static final double STORE_TYPE_WIDTH = 0.1;
private static final double DETAILS_WIDTH = 0.35;
@Override
public Region create(List<SourceEntryWrapper> entries) {
VBox content = new VBox();
Runnable updateList = () -> {
var nw = entries.stream()
.map(v -> {
return new SourceEntryComp(v).createRegion();
})
.collect(Collectors.toList());
content.getChildren().setAll(nw);
};
updateList.run();
content.setFillWidth(true);
content.setSpacing(5);
content.getStyleClass().add("content");
content.getStyleClass().add("list-mode");
return content;
}
}
}

View file

@ -0,0 +1,70 @@
package io.xpipe.app.comp.storage.source;
import io.xpipe.app.comp.base.FileDropOverlayComp;
import io.xpipe.app.comp.base.MultiContentComp;
import io.xpipe.app.comp.storage.collection.SourceCollectionEmptyIntroComp;
import io.xpipe.app.comp.storage.collection.SourceCollectionViewState;
import io.xpipe.app.comp.storage.collection.SourceCollectionWrapper;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.SimpleComp;
import javafx.application.Platform;
import javafx.beans.value.ObservableBooleanValue;
import javafx.collections.ListChangeListener;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.Region;
import java.util.List;
import java.util.Map;
public class SourceEntryListComp extends SimpleComp {
private final SourceCollectionWrapper group;
public SourceEntryListComp(SourceCollectionWrapper group) {
this.group = group;
}
@SuppressWarnings("unchecked")
private Region createList() {
if (group == null) {
return null;
}
var content =
group.getDisplayMode().create(SourceCollectionViewState.get().getShownEntries());
var cp = new ScrollPane(content);
cp.setFitToWidth(true);
content.getStyleClass().add("content-pane");
cp.getStyleClass().add("storage-entry-list-comp");
SourceCollectionViewState.get().getShownEntries().addListener((ListChangeListener<? super SourceEntryWrapper>)
(c) -> {
Platform.runLater(() -> {
cp.setContent(group.getDisplayMode().create((List<SourceEntryWrapper>) c.getList()));
});
});
return cp;
}
@Override
protected Region createSimple() {
Map<Comp<?>, ObservableBooleanValue> map;
if (group == null) {
map = Map.of(
new SourceStorageEmptyIntroComp(),
SourceCollectionViewState.get().getStorageEmpty());
} else {
map = Map.of(
Comp.of(() -> createList()),
group.emptyProperty().not(),
new SourceCollectionEmptyIntroComp(),
group.emptyProperty());
}
var overlay = new FileDropOverlayComp<>(new MultiContentComp(map), files -> {
files.forEach(group::dropFile);
});
return overlay.createRegion();
}
}

View file

@ -0,0 +1,295 @@
package io.xpipe.app.comp.storage.source;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.comp.base.CountComp;
import io.xpipe.app.comp.source.GuiDsCreatorMultiStep;
import io.xpipe.app.comp.storage.collection.SourceCollectionSortMode;
import io.xpipe.app.comp.storage.collection.SourceCollectionViewState;
import io.xpipe.app.comp.storage.collection.SourceCollectionWrapper;
import io.xpipe.app.core.AppFont;
import io.xpipe.extension.DataSourceProvider;
import io.xpipe.extension.I18n;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.SimpleComp;
import io.xpipe.extension.fxcomps.impl.FancyTooltipAugment;
import io.xpipe.extension.fxcomps.impl.HorizontalComp;
import io.xpipe.extension.fxcomps.impl.IconButtonComp;
import io.xpipe.extension.fxcomps.impl.VerticalComp;
import io.xpipe.extension.fxcomps.util.BindingsHelper;
import javafx.beans.binding.Bindings;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.Separator;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.layout.*;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
public class SourceEntryListHeaderComp extends SimpleComp {
private final SourceCollectionWrapper group;
public SourceEntryListHeaderComp(SourceCollectionWrapper group) {
this.group = group;
}
private Comp<?> createAlphabeticalSortButton() {
var icon = Bindings.createStringBinding(
() -> {
if (group.getSortMode() == SourceCollectionSortMode.ALPHABETICAL_ASC) {
return "mdi2s-sort-alphabetical-descending";
}
if (group.getSortMode() == SourceCollectionSortMode.ALPHABETICAL_DESC) {
return "mdi2s-sort-alphabetical-ascending";
}
return "mdi2s-sort-alphabetical-descending";
},
group.sortModeProperty());
var alphabetical = new IconButtonComp(icon, () -> {
if (group.getSortMode() == SourceCollectionSortMode.ALPHABETICAL_ASC) {
group.sortModeProperty().setValue(SourceCollectionSortMode.ALPHABETICAL_DESC);
} else if (group.getSortMode() == SourceCollectionSortMode.ALPHABETICAL_DESC) {
group.sortModeProperty().setValue(SourceCollectionSortMode.ALPHABETICAL_ASC);
} else {
group.sortModeProperty().setValue(SourceCollectionSortMode.ALPHABETICAL_ASC);
}
});
alphabetical.apply(alphabeticalR -> {
alphabeticalR
.get()
.opacityProperty()
.bind(Bindings.createDoubleBinding(
() -> {
if (group.getSortMode() == SourceCollectionSortMode.ALPHABETICAL_ASC
|| group.getSortMode() == SourceCollectionSortMode.ALPHABETICAL_DESC) {
return 1.0;
}
return 0.4;
},
group.sortModeProperty()));
});
alphabetical.apply(new FancyTooltipAugment<>("sortAlphabetical"));
alphabetical.shortcut(new KeyCodeCombination(KeyCode.P, KeyCombination.SHORTCUT_DOWN));
return alphabetical;
}
private Comp<?> createDateSortButton() {
var icon = Bindings.createStringBinding(
() -> {
if (group.getSortMode() == SourceCollectionSortMode.DATE_ASC) {
return "mdi2s-sort-clock-ascending-outline";
}
if (group.getSortMode() == SourceCollectionSortMode.DATE_DESC) {
return "mdi2s-sort-clock-descending-outline";
}
return "mdi2s-sort-clock-ascending-outline";
},
group.sortModeProperty());
var date = new IconButtonComp(icon, () -> {
if (group.getSortMode() == SourceCollectionSortMode.DATE_ASC) {
group.sortModeProperty().setValue(SourceCollectionSortMode.DATE_DESC);
} else if (group.getSortMode() == SourceCollectionSortMode.DATE_DESC) {
group.sortModeProperty().setValue(SourceCollectionSortMode.DATE_ASC);
} else {
group.sortModeProperty().setValue(SourceCollectionSortMode.DATE_ASC);
}
});
date.apply(dateR -> {
dateR.get()
.opacityProperty()
.bind(Bindings.createDoubleBinding(
() -> {
if (group.getSortMode() == SourceCollectionSortMode.DATE_ASC
|| group.getSortMode() == SourceCollectionSortMode.DATE_DESC) {
return 1.0;
}
return 0.4;
},
group.sortModeProperty()));
});
date.apply(new FancyTooltipAugment<>("sortLastUsed"));
date.shortcut(new KeyCodeCombination(KeyCode.L, KeyCombination.SHORTCUT_DOWN));
return date;
}
private Comp<?> createListDisplayModeButton() {
var list = new IconButtonComp("mdi2f-format-list-bulleted-type", () -> {
group.displayModeProperty().setValue(SourceEntryDisplayMode.LIST);
});
list.apply(dateR -> {
dateR.get()
.opacityProperty()
.bind(Bindings.createDoubleBinding(
() -> {
if (group.getDisplayMode() == SourceEntryDisplayMode.LIST) {
return 1.0;
}
return 0.4;
},
group.displayModeProperty()));
});
list.apply(new FancyTooltipAugment<>("displayList"));
return list;
}
private Comp<?> createTilesDisplayModeButton() {
var tiles = new IconButtonComp("mdal-apps", () -> {
group.displayModeProperty().setValue(SourceEntryDisplayMode.TILES);
});
tiles.apply(dateR -> {
dateR.get()
.opacityProperty()
.bind(Bindings.createDoubleBinding(
() -> {
if (group.getDisplayMode() == SourceEntryDisplayMode.TILES) {
return 1.0;
}
return 0.4;
},
group.displayModeProperty()));
});
tiles.apply(new FancyTooltipAugment<>("displayTiles"));
return tiles;
}
private Comp<?> createSortButtonBar() {
return new HorizontalComp(List.of(createDateSortButton(), createAlphabeticalSortButton()));
}
private Comp<?> createDisplayModeButtonBar() {
return new HorizontalComp(List.of(createListDisplayModeButton(), createTilesDisplayModeButton()));
}
private Comp<?> createRightButtons() {
var v = new VerticalComp(List.of(
createDisplayModeButtonBar().apply(struc -> struc.get().setVisible(false)),
Comp.of(() -> {
return new StackPane(new Separator(Orientation.HORIZONTAL));
}),
createSortButtonBar()));
v.apply(r -> {
var sep = r.get().getChildren().get(1);
VBox.setVgrow(sep, Priority.ALWAYS);
})
.apply(s -> {
s.get()
.visibleProperty()
.bind(BindingsHelper.persist(Bindings.greaterThan(
Bindings.size(
SourceCollectionViewState.get().getAllEntries()),
0)));
});
return v;
}
@Override
protected Region createSimple() {
var label = new Label(I18n.get("none"));
if (SourceCollectionViewState.get().getSelectedGroup() != null) {
label.textProperty()
.bind(SourceCollectionViewState.get().getSelectedGroup().nameProperty());
}
label.getStyleClass().add("name");
SourceCollectionViewState.get().selectedGroupProperty().addListener((c, o, n) -> {
if (n != null) {
label.textProperty().bind(n.nameProperty());
}
});
var count = new CountComp<>(
SourceCollectionViewState.get().getShownEntries(),
SourceCollectionViewState.get().getAllEntries());
var close = new IconButtonComp("mdi2a-arrow-collapse-left", () -> SourceCollectionViewState.get()
.selectedGroupProperty()
.set(null))
.createRegion();
AppFont.medium(close);
var leftSep = new StackPane(new Separator(Orientation.HORIZONTAL));
var top = new HBox(label);
if (group != null) {
top.getChildren().add(0, close);
top.getChildren().addAll(count.createRegion());
}
top.setAlignment(Pos.CENTER_LEFT);
top.setSpacing(3);
var left = new VBox(top, leftSep, createActionsButtonBar().createRegion());
VBox.setVgrow(leftSep, Priority.ALWAYS);
var rspacer = new Region();
HBox.setHgrow(rspacer, Priority.ALWAYS);
var topBar = new HBox(left, rspacer);
if (group != null) {
var right = createRightButtons().createRegion();
topBar.getChildren().addAll(right);
}
topBar.setFillHeight(true);
topBar.setSpacing(13);
topBar.getStyleClass().add("top");
topBar.setAlignment(Pos.CENTER);
AppFont.header(topBar);
topBar.getStyleClass().add("bar");
topBar.getStyleClass().add("entry-bar");
return topBar;
}
private Comp<?> createActionsButtonBar() {
var newFile = new ButtonComp(
I18n.observable(group != null ? "addStream" : "pipeStream"),
new FontIcon("mdi2f-file-plus-outline"),
() -> {
var selected = SourceCollectionViewState.get()
.selectedGroupProperty()
.get();
GuiDsCreatorMultiStep.showCreation(
DataSourceProvider.Category.STREAM,
selected != null ? selected.getCollection() : null);
})
.shortcut(new KeyCodeCombination(KeyCode.S, KeyCombination.SHORTCUT_DOWN))
.apply(new FancyTooltipAugment<>("addStreamDataSource"));
var newDb = new ButtonComp(
I18n.observable(group != null ? "addDatabase" : "pipeDatabase"),
new FontIcon("mdi2d-database-plus-outline"),
() -> {
var selected = SourceCollectionViewState.get()
.selectedGroupProperty()
.get();
GuiDsCreatorMultiStep.showCreation(
DataSourceProvider.Category.DATABASE,
selected != null ? selected.getCollection() : null);
})
.shortcut(new KeyCodeCombination(KeyCode.D, KeyCombination.SHORTCUT_DOWN))
.apply(new FancyTooltipAugment<>("addDatabaseDataSource"));
// var newStructure = new IconButton("mdi2b-beaker-plus-outline", () -> {
// GuiDsCreatorMultiStep.show(StorageViewState.get().selectedGroupProperty().get(),
// DataSourceType.STRUCTURE);
// }).apply(JfxHelper.apply(new FancyTooltipAugment<>("addStructureDataSource")));
//
// var newText = new IconButton("mdi2t-text-box-plus-outline", () -> {
// GuiDsCreatorMultiStep.show(StorageViewState.get().selectedGroupProperty().get(),
// DataSourceType.TEXT);
// }).apply(JfxHelper.apply(new FancyTooltipAugment<>("addTextDataSource")));
//
// var newBinary = new IconButton("mdi2c-card-plus-outline", () -> {
// GuiDsCreatorMultiStep.show(StorageViewState.get().selectedGroupProperty().get(),
// DataSourceType.RAW);
// }).apply(JfxHelper.apply(new FancyTooltipAugment<>("addBinaryDataSource")));
//
// var newCollection = new IconButton("mdi2b-briefcase-plus-outline", () -> {
// GuiDsCreatorMultiStep.show(StorageViewState.get().selectedGroupProperty().get(),
// DataSourceType.COLLECTION);
// }).apply(JfxHelper.apply(new FancyTooltipAugment<>("addCollectionDataSource")));
var spaceOr = new Region();
spaceOr.setPrefWidth(12);
var box = new HorizontalComp(List.of(newFile, Comp.of(() -> spaceOr), newDb));
box.apply(s -> AppFont.normal(s.get()));
return box;
}
}

View file

@ -0,0 +1,125 @@
package io.xpipe.app.comp.storage.source;
import io.xpipe.app.comp.source.GuiDsCreatorMultiStep;
import io.xpipe.app.comp.storage.StorageFilter;
import io.xpipe.app.comp.storage.collection.SourceCollectionViewState;
import io.xpipe.app.comp.storage.collection.SourceCollectionWrapper;
import io.xpipe.app.storage.*;
import io.xpipe.core.source.DataSource;
import io.xpipe.core.store.DataFlow;
import io.xpipe.extension.DataSourceActionProvider;
import io.xpipe.extension.DataStoreProviders;
import io.xpipe.extension.I18n;
import io.xpipe.extension.event.ErrorEvent;
import io.xpipe.extension.fxcomps.util.PlatformThread;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import lombok.Value;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
@Value
public class SourceEntryWrapper implements StorageFilter.Filterable {
DataSourceEntry entry;
StringProperty name = new SimpleStringProperty();
BooleanProperty usable = new SimpleBooleanProperty();
StringProperty information = new SimpleStringProperty();
StringProperty storeSummary = new SimpleStringProperty();
Property<Instant> lastUsed = new SimpleObjectProperty<>();
Property<AccessMode> accessMode = new SimpleObjectProperty<>();
Property<DataFlow> dataFlow = new SimpleObjectProperty<>();
ObjectProperty<DataSourceEntry.State> state = new SimpleObjectProperty<>();
BooleanProperty loading = new SimpleBooleanProperty();
List<DataSourceActionProvider<?>> actionProviders = new ArrayList<>();
ListProperty<ApplicationAccess> accesses = new SimpleListProperty<>(FXCollections.observableArrayList());
public SourceEntryWrapper(DataSourceEntry entry) {
this.entry = entry;
entry.addListener(new StorageElement.Listener() {
@Override
public void onUpdate() {
PlatformThread.runLaterIfNeeded(() -> {
update();
});
}
});
update();
name.addListener((c, o, n) -> {
if (!entry.getName().equals(n)) {
entry.setName(n);
}
});
}
public void moveTo(SourceCollectionWrapper newGroup) {
var old = SourceCollectionViewState.get().getGroup(this);
old.getCollection().removeEntry(this.entry);
newGroup.getCollection().addEntry(this.entry);
}
public void editDialog() {
if (!DataStorage.get().getSourceEntries().contains(entry)) {
return;
}
GuiDsCreatorMultiStep.showEdit(getEntry());
}
public void delete() {
if (!DataStorage.get().getSourceEntries().contains(entry)) {
return;
}
DataStorage.get().deleteEntry(entry);
}
private <T extends DataSource<?>> void update() {
// Avoid reupdating name when changed from the name property!
if (!entry.getName().equals(name.getValue())) {
name.set(entry.getName());
}
lastUsed.setValue(entry.getLastUsed());
state.setValue(entry.getState());
usable.setValue(entry.getState().isUsable());
dataFlow.setValue(entry.getSource() != null ? entry.getSource().getFlow() : null);
storeSummary.setValue(
entry.getState().isUsable()
? DataStoreProviders.byStore(entry.getStore()).toSummaryString(entry.getStore(), 50)
: null);
information.setValue(
entry.getState() != DataSourceEntry.State.LOAD_FAILED
? entry.getInformation() != null
? entry.getInformation()
: entry.getProvider().getDisplayName()
: I18n.get("failedToLoad"));
loading.setValue(entry.getState() == null || entry.getState() == DataSourceEntry.State.VALIDATING);
actionProviders.clear();
actionProviders.addAll(DataSourceActionProvider.ALL.stream()
.filter(p -> {
try {
if (!entry.getState().isUsable()) {
return false;
}
return p.getApplicableClass()
.isAssignableFrom(entry.getSource().getClass())
&& p.isApplicable(entry.getSource().asNeeded());
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
return false;
}
})
.toList());
}
@Override
public boolean shouldShow(String filter) {
return getName().get().toLowerCase().contains(filter.toLowerCase());
}
}

View file

@ -0,0 +1,77 @@
package io.xpipe.app.comp.storage.source;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.util.Hyperlinks;
import io.xpipe.extension.I18n;
import io.xpipe.extension.fxcomps.SimpleComp;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.control.Separator;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.kordamp.ikonli.javafx.FontIcon;
public class SourceStorageEmptyIntroComp extends SimpleComp {
@Override
public Region createSimple() {
var title = new Label(I18n.get("introTitle"));
AppFont.setSize(title, 7);
title.getStyleClass().add("title-header");
var descFi = new FontIcon("mdi2i-information-outline");
var introDesc = new Label(I18n.get("introDescription"));
introDesc.heightProperty().addListener((c, o, n) -> {
descFi.iconSizeProperty().set(n.intValue());
});
var fi = new FontIcon("mdi2f-folder-plus-outline");
var addCollection = new Label(I18n.get("introCollection"), fi);
addCollection.heightProperty().addListener((c, o, n) -> {
fi.iconSizeProperty().set(n.intValue());
});
var pipeFi = new FontIcon("mdi2p-pipe-disconnected");
var pipe = new Label(I18n.get("introPipe"), pipeFi);
pipe.heightProperty().addListener((c, o, n) -> {
pipeFi.iconSizeProperty().set(n.intValue());
});
var dfi = new FontIcon("mdi2b-book-open-variant");
var documentation = new Label(I18n.get("introDocumentation"), dfi);
documentation.heightProperty().addListener((c, o, n) -> {
dfi.iconSizeProperty().set(n.intValue());
});
var docLink = new Hyperlink(Hyperlinks.DOCS_GETTING_STARTED);
docLink.setOnAction(e -> {
Hyperlinks.open(Hyperlinks.DOCS_GETTING_STARTED);
});
var docLinkPane = new StackPane(docLink);
docLinkPane.setAlignment(Pos.CENTER);
var v = new VBox(
title,
introDesc,
new Separator(Orientation.HORIZONTAL),
addCollection,
new Separator(Orientation.HORIZONTAL),
pipe,
new Separator(Orientation.HORIZONTAL),
documentation,
docLinkPane);
v.setMinWidth(Region.USE_PREF_SIZE);
v.setMaxWidth(Region.USE_PREF_SIZE);
v.setMinHeight(Region.USE_PREF_SIZE);
v.setMaxHeight(Region.USE_PREF_SIZE);
v.setSpacing(10);
v.getStyleClass().add("intro");
var sp = new StackPane(v);
sp.setAlignment(Pos.CENTER);
return sp;
}
}

View file

@ -0,0 +1,51 @@
package io.xpipe.app.comp.storage.store;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.comp.source.store.GuiDsStoreCreator;
import io.xpipe.app.core.AppFont;
import io.xpipe.extension.DataStoreProvider;
import io.xpipe.extension.I18n;
import io.xpipe.extension.fxcomps.SimpleComp;
import io.xpipe.extension.fxcomps.impl.FancyTooltipAugment;
import io.xpipe.extension.fxcomps.impl.VerticalComp;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.layout.Region;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
public class StoreCreationBarComp extends SimpleComp {
@Override
protected Region createSimple() {
var newStreamStore = new ButtonComp(
I18n.observable("addStreamStore"), new FontIcon("mdi2c-card-plus-outline"), () -> {
GuiDsStoreCreator.showCreation(DataStoreProvider.Category.STREAM);
})
.shortcut(new KeyCodeCombination(KeyCode.L, KeyCombination.SHORTCUT_DOWN))
.apply(new FancyTooltipAugment<>("addStreamStore"));
var newShellStore = new ButtonComp(
I18n.observable("addShellStore"), new FontIcon("mdi2h-home-plus-outline"), () -> {
GuiDsStoreCreator.showCreation(DataStoreProvider.Category.SHELL);
})
.shortcut(new KeyCodeCombination(KeyCode.M, KeyCombination.SHORTCUT_DOWN))
.apply(new FancyTooltipAugment<>("addShellStore"));
var newDbStore = new ButtonComp(
I18n.observable("addDatabaseStore"), new FontIcon("mdi2d-database-plus-outline"), () -> {
GuiDsStoreCreator.showCreation(DataStoreProvider.Category.DATABASE);
})
.shortcut(new KeyCodeCombination(KeyCode.D, KeyCombination.SHORTCUT_DOWN))
.apply(new FancyTooltipAugment<>("addDatabaseStore"));
var box = new VerticalComp(List.of(newShellStore, newDbStore, newStreamStore));
box.apply(s -> AppFont.medium(s.get()));
var bar = box.createRegion();
bar.getStyleClass().add("bar");
bar.getStyleClass().add("store-creation-bar");
return bar;
}
}

View file

@ -0,0 +1,273 @@
package io.xpipe.app.comp.storage.store;
import io.xpipe.app.comp.base.LazyTextFieldComp;
import io.xpipe.app.comp.base.LoadingOverlayComp;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.extension.I18n;
import io.xpipe.extension.event.ErrorEvent;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.SimpleComp;
import io.xpipe.extension.fxcomps.augment.GrowAugment;
import io.xpipe.extension.fxcomps.augment.PopupMenuAugment;
import io.xpipe.extension.fxcomps.impl.FancyTooltipAugment;
import io.xpipe.extension.fxcomps.impl.HorizontalComp;
import io.xpipe.extension.fxcomps.impl.IconButtonComp;
import io.xpipe.extension.fxcomps.impl.PrettyImageComp;
import io.xpipe.extension.fxcomps.util.PlatformThread;
import io.xpipe.extension.fxcomps.util.SimpleChangeListener;
import io.xpipe.extension.util.OsHelper;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty;
import javafx.css.PseudoClass;
import javafx.geometry.HPos;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Region;
import lombok.SneakyThrows;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.ArrayList;
public class StoreEntryComp extends SimpleComp {
private static final double NAME_WIDTH = 0.30;
private static final double STORE_TYPE_WIDTH = 0.08;
private static final double DETAILS_WIDTH = 0.52;
private static final double BUTTONS_WIDTH = 0.1;
private static final PseudoClass FAILED = PseudoClass.getPseudoClass("failed");
private static final PseudoClass INCOMPLETE = PseudoClass.getPseudoClass("incomplete");
private final StoreEntryWrapper entry;
public StoreEntryComp(StoreEntryWrapper entry) {
this.entry = entry;
}
private Label createInformation() {
var information = new Label();
information.textProperty().bind(PlatformThread.sync(entry.getInformation()));
information.getStyleClass().add("information");
AppFont.header(information);
return information;
}
private Label createSummary() {
var summary = new Label();
summary.textProperty().bind(PlatformThread.sync(entry.getSummary()));
summary.getStyleClass().add("summary");
AppFont.small(summary);
return summary;
}
private void applyState(Node node) {
SimpleChangeListener.apply(PlatformThread.sync(entry.getState()), val -> {
switch (val) {
case LOAD_FAILED -> {
node.pseudoClassStateChanged(FAILED, true);
node.pseudoClassStateChanged(INCOMPLETE, false);
}
case INCOMPLETE -> {
node.pseudoClassStateChanged(FAILED, false);
node.pseudoClassStateChanged(INCOMPLETE, true);
}
default -> {
node.pseudoClassStateChanged(FAILED, false);
node.pseudoClassStateChanged(INCOMPLETE, false);
}
}
});
}
private LazyTextFieldComp createName() {
var name = new LazyTextFieldComp(entry.nameProperty());
name.apply(struc -> struc.getTextField().editableProperty().bind(entry.getRenamable()));
name.apply(s -> AppFont.header(s.get()));
return name;
}
private Node createIcon() {
var img = entry.isDisabled()
? "disabled_icon.png"
: entry.getEntry().getProvider().getDisplayIconFileName();
var imageComp = new PrettyImageComp(new SimpleStringProperty(img), 55, 45);
var storeIcon = imageComp.createRegion();
storeIcon.getStyleClass().add("icon");
return storeIcon;
}
protected Region createContent() {
var name = createName().createRegion();
var size = createInformation();
var date = new Label();
date.textProperty().bind(AppI18n.readableDuration("usedDate", PlatformThread.sync(entry.lastAccessProperty())));
AppFont.small(date);
date.getStyleClass().add("date");
var grid = new GridPane();
var storeIcon = createIcon();
grid.getColumnConstraints()
.addAll(
createShareConstraint(grid, STORE_TYPE_WIDTH), createShareConstraint(grid, NAME_WIDTH),
createShareConstraint(grid, DETAILS_WIDTH), createShareConstraint(grid, BUTTONS_WIDTH));
grid.add(storeIcon, 0, 0, 1, 2);
grid.add(name, 1, 0);
grid.add(date, 1, 1);
grid.add(createSummary(), 2, 1);
grid.add(createInformation(), 2, 0);
grid.add(createButtonBar().createRegion(), 3, 0, 1, 2);
grid.setVgap(5);
GridPane.setHalignment(storeIcon, HPos.CENTER);
AppFont.small(size);
AppFont.small(date);
grid.getStyleClass().add("store-entry-comp");
grid.setOnMouseClicked(event -> {
if (entry.getEditable().get()) {
entry.editDialog();
}
});
applyState(grid);
return grid;
}
private Comp<?> createButtonBar() {
var list = new ArrayList<Comp<?>>();
for (var p : entry.getActionProviders().entrySet()) {
var actionProvider = p.getKey();
if (!actionProvider.isMajor()) {
continue;
}
var button = new IconButtonComp(
actionProvider.getIcon(entry.getEntry().getStore().asNeeded()), () -> {
try {
actionProvider.execute(entry.getEntry().getStore().asNeeded());
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
}
});
button.apply(new FancyTooltipAugment<>(
actionProvider.getName(entry.getEntry().getStore().asNeeded())));
button.disable(Bindings.not(p.getValue()));
list.add(button);
}
var settingsButton = createSettingsButton();
list.add(settingsButton);
return new HorizontalComp(list)
.apply(struc -> struc.get().setAlignment(Pos.CENTER_RIGHT))
.apply(struc -> {
for (Node child : struc.get().getChildren()) {
((Region) child)
.prefWidthProperty()
.bind((struc.get().heightProperty().divide(1.7)));
((Region) child).prefHeightProperty().bind((struc.get().heightProperty()));
}
});
}
private Comp<?> createSettingsButton() {
var settingsButton = new IconButtonComp("mdi2v-view-headline");
settingsButton.styleClass("settings");
settingsButton.apply(new PopupMenuAugment<>(true) {
@Override
protected ContextMenu createContextMenu() {
return StoreEntryComp.this.createContextMenu();
}
});
settingsButton.apply(GrowAugment.create(false, true));
settingsButton.apply(s -> {
s.get().prefWidthProperty().bind(Bindings.divide(s.get().heightProperty(), 1.35));
});
settingsButton.apply(new FancyTooltipAugment<>("entrySettings"));
return settingsButton;
}
private ContextMenu createContextMenu() {
var contextMenu = new ContextMenu();
AppFont.normal(contextMenu.getStyleableNode());
for (var p : entry.getActionProviders().entrySet()) {
var actionProvider = p.getKey();
if (actionProvider.isMajor()) {
continue;
}
var name = actionProvider.getName(entry.getEntry().getStore().asNeeded());
var icon = actionProvider.getIcon(entry.getEntry().getStore().asNeeded());
var item = new MenuItem(null, new FontIcon(icon));
item.setOnAction(event -> {
try {
actionProvider.execute(entry.getEntry().getStore().asNeeded());
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
}
});
item.textProperty().bind(name);
item.disableProperty().bind(Bindings.not(p.getValue()));
if (!actionProvider.showIfDisabled()) {
item.visibleProperty().bind(p.getValue());
}
contextMenu.getItems().add(item);
}
if (entry.getActionProviders().size() > 0) {
contextMenu.getItems().add(new SeparatorMenuItem());
}
if (AppPrefs.get().developerMode().getValue()) {
var browse = new MenuItem(I18n.get("browse"), new FontIcon("mdi2f-folder-open-outline"));
browse.setOnAction(event -> OsHelper.browsePath(entry.getEntry().getDirectory()));
contextMenu.getItems().add(browse);
}
var refresh = new MenuItem(I18n.get("refresh"), new FontIcon("mdal-360"));
refresh.disableProperty().bind(entry.getRefreshable().not());
refresh.setOnAction(event -> {
DataStorage.get().refreshAsync(entry.getEntry(), true);
});
contextMenu.getItems().add(refresh);
var edit = new MenuItem(I18n.get("edit"), new FontIcon("mdal-edit"));
edit.disableProperty().bind(entry.getEditable().not());
edit.setOnAction(event -> entry.editDialog());
contextMenu.getItems().add(edit);
var del = new MenuItem(I18n.get("delete"), new FontIcon("mdal-delete_outline"));
del.disableProperty().bind(entry.getDeletable().not());
del.setOnAction(event -> entry.delete());
contextMenu.getItems().add(del);
return contextMenu;
}
private ColumnConstraints createShareConstraint(Region r, double share) {
var cc = new ColumnConstraints();
cc.prefWidthProperty().bind(Bindings.createDoubleBinding(() -> r.getWidth() * share, r.widthProperty()));
return cc;
}
@SneakyThrows
@Override
protected Region createSimple() {
var loading = new LoadingOverlayComp(Comp.of(() -> createContent()), entry.getLoading());
var region = loading.createRegion();
return region;
}
}

View file

@ -0,0 +1,43 @@
package io.xpipe.app.comp.storage.store;
import io.xpipe.app.comp.base.ListViewComp;
import io.xpipe.app.comp.base.MultiContentComp;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.SimpleComp;
import io.xpipe.extension.fxcomps.augment.GrowAugment;
import io.xpipe.extension.fxcomps.util.BindingsHelper;
import javafx.beans.binding.Bindings;
import javafx.beans.value.ObservableBooleanValue;
import javafx.scene.layout.Region;
import java.util.Map;
public class StoreEntryListComp extends SimpleComp {
private Comp<?> createList() {
var content = new ListViewComp<>(
StoreViewState.get().getShownEntries(),
StoreViewState.get().getAllEntries(),
null,
(StoreEntryWrapper e) -> {
return new StoreEntryComp(e).apply(GrowAugment.create(true, false));
});
return content;
}
@Override
protected Region createSimple() {
var map = Map.<Comp<?>, ObservableBooleanValue>of(
createList(),
BindingsHelper.persist(Bindings.and(
Bindings.not(StoreViewState.get().emptyProperty()),
Bindings.not(Bindings.isEmpty(StoreViewState.get().getShownEntries())))),
new StoreStorageEmptyIntroComp(),
StoreViewState.get().emptyProperty(),
new StoreNotFoundComp(),
BindingsHelper.persist(Bindings.and(
Bindings.not(Bindings.isEmpty(StoreViewState.get().getAllEntries())),
Bindings.isEmpty(StoreViewState.get().getShownEntries()))));
return new MultiContentComp(map).createRegion();
}
}

View file

@ -0,0 +1,59 @@
package io.xpipe.app.comp.storage.store;
import io.xpipe.app.comp.base.CountComp;
import io.xpipe.app.core.AppFont;
import io.xpipe.extension.fxcomps.SimpleComp;
import io.xpipe.extension.fxcomps.impl.FilterComp;
import io.xpipe.extension.util.ThreadHelper;
import javafx.beans.property.SimpleStringProperty;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.layout.*;
public class StoreEntryListHeaderComp extends SimpleComp {
private Region createGroupListHeader() {
var label = new Label("Connections");
label.getStyleClass().add("name");
var count = new CountComp<>(
StoreViewState.get().getShownEntries(), StoreViewState.get().getAllEntries());
var spacer = new Region();
var topBar = new HBox(label, spacer, count.createRegion());
AppFont.setSize(topBar, 1);
topBar.setAlignment(Pos.CENTER);
HBox.setHgrow(spacer, Priority.ALWAYS);
topBar.getStyleClass().add("top");
return topBar;
}
private Region createGroupListFilter() {
var filledHerProperty = new SimpleStringProperty();
filledHerProperty.addListener((observable, oldValue, newValue) -> {
ThreadHelper.runAsync(() -> {
StoreViewState.get().getFilter().filterProperty().setValue(newValue);
});
});
var filter = new FilterComp(StoreViewState.get().getFilter().filterProperty());
filter.shortcut(new KeyCodeCombination(KeyCode.F, KeyCombination.SHORTCUT_DOWN), s -> {
s.getText().requestFocus();
});
var r = new StackPane(filter.createRegion());
r.setAlignment(Pos.CENTER);
r.getStyleClass().add("filter-bar");
AppFont.medium(r);
return r;
}
@Override
public Region createSimple() {
var bar = new VBox(createGroupListHeader(), createGroupListFilter());
bar.getStyleClass().add("bar");
bar.getStyleClass().add("store-header-bar");
return bar;
}
}

View file

@ -0,0 +1,155 @@
package io.xpipe.app.comp.storage.store;
import io.xpipe.app.comp.source.store.GuiDsStoreCreator;
import io.xpipe.app.comp.storage.StorageFilter;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.extension.DataStoreActionProvider;
import io.xpipe.extension.event.ErrorEvent;
import io.xpipe.extension.fxcomps.util.PlatformThread;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.value.ObservableBooleanValue;
import lombok.Getter;
import java.time.Duration;
import java.time.Instant;
import java.util.LinkedHashMap;
import java.util.Map;
@Getter
public class StoreEntryWrapper implements StorageFilter.Filterable {
private final Property<String> name;
private final DataStoreEntry entry;
private final Property<Instant> lastAccess;
private final BooleanProperty disabled = new SimpleBooleanProperty();
private final BooleanProperty loading = new SimpleBooleanProperty();
private final Property<DataStoreEntry.State> state = new SimpleObjectProperty<>();
private final StringProperty information = new SimpleStringProperty();
private final StringProperty summary = new SimpleStringProperty();
private final Map<DataStoreActionProvider<?>, ObservableBooleanValue> actionProviders;
private final BooleanProperty editable = new SimpleBooleanProperty();
private final BooleanProperty renamable = new SimpleBooleanProperty();
private final BooleanProperty refreshable = new SimpleBooleanProperty();
private final BooleanProperty deletable = new SimpleBooleanProperty();
public StoreEntryWrapper(DataStoreEntry entry) {
this.entry = entry;
this.name = new SimpleStringProperty(entry.getName());
this.lastAccess = new SimpleObjectProperty<>(entry.getLastAccess().minus(Duration.ofMillis(500)));
this.actionProviders = new LinkedHashMap<>();
DataStoreActionProvider.ALL.stream()
.filter(dataStoreActionProvider -> {
return !entry.isDisabled()
&& dataStoreActionProvider
.getApplicableClass()
.isAssignableFrom(entry.getStore().getClass());
})
.forEach(dataStoreActionProvider -> {
var property = Bindings.createBooleanBinding(
() -> {
if (!entry.getState().isUsable()) {
return false;
}
return dataStoreActionProvider.isApplicable(
entry.getStore().asNeeded());
},
disabledProperty(),
state,
lastAccess);
actionProviders.put(dataStoreActionProvider, property);
});
setupListeners();
update();
}
public void editDialog() {
GuiDsStoreCreator.showEdit(entry);
}
public void delete() {
DataStorage.get().deleteStoreEntry(this.entry);
}
private void setupListeners() {
name.addListener((c, o, n) -> {
entry.setName(n);
});
entry.addListener(() -> PlatformThread.runLaterIfNeeded(() -> {
update();
}));
}
public void update() {
// Avoid reupdating name when changed from the name property!
if (!entry.getName().equals(name.getValue())) {
name.setValue(entry.getName());
}
lastAccess.setValue(entry.getLastAccess());
disabled.setValue(entry.isDisabled());
state.setValue(entry.getState());
information.setValue(
entry.getInformation() != null
? entry.getInformation()
: entry.isDisabled() ? null : entry.getProvider().getDisplayName());
loading.setValue(entry.getState() == DataStoreEntry.State.VALIDATING);
if (entry.getState().isUsable()) {
try {
summary.setValue(entry.getProvider().toSummaryString(entry.getStore(), 50));
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
}
}
editable.setValue(entry.getState() != DataStoreEntry.State.LOAD_FAILED
&& (entry.getConfiguration().isEditable()
|| AppPrefs.get().developerDisableGuiRestrictions().get()));
renamable.setValue(entry.getConfiguration().isRenameable()
|| AppPrefs.get().developerDisableGuiRestrictions().getValue());
refreshable.setValue(entry.getConfiguration().isRefreshable()
|| AppPrefs.get().developerDisableGuiRestrictions().getValue());
deletable.setValue(entry.getConfiguration().isDeletable()
|| AppPrefs.get().developerDisableGuiRestrictions().getValue());
}
@Override
public boolean shouldShow(String filter) {
return getName().toLowerCase().contains(filter.toLowerCase())
|| (summary.get() != null && summary.get().toLowerCase().contains(filter.toLowerCase()))
|| (information.get() != null && information.get().toLowerCase().contains(filter.toLowerCase()));
}
public String getName() {
return name.getValue();
}
public Property<String> nameProperty() {
return name;
}
public DataStoreEntry getEntry() {
return entry;
}
public Instant getLastAccess() {
return lastAccess.getValue();
}
public Property<Instant> lastAccessProperty() {
return lastAccess;
}
public boolean isDisabled() {
return disabled.get();
}
public BooleanProperty disabledProperty() {
return disabled;
}
}

View file

@ -0,0 +1,21 @@
package io.xpipe.app.comp.storage.store;
import io.xpipe.extension.fxcomps.SimpleComp;
import io.xpipe.extension.fxcomps.augment.GrowAugment;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Region;
public class StoreLayoutComp extends SimpleComp {
@Override
protected Region createSimple() {
var listComp = new StoreEntryListComp().apply(GrowAugment.create(false, true));
var r = new BorderPane();
var listR = listComp.createRegion();
var groupHeader = new StoreSidebarComp().createRegion();
r.setLeft(groupHeader);
r.setCenter(listR);
return r;
}
}

View file

@ -0,0 +1,14 @@
package io.xpipe.app.comp.storage.store;
import io.xpipe.extension.fxcomps.SimpleComp;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
public class StoreNotFoundComp extends SimpleComp {
@Override
public Region createSimple() {
var sp = new StackPane();
return sp;
}
}

View file

@ -0,0 +1,23 @@
package io.xpipe.app.comp.storage.store;
import io.xpipe.extension.fxcomps.Comp;
import io.xpipe.extension.fxcomps.SimpleComp;
import io.xpipe.extension.fxcomps.impl.VerticalComp;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import java.util.List;
public class StoreSidebarComp extends SimpleComp {
@Override
protected Region createSimple() {
var sideBar = new VerticalComp(List.of(
new StoreEntryListHeaderComp(),
new StoreCreationBarComp(),
Comp.of(() -> new Region()).styleClass("bar").styleClass("filler-bar")));
sideBar.apply(s -> VBox.setVgrow(s.get().getChildren().get(2), Priority.ALWAYS));
sideBar.styleClass("sidebar");
return sideBar.createRegion();
}
}

View file

@ -0,0 +1,85 @@
package io.xpipe.app.comp.storage.store;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.util.Hyperlinks;
import io.xpipe.extension.I18n;
import io.xpipe.extension.fxcomps.SimpleComp;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.control.Separator;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.kordamp.ikonli.javafx.FontIcon;
public class StoreStorageEmptyIntroComp extends SimpleComp {
@Override
public Region createSimple() {
var title = new Label(I18n.get("storeIntroTitle"));
AppFont.setSize(title, 7);
title.getStyleClass().add("title-header");
var descFi = new FontIcon("mdi2i-information-outline");
var introDesc = new Label(I18n.get("storeIntroDescription"));
introDesc.heightProperty().addListener((c, o, n) -> {
descFi.iconSizeProperty().set(n.intValue());
});
var mfi = new FontIcon("mdi2h-home-plus-outline");
var machine = new Label(I18n.get("storeMachineDescription"), mfi);
machine.heightProperty().addListener((c, o, n) -> {
mfi.iconSizeProperty().set(n.intValue());
});
var dfi = new FontIcon("mdi2d-database-plus-outline");
var database = new Label(I18n.get("storeDatabaseDescription"), dfi);
database.heightProperty().addListener((c, o, n) -> {
dfi.iconSizeProperty().set(n.intValue());
});
var fi = new FontIcon("mdi2c-card-plus-outline");
var stream = new Label(I18n.get("storeStreamDescription"), fi);
stream.heightProperty().addListener((c, o, n) -> {
fi.iconSizeProperty().set(n.intValue());
});
var dofi = new FontIcon("mdi2b-book-open-variant");
var documentation = new Label(I18n.get("introDocumentation"), dofi);
documentation.heightProperty().addListener((c, o, n) -> {
dofi.iconSizeProperty().set(n.intValue());
});
var docLink = new Hyperlink(Hyperlinks.DOCS_GETTING_STARTED);
docLink.setOnAction(e -> {
Hyperlinks.open(Hyperlinks.DOCS_GETTING_STARTED);
});
var docLinkPane = new StackPane(docLink);
docLinkPane.setAlignment(Pos.CENTER);
var v = new VBox(
title,
introDesc,
new Separator(Orientation.HORIZONTAL),
machine,
new Separator(Orientation.HORIZONTAL),
database,
new Separator(Orientation.HORIZONTAL),
stream,
new Separator(Orientation.HORIZONTAL),
documentation,
docLinkPane);
v.setMinWidth(Region.USE_PREF_SIZE);
v.setMaxWidth(Region.USE_PREF_SIZE);
v.setMinHeight(Region.USE_PREF_SIZE);
v.setMaxHeight(Region.USE_PREF_SIZE);
v.setSpacing(10);
v.getStyleClass().add("intro");
var sp = new StackPane(v);
sp.setAlignment(Pos.CENTER);
return sp;
}
}

View file

@ -0,0 +1,113 @@
package io.xpipe.app.comp.storage.store;
import io.xpipe.app.comp.storage.StorageFilter;
import io.xpipe.app.storage.DataSourceCollection;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.storage.StorageListener;
import io.xpipe.extension.event.ErrorEvent;
import io.xpipe.extension.fxcomps.util.BindingsHelper;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableBooleanValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import java.time.Instant;
import java.util.Comparator;
import java.util.concurrent.CopyOnWriteArrayList;
public class StoreViewState {
private static StoreViewState INSTANCE;
private final StorageFilter filter = new StorageFilter();
private final ObservableList<StoreEntryWrapper> allEntries =
FXCollections.observableList(new CopyOnWriteArrayList<>());
private final ObservableList<StoreEntryWrapper> shownEntries =
FXCollections.observableList(new CopyOnWriteArrayList<>());
private final ObservableBooleanValue empty =
BindingsHelper.persist(Bindings.equal(Bindings.size(allEntries), 1));
private StoreViewState() {
try {
addStorageGroupListeners();
addShownContentChangeListeners();
} catch (Exception exception) {
ErrorEvent.fromThrowable(exception).handle();
}
}
public static void init() {
INSTANCE = new StoreViewState();
}
public static void reset() {
INSTANCE = null;
}
public static StoreViewState get() {
return INSTANCE;
}
private void addStorageGroupListeners() {
allEntries.setAll(FXCollections.observableArrayList(DataStorage.get().getStores().stream()
.map(StoreEntryWrapper::new)
.toList()));
DataStorage.get().addListener(new StorageListener() {
@Override
public void onStoreAdd(DataStoreEntry entry) {
Platform.runLater(() -> {
var sg = new StoreEntryWrapper(entry);
allEntries.add(sg);
});
}
@Override
public void onStoreRemove(DataStoreEntry entry) {
Platform.runLater(() -> {
allEntries.removeIf(e -> e.getEntry().equals(entry));
});
}
@Override
public void onCollectionAdd(DataSourceCollection collection) {}
@Override
public void onCollectionRemove(DataSourceCollection collection) {}
});
}
private void addShownContentChangeListeners() {
filter.createFilterBinding(
allEntries,
shownEntries,
new SimpleObjectProperty<>(Comparator.<StoreEntryWrapper, Instant>comparing(
storeEntryWrapper -> storeEntryWrapper.getLastAccess())
.reversed()));
}
public StorageFilter getFilter() {
return filter;
}
public ObservableList<StoreEntryWrapper> getAllEntries() {
return allEntries;
}
public ObservableList<StoreEntryWrapper> getShownEntries() {
return shownEntries;
}
public boolean isEmpty() {
return empty.get();
}
public ObservableBooleanValue emptyProperty() {
return empty;
}
}

View file

@ -0,0 +1,98 @@
package io.xpipe.app.core;
import io.xpipe.app.Main;
import io.xpipe.app.comp.AppLayoutComp;
import io.xpipe.extension.event.ErrorEvent;
import io.xpipe.extension.event.TrackEvent;
import io.xpipe.extension.fxcomps.util.PlatformThread;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.image.Image;
import javafx.scene.input.MouseEvent;
import javafx.stage.Stage;
import org.apache.commons.lang3.SystemUtils;
import javax.imageio.ImageIO;
import java.awt.*;
public class App extends Application {
private static App APP;
private Stage stage;
private Image icon;
public static boolean isPlatformRunning() {
return APP != null;
}
public static App getApp() {
return APP;
}
@Override
public void start(Stage primaryStage) {
TrackEvent.info("Application launched");
APP = this;
stage = primaryStage;
icon = AppImages.image("logo.png");
// Set dock icon explicitly on mac
// This is necessary in case X-Pipe was started through a script as it will have no icon otherwise
if (SystemUtils.IS_OS_MAC) {
try {
var iconUrl = Main.class.getResourceAsStream("resources/img/logo.png");
if (iconUrl != null) {
var awtIcon = ImageIO.read(iconUrl);
Taskbar.getTaskbar().setIconImage(awtIcon);
}
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).omitted(true).build().handle();
}
}
primaryStage.getIcons().clear();
primaryStage.getIcons().add(icon);
Platform.setImplicitExit(false);
}
public void close() {
Platform.runLater(() -> {
stage.hide();
TrackEvent.debug("Closed main window");
});
}
public void setupWindow() {
var content = new AppLayoutComp();
content.apply(struc -> {
struc.get().addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {
AppActionDetector.detectOnFocus();
});
});
var title = String.format("X-Pipe Desktop (%s)", AppProperties.get().getVersion());
var appWindow = new AppMainWindow(stage);
appWindow.initialize();
appWindow.setContent(title, content);
TrackEvent.info("Application window initialized");
stage.setOnShown(event -> {
focus();
});
appWindow.show();
}
public void focus() {
PlatformThread.runLaterIfNeeded(() -> {
stage.setAlwaysOnTop(true);
stage.setAlwaysOnTop(false);
stage.requestFocus();
});
}
public Image getIcon() {
return icon;
}
public Stage getStage() {
return stage;
}
}

View file

@ -0,0 +1,68 @@
package io.xpipe.app.core;
import io.xpipe.app.launcher.LauncherInput;
import io.xpipe.extension.I18n;
import javafx.scene.control.Alert;
import javafx.scene.input.Clipboard;
import javafx.scene.input.DataFormat;
import java.util.List;
public class AppActionDetector {
private static String lastDetectedAction;
private static String getClipboardAction() {
var content = Clipboard.getSystemClipboard().getContent(DataFormat.URL);
if (content == null) {
content = Clipboard.getSystemClipboard().getContent(DataFormat.PLAIN_TEXT);
}
return content != null?content.toString():null;
}
private static void handle(String content, boolean showAlert) {
var detected = LauncherInput.of(content);
if (detected.size() == 0) {
return;
}
if (showAlert && !showAlert()) {
return;
}
LauncherInput.handle(List.of(content));
}
public static void detectOnFocus() {
var content = getClipboardAction();
if (content == null) {
lastDetectedAction = null;
return;
}
if (content.equals(lastDetectedAction)) {
return;
}
lastDetectedAction = content;
handle(content, true);
}
public static void detectOnPaste() {
var content = getClipboardAction();
if (content == null) {
return;
}
lastDetectedAction = content;
handle(content, false);
}
private static boolean showAlert() {
var paste = AppWindowHelper.showBlockingAlert(alert -> {
alert.setAlertType(Alert.AlertType.CONFIRMATION);
alert.setTitle(I18n.get("clipboardActionDetectedTitle"));
alert.setHeaderText(I18n.get("clipboardActionDetectedHeader"));
alert.getDialogPane().setContent(AppWindowHelper.alertContentText(I18n.get("clipboardActionDetectedContent")));
}).map(buttonType -> buttonType.getButtonData().isDefaultButton()).orElse(false);
return paste;
}
}

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