mirror of
https://github.com/xpipe-io/xpipe.git
synced 2024-09-03 03:01:13 +12:00
Merge main repository
This commit is contained in:
parent
81369aca04
commit
d9caa29694
614 changed files with 271738 additions and 457 deletions
2
.gitattributes
vendored
2
.gitattributes
vendored
|
@ -1,3 +1,5 @@
|
||||||
* text=auto
|
* text=auto
|
||||||
|
*.sh text eol=lf
|
||||||
|
*.bat text eol=crlf
|
||||||
*.png binary
|
*.png binary
|
||||||
*.xcf binary
|
*.xcf binary
|
28
.github/workflows/build.yml
vendored
28
.github/workflows/build.yml
vendored
|
@ -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
|
|
39
.github/workflows/publish.yml
vendored
39
.github/workflows/publish.yml
vendored
|
@ -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 }}
|
|
12
.gitignore
vendored
12
.gitignore
vendored
|
@ -1,11 +1,15 @@
|
||||||
.gradle/
|
.gradle/
|
||||||
build/
|
build/
|
||||||
.idea
|
.idea
|
||||||
local/
|
lib/
|
||||||
local_test/
|
|
||||||
local_stage/
|
|
||||||
dev.properties
|
dev.properties
|
||||||
extensions.txt
|
extensions.txt
|
||||||
local/
|
dev_storage
|
||||||
|
local*/
|
||||||
|
.vs
|
||||||
.vscode
|
.vscode
|
||||||
|
obj
|
||||||
|
out
|
||||||
bin
|
bin
|
||||||
|
.DS_Store
|
||||||
|
ComponentsGenerated.wxs
|
169
README.md
169
README.md
|
@ -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)
|
<img src="https://user-images.githubusercontent.com/72509152/213873342-7638e830-8a95-4b5d-ad3e-5a9a0b4bf538.png" alt="drawing" width="300"/>
|
||||||
[![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)
|
|
||||||
|
|
||||||
## 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).
|
- Could we support connections to any remote system with pipes instead of limiting yourself to your local system?
|
||||||
This repository contains the following four modules:
|
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
|
- Could we support more than just transferring raw bytes and text?
|
||||||
- API - The API that can be used to interact with X-Pipe from any JVM-based language10
|
Why not work on a higher level of abstraction instead,
|
||||||
- Beacon - The X-Pipe beacon component is responsible for handling all communications between the X-Pipe daemon
|
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
|
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
|
||||||
|
|
||||||
## Installation / Usage
|
|
||||||
|
|
||||||
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.
|
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
|
The other modules make up the X-Pipe implementation and are licensed under GPL:
|
||||||
reference when implementing the communication of an API or program that interacts with the X-Pipe daemon.
|
- [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 *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.
|
|
||||||
|
|
||||||
## Development Notes
|
## Development
|
||||||
|
|
||||||
All X-Pipe components target [JDK 17](https://openjdk.java.net/projects/jdk/17/) and make full use of the Java Module System (JPMS).
|
Any contribution is welcomed!
|
||||||
|
There are no real formal contribution guidelines right now, they will maybe come later.
|
||||||
|
|
||||||
|
### Modularity
|
||||||
|
|
||||||
|
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.
|
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).
|
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)
|
Further, note that as this is a pretty complicated Java project that fully utilizes modularity,
|
||||||
repository, which is shared between all components and integrated as a git submodule.
|
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.
|
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
|
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.
|
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.
|
||||||
|
|
||||||
|
|
|
@ -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
|
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`
|
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
|
## Usage
|
||||||
|
|
||||||
|
|
|
@ -5,13 +5,13 @@ plugins {
|
||||||
id "org.moditect.gradleplugin" version "1.0.0-rc3"
|
id "org.moditect.gradleplugin" version "1.0.0-rc3"
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$projectDir/../gradle_scripts/java.gradle"
|
apply from: "$rootDir/gradle/gradle_scripts/java.gradle"
|
||||||
apply from: "$projectDir/../gradle_scripts/junit.gradle"
|
apply from: "$rootDir/gradle/gradle_scripts/junit.gradle"
|
||||||
|
|
||||||
System.setProperty('excludeExtensionLibrary', 'true')
|
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'
|
group = 'io.xpipe'
|
||||||
archivesBaseName = 'xpipe-api'
|
archivesBaseName = 'xpipe-api'
|
||||||
|
|
||||||
|
@ -33,6 +33,11 @@ configurations {
|
||||||
testImplementation.extendsFrom(dep)
|
testImplementation.extendsFrom(dep)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
task dist(type: Copy) {
|
||||||
|
from jar.archiveFile
|
||||||
|
into "${project(':dist').buildDir}/dist/libraries"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
apply from: 'publish.gradle'
|
apply from: 'publish.gradle'
|
||||||
apply from: "$projectDir/../gradle_scripts/publish-base.gradle"
|
apply from: "$rootDir/gradle/gradle_scripts/publish-base.gradle"
|
|
@ -15,11 +15,11 @@ publishing {
|
||||||
pom {
|
pom {
|
||||||
name = 'X-Pipe Java API'
|
name = 'X-Pipe Java API'
|
||||||
description = 'Contains everything necessary to interact with X-Pipe from Java applications.'
|
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 {
|
licenses {
|
||||||
license {
|
license {
|
||||||
name = 'The MIT License (MIT)'
|
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 {
|
developers {
|
||||||
|
@ -30,9 +30,9 @@ publishing {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
scm {
|
scm {
|
||||||
connection = 'scm:git:git://github.com/xpipe-io/xpipe_java.git'
|
connection = 'scm:git:git://github.com/xpipe-io/xpipe.git'
|
||||||
developerConnection = 'scm:git:ssh://github.com/xpipe-io/xpipe_java.git'
|
developerConnection = 'scm:git:ssh://github.com/xpipe-io/xpipe.git'
|
||||||
url = 'https://github.com/xpipe-io/xpipe_java'
|
url = 'https://github.com/xpipe-io/xpipe'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
194
app/build.gradle
Normal file
194
app/build.gradle
Normal 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;
|
||||||
|
}
|
159
app/gradle_scripts/flexmark.gradle
Normal file
159
app/gradle_scripts/flexmark.gradle
Normal 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;
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
24
app/gradle_scripts/fxtrayicon.gradle
Normal file
24
app/gradle_scripts/fxtrayicon.gradle
Normal 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;
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
30
app/gradle_scripts/github-api.gradle
Normal file
30
app/gradle_scripts/github-api.gradle
Normal 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;
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
87
app/gradle_scripts/richtextfx.gradle
Normal file
87
app/gradle_scripts/richtextfx.gradle
Normal 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;
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
41
app/gradle_scripts/sentry.gradle
Normal file
41
app/gradle_scripts/sentry.gradle
Normal 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;
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
app/src/main/java/io/xpipe/app/Main.java
Normal file
17
app/src/main/java/io/xpipe/app/Main.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
100
app/src/main/java/io/xpipe/app/comp/AppLayoutComp.java
Normal file
100
app/src/main/java/io/xpipe/app/comp/AppLayoutComp.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
54
app/src/main/java/io/xpipe/app/comp/DeveloperTabComp.java
Normal file
54
app/src/main/java/io/xpipe/app/comp/DeveloperTabComp.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
74
app/src/main/java/io/xpipe/app/comp/PrefsComp.java
Normal file
74
app/src/main/java/io/xpipe/app/comp/PrefsComp.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
76
app/src/main/java/io/xpipe/app/comp/about/AboutTabComp.java
Normal file
76
app/src/main/java/io/xpipe/app/comp/about/AboutTabComp.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
156
app/src/main/java/io/xpipe/app/comp/about/UpdateCheckComp.java
Normal file
156
app/src/main/java/io/xpipe/app/comp/about/UpdateCheckComp.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
63
app/src/main/java/io/xpipe/app/comp/base/BigIconButton.java
Normal file
63
app/src/main/java/io/xpipe/app/comp/base/BigIconButton.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
69
app/src/main/java/io/xpipe/app/comp/base/ButtonComp.java
Normal file
69
app/src/main/java/io/xpipe/app/comp/base/ButtonComp.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
42
app/src/main/java/io/xpipe/app/comp/base/CountComp.java
Normal file
42
app/src/main/java/io/xpipe/app/comp/base/CountComp.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
114
app/src/main/java/io/xpipe/app/comp/base/LazyTextFieldComp.java
Normal file
114
app/src/main/java/io/xpipe/app/comp/base/LazyTextFieldComp.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
124
app/src/main/java/io/xpipe/app/comp/base/ListViewComp.java
Normal file
124
app/src/main/java/io/xpipe/app/comp/base/ListViewComp.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
91
app/src/main/java/io/xpipe/app/comp/base/MarkdownComp.java
Normal file
91
app/src/main/java/io/xpipe/app/comp/base/MarkdownComp.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
76
app/src/main/java/io/xpipe/app/comp/base/MessageComp.java
Normal file
76
app/src/main/java/io/xpipe/app/comp/base/MessageComp.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
303
app/src/main/java/io/xpipe/app/comp/base/MultiStepComp.java
Normal file
303
app/src/main/java/io/xpipe/app/comp/base/MultiStepComp.java
Normal 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) {}
|
||||||
|
}
|
|
@ -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) {}
|
||||||
|
}
|
43
app/src/main/java/io/xpipe/app/comp/base/TitledPaneComp.java
Normal file
43
app/src/main/java/io/xpipe/app/comp/base/TitledPaneComp.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
35
app/src/main/java/io/xpipe/app/comp/source/DsRawComp.java
Normal file
35
app/src/main/java/io/xpipe/app/comp/source/DsRawComp.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
93
app/src/main/java/io/xpipe/app/comp/source/DsTableComp.java
Normal file
93
app/src/main/java/io/xpipe/app/comp/source/DsTableComp.java
Normal 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) {}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
34
app/src/main/java/io/xpipe/app/comp/source/DsTextComp.java
Normal file
34
app/src/main/java/io/xpipe/app/comp/source/DsTextComp.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
279
app/src/main/java/io/xpipe/app/comp/source/GuiDsConfigStep.java
Normal file
279
app/src/main/java/io/xpipe/app/comp/source/GuiDsConfigStep.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
98
app/src/main/java/io/xpipe/app/core/App.java
Normal file
98
app/src/main/java/io/xpipe/app/core/App.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
68
app/src/main/java/io/xpipe/app/core/AppActionDetector.java
Normal file
68
app/src/main/java/io/xpipe/app/core/AppActionDetector.java
Normal 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
Loading…
Reference in a new issue