Initial project
This commit is contained in:
130
.gitignore
vendored
130
.gitignore
vendored
@ -1,98 +1,40 @@
|
|||||||
# ---> JetBrains
|
|
||||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
|
||||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
|
||||||
|
|
||||||
# User-specific stuff
|
|
||||||
.idea/**/workspace.xml
|
|
||||||
.idea/**/tasks.xml
|
|
||||||
.idea/**/usage.statistics.xml
|
|
||||||
.idea/**/dictionaries
|
|
||||||
.idea/**/shelf
|
|
||||||
|
|
||||||
# AWS User-specific
|
|
||||||
.idea/**/aws.xml
|
|
||||||
|
|
||||||
# Generated files
|
|
||||||
.idea/**/contentModel.xml
|
|
||||||
|
|
||||||
# Sensitive or high-churn files
|
|
||||||
.idea/**/dataSources/
|
|
||||||
.idea/**/dataSources.ids
|
|
||||||
.idea/**/dataSources.local.xml
|
|
||||||
.idea/**/sqlDataSources.xml
|
|
||||||
.idea/**/dynamic.xml
|
|
||||||
.idea/**/uiDesigner.xml
|
|
||||||
.idea/**/dbnavigator.xml
|
|
||||||
|
|
||||||
# Gradle
|
|
||||||
.idea/**/gradle.xml
|
|
||||||
.idea/**/libraries
|
|
||||||
|
|
||||||
# Gradle and Maven with auto-import
|
|
||||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
|
||||||
# since they will be recreated, and may cause churn. Uncomment if using
|
|
||||||
# auto-import.
|
|
||||||
# .idea/artifacts
|
|
||||||
# .idea/compiler.xml
|
|
||||||
# .idea/jarRepositories.xml
|
|
||||||
# .idea/modules.xml
|
|
||||||
# .idea/*.iml
|
|
||||||
# .idea/modules
|
|
||||||
# *.iml
|
|
||||||
# *.ipr
|
|
||||||
|
|
||||||
# CMake
|
|
||||||
cmake-build-*/
|
|
||||||
|
|
||||||
# Mongo Explorer plugin
|
|
||||||
.idea/**/mongoSettings.xml
|
|
||||||
|
|
||||||
# File-based project format
|
|
||||||
*.iws
|
|
||||||
|
|
||||||
# IntelliJ
|
|
||||||
out/
|
|
||||||
|
|
||||||
# mpeltonen/sbt-idea plugin
|
|
||||||
.idea_modules/
|
|
||||||
|
|
||||||
# JIRA plugin
|
|
||||||
atlassian-ide-plugin.xml
|
|
||||||
|
|
||||||
# Cursive Clojure plugin
|
|
||||||
.idea/replstate.xml
|
|
||||||
|
|
||||||
# SonarLint plugin
|
|
||||||
.idea/sonarlint/
|
|
||||||
|
|
||||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
|
||||||
com_crashlytics_export_strings.xml
|
|
||||||
crashlytics.properties
|
|
||||||
crashlytics-build.properties
|
|
||||||
fabric.properties
|
|
||||||
|
|
||||||
# Editor-based Rest Client
|
|
||||||
.idea/httpRequests
|
|
||||||
|
|
||||||
# Android studio 3.1+ serialized cache file
|
|
||||||
.idea/caches/build_file_checksums.ser
|
|
||||||
|
|
||||||
# ---> Maven
|
|
||||||
target/
|
target/
|
||||||
pom.xml.tag
|
!.mvn/wrapper/maven-wrapper.jar
|
||||||
pom.xml.releaseBackup
|
!**/src/main/**/target/
|
||||||
pom.xml.versionsBackup
|
!**/src/test/**/target/
|
||||||
pom.xml.next
|
|
||||||
release.properties
|
|
||||||
dependency-reduced-pom.xml
|
|
||||||
buildNumber.properties
|
|
||||||
.mvn/timing.properties
|
|
||||||
# https://github.com/takari/maven-wrapper#usage-without-binary-jar
|
|
||||||
.mvn/wrapper/maven-wrapper.jar
|
|
||||||
|
|
||||||
# Eclipse m2e generated files
|
### IntelliJ IDEA ###
|
||||||
# Eclipse Core
|
.idea/modules.xml
|
||||||
.project
|
.idea/jarRepositories.xml
|
||||||
# JDT-specific (Eclipse Java Development Tools)
|
.idea/compiler.xml
|
||||||
|
.idea/libraries/
|
||||||
|
*.iws
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
|
||||||
|
### Eclipse ###
|
||||||
|
.apt_generated
|
||||||
.classpath
|
.classpath
|
||||||
|
.factorypath
|
||||||
|
.project
|
||||||
|
.settings
|
||||||
|
.springBeans
|
||||||
|
.sts4-cache
|
||||||
|
|
||||||
|
### NetBeans ###
|
||||||
|
/nbproject/private/
|
||||||
|
/nbbuild/
|
||||||
|
/dist/
|
||||||
|
/nbdist/
|
||||||
|
/.nb-gradle/
|
||||||
|
build/
|
||||||
|
!**/src/main/**/build/
|
||||||
|
!**/src/test/**/build/
|
||||||
|
|
||||||
|
### VS Code ###
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
### Mac OS ###
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
/TimiFXExamples.yaml
|
||||||
|
|||||||
3
.idea/.gitignore
generated
vendored
Normal file
3
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
7
.idea/encodings.xml
generated
Normal file
7
.idea/encodings.xml
generated
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Encoding" defaultCharsetForPropertiesFiles="UTF-8">
|
||||||
|
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
|
||||||
|
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
14
.idea/misc.xml
generated
Normal file
14
.idea/misc.xml
generated
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
|
<component name="MavenProjectsManager">
|
||||||
|
<option name="originalFiles">
|
||||||
|
<list>
|
||||||
|
<option value="$PROJECT_DIR$/pom.xml" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK">
|
||||||
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
124
.idea/uiDesigner.xml
generated
Normal file
124
.idea/uiDesigner.xml
generated
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Palette2">
|
||||||
|
<group name="Swing">
|
||||||
|
<item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="1" hsize-policy="6" anchor="0" fill="1" />
|
||||||
|
</item>
|
||||||
|
<item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="6" hsize-policy="1" anchor="0" fill="2" />
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JPanel" icon="/com/intellij/uiDesigner/icons/panel.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3" />
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JScrollPane" icon="/com/intellij/uiDesigner/icons/scrollPane.svg" removable="false" auto-create-binding="false" can-attach-label="true">
|
||||||
|
<default-constraints vsize-policy="7" hsize-policy="7" anchor="0" fill="3" />
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JButton" icon="/com/intellij/uiDesigner/icons/button.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="3" anchor="0" fill="1" />
|
||||||
|
<initial-values>
|
||||||
|
<property name="text" value="Button" />
|
||||||
|
</initial-values>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JRadioButton" icon="/com/intellij/uiDesigner/icons/radioButton.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
|
||||||
|
<initial-values>
|
||||||
|
<property name="text" value="RadioButton" />
|
||||||
|
</initial-values>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JCheckBox" icon="/com/intellij/uiDesigner/icons/checkBox.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
|
||||||
|
<initial-values>
|
||||||
|
<property name="text" value="CheckBox" />
|
||||||
|
</initial-values>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JLabel" icon="/com/intellij/uiDesigner/icons/label.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="0" anchor="8" fill="0" />
|
||||||
|
<initial-values>
|
||||||
|
<property name="text" value="Label" />
|
||||||
|
</initial-values>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JTextField" icon="/com/intellij/uiDesigner/icons/textField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
|
||||||
|
<preferred-size width="150" height="-1" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JPasswordField" icon="/com/intellij/uiDesigner/icons/passwordField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
|
||||||
|
<preferred-size width="150" height="-1" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JFormattedTextField" icon="/com/intellij/uiDesigner/icons/formattedTextField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
|
||||||
|
<preferred-size width="150" height="-1" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JTextArea" icon="/com/intellij/uiDesigner/icons/textArea.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||||
|
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||||
|
<preferred-size width="150" height="50" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JTextPane" icon="/com/intellij/uiDesigner/icons/textPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||||
|
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||||
|
<preferred-size width="150" height="50" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JEditorPane" icon="/com/intellij/uiDesigner/icons/editorPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||||
|
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||||
|
<preferred-size width="150" height="50" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JComboBox" icon="/com/intellij/uiDesigner/icons/comboBox.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="2" anchor="8" fill="1" />
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JTable" icon="/com/intellij/uiDesigner/icons/table.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||||
|
<preferred-size width="150" height="50" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JList" icon="/com/intellij/uiDesigner/icons/list.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="6" hsize-policy="2" anchor="0" fill="3">
|
||||||
|
<preferred-size width="150" height="50" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JTree" icon="/com/intellij/uiDesigner/icons/tree.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||||
|
<preferred-size width="150" height="50" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JTabbedPane" icon="/com/intellij/uiDesigner/icons/tabbedPane.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
|
||||||
|
<preferred-size width="200" height="200" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JSplitPane" icon="/com/intellij/uiDesigner/icons/splitPane.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
|
||||||
|
<preferred-size width="200" height="200" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JSpinner" icon="/com/intellij/uiDesigner/icons/spinner.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JSlider" icon="/com/intellij/uiDesigner/icons/slider.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JSeparator" icon="/com/intellij/uiDesigner/icons/separator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3" />
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JProgressBar" icon="/com/intellij/uiDesigner/icons/progressbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1" />
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JToolBar" icon="/com/intellij/uiDesigner/icons/toolbar.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1">
|
||||||
|
<preferred-size width="-1" height="20" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JToolBar$Separator" icon="/com/intellij/uiDesigner/icons/toolbarSeparator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="0" anchor="0" fill="1" />
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JScrollBar" icon="/com/intellij/uiDesigner/icons/scrollbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="6" hsize-policy="0" anchor="0" fill="2" />
|
||||||
|
</item>
|
||||||
|
</group>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
66
pom.xml
Normal file
66
pom.xml
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>com.imyeyu.fx.ui</groupId>
|
||||||
|
<artifactId>timi-fx-ui</artifactId>
|
||||||
|
<version>0.0.1</version>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.test.skip>true</maven.test.skip>
|
||||||
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
|
<maven.compiler.target>21</maven.compiler.target>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.imyeyu.fx</groupId>
|
||||||
|
<artifactId>timi-fx</artifactId>
|
||||||
|
<version>0.0.1</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.imyeyu.fx.icon</groupId>
|
||||||
|
<artifactId>timi-fx-icon</artifactId>
|
||||||
|
<version>0.0.1</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.imyeyu.lang</groupId>
|
||||||
|
<artifactId>timi-lang</artifactId>
|
||||||
|
<version>0.0.1</version>
|
||||||
|
</dependency>
|
||||||
|
<!-- 演示程序依赖 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>1.18.36</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.imyeyu.network</groupId>
|
||||||
|
<artifactId>timi-network</artifactId>
|
||||||
|
<version>0.0.1</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.imyeyu.config</groupId>
|
||||||
|
<artifactId>timi-config</artifactId>
|
||||||
|
<version>0.0.1</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.code.gson</groupId>
|
||||||
|
<artifactId>gson</artifactId>
|
||||||
|
<version>2.11.0</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.imyeyu.inject</groupId>
|
||||||
|
<artifactId>timi-inject</artifactId>
|
||||||
|
<version>0.0.1</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
||||||
84
src/main/java/com/imyeyu/fx/ui/MinecraftFont.java
Normal file
84
src/main/java/com/imyeyu/fx/ui/MinecraftFont.java
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
package com.imyeyu.fx.ui;
|
||||||
|
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.text.Font;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minecraft 字体,此字体为点阵字体,不会模糊渲染,在 16,32,64,128 像素时最为清晰
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2021-04-14 00:11
|
||||||
|
*/
|
||||||
|
public class MinecraftFont implements TimiFXUI {
|
||||||
|
|
||||||
|
/** 小号,16 像素 */
|
||||||
|
public static int X16 = 16;
|
||||||
|
|
||||||
|
/** 中号,32 像素 */
|
||||||
|
public static int X32 = 32;
|
||||||
|
|
||||||
|
/** 大号,64 像素 */
|
||||||
|
public static int X64 = 64;
|
||||||
|
|
||||||
|
/** 特大号,128 像素 */
|
||||||
|
public static int X128 = 128;
|
||||||
|
|
||||||
|
private static Font F_X16, F_X32, F_X64, F_X128;
|
||||||
|
private static final String NAME = "MinecraftAE.ttf";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过 CSS 修改字号,请置于组件样式修改的最后
|
||||||
|
*
|
||||||
|
* @param node 组件
|
||||||
|
* @param size 字号,单位:像素
|
||||||
|
*/
|
||||||
|
public static void css(Node node, int size) {
|
||||||
|
node.setStyle(node.getStyle() + "; -fx-font-size: " + size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取小号字体 X16
|
||||||
|
*
|
||||||
|
* @return 小号字体
|
||||||
|
*/
|
||||||
|
public static Font X16() {
|
||||||
|
return F_X16 == null ? F_X16 = build(X16) : F_X16;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取中号字体 X32
|
||||||
|
*
|
||||||
|
* @return 中号字体
|
||||||
|
*/
|
||||||
|
public static Font X32() {
|
||||||
|
return F_X32 == null ? F_X32 = build(X32) : F_X32;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取大号字体 X64
|
||||||
|
*
|
||||||
|
* @return 大号字体
|
||||||
|
*/
|
||||||
|
public static Font X64() {
|
||||||
|
return F_X64 == null ? F_X64 = build(X64) : F_X64;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取特大号字体 X128
|
||||||
|
*
|
||||||
|
* @return 特大号字体
|
||||||
|
*/
|
||||||
|
public static Font X128() {
|
||||||
|
return F_X128 == null ? F_X128 = build(X128) : F_X128;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建字体,Minecraft 字体在 16 的公倍数时渲染最佳
|
||||||
|
*
|
||||||
|
* @param size 字号
|
||||||
|
* @return 字体
|
||||||
|
*/
|
||||||
|
public static Font build(int size) {
|
||||||
|
return Font.loadFont(MinecraftFont.class.getResourceAsStream(RESOURCE + NAME), size);
|
||||||
|
}
|
||||||
|
}
|
||||||
118
src/main/java/com/imyeyu/fx/ui/ScreenIdentify.java
Normal file
118
src/main/java/com/imyeyu/fx/ui/ScreenIdentify.java
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
package com.imyeyu.fx.ui;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.utils.ScreenFX;
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.beans.property.BooleanProperty;
|
||||||
|
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||||
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.collections.ListChangeListener;
|
||||||
|
import javafx.collections.ObservableList;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.geometry.Rectangle2D;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.stage.Popup;
|
||||||
|
import javafx.stage.Screen;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import javafx.stage.StageStyle;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author 夜雨
|
||||||
|
* @version 2024-04-13 22:40
|
||||||
|
*/
|
||||||
|
public class ScreenIdentify extends Stage implements TimiFXUI {
|
||||||
|
|
||||||
|
private final BooleanProperty showingIdentify;
|
||||||
|
private final ObservableList<Identify> showingIdentifyList;
|
||||||
|
|
||||||
|
public ScreenIdentify() {
|
||||||
|
|
||||||
|
showingIdentifyList = FXCollections.observableArrayList();
|
||||||
|
|
||||||
|
showingIdentify = new SimpleBooleanProperty(false);
|
||||||
|
showingIdentify.bind(Bindings.isEmpty(showingIdentifyList));
|
||||||
|
|
||||||
|
initStyle(StageStyle.UTILITY);
|
||||||
|
setOpacity(0);
|
||||||
|
setWidth(10);
|
||||||
|
setHeight(10);
|
||||||
|
setX(-20);
|
||||||
|
setY(-20);
|
||||||
|
|
||||||
|
// ---------- 事件 ----------
|
||||||
|
|
||||||
|
showingIdentifyList.addListener((ListChangeListener<Identify>) c -> {
|
||||||
|
while (c.next()) {
|
||||||
|
if (c.wasAdded()) {
|
||||||
|
List<? extends Identify> list = c.getAddedSubList();
|
||||||
|
for (int i = 0; i < list.size(); i++) {
|
||||||
|
Rectangle2D r2d = list.get(i).screen.getBounds();
|
||||||
|
list.get(i).show(this, r2d.getMinX() + 80, r2d.getMinY() + 80);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (c.wasRemoved()) {
|
||||||
|
List<? extends Identify> list = c.getRemoved();
|
||||||
|
for (int i = 0; i < list.size(); i++) {
|
||||||
|
list.get(i).hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 显示标识 */
|
||||||
|
public void showIdentify() {
|
||||||
|
show();
|
||||||
|
|
||||||
|
List<Screen> screens = ScreenFX.SCREENS;
|
||||||
|
for (int i = 0; i < screens.size(); i++) {
|
||||||
|
showingIdentifyList.add(new Identify(screens.get(i), i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 隐藏标识 */
|
||||||
|
public void hideIdentify() {
|
||||||
|
showingIdentifyList.clear();
|
||||||
|
hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return true 为正在显示标识 */
|
||||||
|
public boolean isShowingIdentify() {
|
||||||
|
return showingIdentify.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return 正在显示标识监听 */
|
||||||
|
public ReadOnlyBooleanProperty showingIdentify() {
|
||||||
|
return showingIdentify;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 屏幕标识
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-02-17 15:42
|
||||||
|
*/
|
||||||
|
private static class Identify extends Popup implements TimiFXUI, TimiFXUI.Colorful {
|
||||||
|
|
||||||
|
/** 所属屏幕 */
|
||||||
|
final Screen screen;
|
||||||
|
|
||||||
|
public Identify(Screen screen, int i) {
|
||||||
|
this.screen = screen;
|
||||||
|
|
||||||
|
Label text = new Label(String.valueOf(i));
|
||||||
|
text.setTextFill(WHITE);
|
||||||
|
text.setAlignment(Pos.CENTER);
|
||||||
|
text.prefHeightProperty().bind(text.widthProperty());
|
||||||
|
text.prefWidthProperty().bind(text.heightProperty());
|
||||||
|
MinecraftFont.css(text, 256);
|
||||||
|
|
||||||
|
getContent().setAll(text);
|
||||||
|
getScene().setFill(BLACK);
|
||||||
|
getScene().getStylesheets().addAll(CSS_STYLE, CSS_FONT);
|
||||||
|
sizeToScene();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
437
src/main/java/com/imyeyu/fx/ui/TimiFXUI.java
Normal file
437
src/main/java/com/imyeyu/fx/ui/TimiFXUI.java
Normal file
@ -0,0 +1,437 @@
|
|||||||
|
package com.imyeyu.fx.ui;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.utils.BgFill;
|
||||||
|
import com.imyeyu.fx.utils.BorderStroke;
|
||||||
|
import com.imyeyu.lang.multi.ResourcesMultilingual;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.SeparatorMenuItem;
|
||||||
|
import javafx.scene.control.TableColumn;
|
||||||
|
import javafx.scene.effect.DropShadow;
|
||||||
|
import javafx.scene.layout.Background;
|
||||||
|
import javafx.scene.layout.Border;
|
||||||
|
import javafx.scene.paint.Color;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author 夜雨
|
||||||
|
* @version 2024-04-13 14:18
|
||||||
|
*/
|
||||||
|
public interface TimiFXUI {
|
||||||
|
|
||||||
|
/** 静态资源路径 */
|
||||||
|
String RESOURCE = "/timifx/";
|
||||||
|
|
||||||
|
/** 样式文件 */
|
||||||
|
String CSS_STYLE = RESOURCE + "style.css";
|
||||||
|
|
||||||
|
/** 全局字体替换 */
|
||||||
|
String CSS_FONT = RESOURCE + "font.css";
|
||||||
|
|
||||||
|
ResourcesMultilingual MULTILINGUAL = new ResourcesMultilingual();
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2024-04-13 11:51
|
||||||
|
*/
|
||||||
|
interface Colorful {
|
||||||
|
|
||||||
|
/** 白色 */
|
||||||
|
Color WHITE = Color.valueOf("#FFF");
|
||||||
|
|
||||||
|
/** 品红 */
|
||||||
|
Color RED = Color.valueOf("#F30");
|
||||||
|
|
||||||
|
/** 褐色 */
|
||||||
|
Color BROWN = Color.valueOf("#A67D7B");
|
||||||
|
|
||||||
|
/** 黑色 */
|
||||||
|
Color BLACK = Color.valueOf("#000");
|
||||||
|
|
||||||
|
/** 橙色 */
|
||||||
|
Color ORANGE = Color.valueOf("#F60");
|
||||||
|
|
||||||
|
/** 黄色 */
|
||||||
|
Color YELLOW = Color.valueOf("#FF0");
|
||||||
|
|
||||||
|
/** 绿色 */
|
||||||
|
Color GREEN = Color.valueOf("#393");
|
||||||
|
|
||||||
|
/** 深绿 */
|
||||||
|
Color DARK_GREEN = Color.valueOf("#373");
|
||||||
|
|
||||||
|
/** 灰色 */
|
||||||
|
Color GRAY = Color.valueOf("#666");
|
||||||
|
|
||||||
|
/** 天蓝 */
|
||||||
|
Color BLUE = Color.valueOf("#008DCB");
|
||||||
|
|
||||||
|
/** 浅蓝 */
|
||||||
|
Color LIGHT_BLUE = Color.valueOf("#DDEAF0");
|
||||||
|
|
||||||
|
/** 灰白(程序默认底色 F4F4F4) */
|
||||||
|
Color GRAY_WHITE = Color.valueOf("#F4F4F4");
|
||||||
|
|
||||||
|
/** 亮灰 */
|
||||||
|
Color LIGHT_GRAY = Color.valueOf("#B5B5B5");
|
||||||
|
|
||||||
|
/** 深灰 */
|
||||||
|
Color DARK_GRAY = Color.valueOf("#333");
|
||||||
|
|
||||||
|
/** 少女粉 */
|
||||||
|
Color PINK = Color.valueOf("#FF7A9B");
|
||||||
|
|
||||||
|
/** 透明 */
|
||||||
|
Color TRANSPARENT = Color.TRANSPARENT;
|
||||||
|
|
||||||
|
// ---------- 聚焦颜色 ----------
|
||||||
|
|
||||||
|
/** 聚焦颜色 - 默认 */
|
||||||
|
Color FOCUSED_DEFAULT = Color.valueOf("#177CB0");
|
||||||
|
|
||||||
|
/** 聚焦颜色 - 亮 */
|
||||||
|
Color FOCUSED_LIGHT = Color.valueOf("#55B0DF");
|
||||||
|
|
||||||
|
/** 聚焦颜色 - 暗 */
|
||||||
|
Color FOCUSED_DARK = Color.valueOf("#0B6C9E");
|
||||||
|
|
||||||
|
// ---------- 图标 ----------
|
||||||
|
|
||||||
|
|
||||||
|
/** 图标颜色 */
|
||||||
|
Color ICON = DARK_GRAY;
|
||||||
|
|
||||||
|
/** 图标禁用颜色 */
|
||||||
|
Color ICON_DISABLED = Color.valueOf("#939393");
|
||||||
|
|
||||||
|
/** 图标指向颜色 */
|
||||||
|
Color ICON_HOVER = LIGHT_GRAY;
|
||||||
|
|
||||||
|
// ---------- 边框 ----------
|
||||||
|
|
||||||
|
/** 默认边框颜色 */
|
||||||
|
Color BORDER = LIGHT_GRAY;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author 夜雨
|
||||||
|
* @version 2024-04-13 11:53
|
||||||
|
*/
|
||||||
|
interface Stroke {
|
||||||
|
|
||||||
|
/** 透明边框 */
|
||||||
|
Border TP = new BorderStroke(Colorful.TRANSPARENT).build();
|
||||||
|
|
||||||
|
/** 默认边框 */
|
||||||
|
Border DEFAULT = new BorderStroke(Colorful.BORDER).build();
|
||||||
|
|
||||||
|
/** 禁用边框 */
|
||||||
|
Border DISABLE = new BorderStroke("#E1E1E1").build();
|
||||||
|
|
||||||
|
/** 聚焦边框 */
|
||||||
|
Border FOCUSED = new BorderStroke(Colorful.BORDER).build();
|
||||||
|
|
||||||
|
/** 上边框 */
|
||||||
|
Border TOP = new BorderStroke(Colorful.BORDER).top().build();
|
||||||
|
|
||||||
|
/** 左边框 */
|
||||||
|
Border LEFT = new BorderStroke(Colorful.BORDER).left().build();
|
||||||
|
|
||||||
|
/** 右边框 */
|
||||||
|
Border RIGHT = new BorderStroke(Colorful.BORDER).right().build();
|
||||||
|
|
||||||
|
/** 下边框 */
|
||||||
|
Border BOTTOM = new BorderStroke(Colorful.BORDER).bottom().build();
|
||||||
|
|
||||||
|
/** 除了上边框 */
|
||||||
|
Border EX_TOP = new BorderStroke(Colorful.BORDER).exTop().build();
|
||||||
|
|
||||||
|
/** 除了左边框 */
|
||||||
|
Border EX_LEFT = new BorderStroke(Colorful.BORDER).exLeft().build();
|
||||||
|
|
||||||
|
/** 除了右边框 */
|
||||||
|
Border EX_RIGHT = new BorderStroke(Colorful.BORDER).exRight().build();
|
||||||
|
|
||||||
|
/** 除了下边框 */
|
||||||
|
Border EX_BOTTOM = new BorderStroke(Colorful.BORDER).exBottom().build();
|
||||||
|
|
||||||
|
/** 上右边框 */
|
||||||
|
Border TR = new BorderStroke(Colorful.BORDER).width(1, 1, 0, 0).build();
|
||||||
|
|
||||||
|
/** 右下边框 */
|
||||||
|
Border RB = new BorderStroke(Colorful.BORDER).width(0, 1, 1, 0).build();
|
||||||
|
|
||||||
|
/** 左下边框 */
|
||||||
|
Border BL = new BorderStroke(Colorful.BORDER).width(0, 0, 1, 1).build();
|
||||||
|
|
||||||
|
/** 左上边框 */
|
||||||
|
Border LT = new BorderStroke(Colorful.BORDER).width(1, 0, 0, 1).build();
|
||||||
|
|
||||||
|
/** 水平边框,边的方向而非位置 */
|
||||||
|
Border H = new BorderStroke(Colorful.BORDER).width(1, 0).build();
|
||||||
|
|
||||||
|
/** 垂直边框,边的方向而非位置 */
|
||||||
|
Border V = new BorderStroke(Colorful.BORDER).width(0, 1).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author 夜雨
|
||||||
|
* @version 2024-04-13 11:53
|
||||||
|
*/
|
||||||
|
interface CSS {
|
||||||
|
|
||||||
|
/** CSS Minecraft AE 字体 */
|
||||||
|
String MINECRAFT = "minecraft-ae";
|
||||||
|
|
||||||
|
// ---------- CSS 边框 ----------
|
||||||
|
|
||||||
|
/** CSS 所有边框 */
|
||||||
|
String BORDER_ALL = "border-all";
|
||||||
|
|
||||||
|
/** CSS 无边框 */
|
||||||
|
String BORDER_N = "border-n";
|
||||||
|
|
||||||
|
/** CSS 上边框 */
|
||||||
|
String BORDER_T = "border-t";
|
||||||
|
|
||||||
|
/** CSS 右边框 */
|
||||||
|
String BORDER_R = "border-r";
|
||||||
|
|
||||||
|
/** CSS 下边框 */
|
||||||
|
String BORDER_B = "border-b";
|
||||||
|
|
||||||
|
/** CSS 左边框 */
|
||||||
|
String BORDER_L = "border-l";
|
||||||
|
|
||||||
|
/** CSS 上右边框 */
|
||||||
|
String BORDER_TR = "border-tr";
|
||||||
|
|
||||||
|
/** CSS 右下边框 */
|
||||||
|
String BORDER_RB = "border-rb";
|
||||||
|
|
||||||
|
/** CSS 左下边框 */
|
||||||
|
String BORDER_BL = "border-bl";
|
||||||
|
|
||||||
|
/** CSS 左上边框 */
|
||||||
|
String BORDER_LT = "border-lt";
|
||||||
|
|
||||||
|
/** CSS 上下右边框 */
|
||||||
|
String BORDER_TRB = "border-trb";
|
||||||
|
|
||||||
|
/** CSS 上下左边框 */
|
||||||
|
String BORDER_BLT = "border-blt";
|
||||||
|
|
||||||
|
/** CSS 左右下边框 */
|
||||||
|
String BORDER_RBL = "border-rbl";
|
||||||
|
|
||||||
|
/** CSS 左右上边框 */
|
||||||
|
String BORDER_LTR = "border-ltr";
|
||||||
|
|
||||||
|
/** CSS 上下边框 */
|
||||||
|
String BORDER_TB = "border-tb";
|
||||||
|
|
||||||
|
/** CSS 左右边框 */
|
||||||
|
String BORDER_LR = "border-lr";
|
||||||
|
|
||||||
|
// ---------- CSS 内边距 ----------
|
||||||
|
|
||||||
|
/** CSS 无内边距 */
|
||||||
|
String PADDING_N = "padding-n";
|
||||||
|
|
||||||
|
// ---------- CSS 背景 ----------
|
||||||
|
|
||||||
|
/** CSS 透明背景 */
|
||||||
|
String BG_TP = "bg-tp";
|
||||||
|
|
||||||
|
/** CSS 纯白背景 */
|
||||||
|
String BG_WHITE = "bg-white";
|
||||||
|
|
||||||
|
/** CSS 纯黑背景 */
|
||||||
|
String BG_BLACK = "bg-black";
|
||||||
|
|
||||||
|
/** CSS 默认背景 */
|
||||||
|
String BG_DEFAULT = "bg-default";
|
||||||
|
|
||||||
|
/** CSS 按钮背景 */
|
||||||
|
String BG_BUTTON = "bg-button";
|
||||||
|
|
||||||
|
/** CSS 按钮背景(只有背景,没有事件样式) */
|
||||||
|
String BG_BUTTON_STATIC = "bg-button-static";
|
||||||
|
|
||||||
|
// ---------- CSS 其他 ----------
|
||||||
|
|
||||||
|
/** CSS 光标指向透明度 */
|
||||||
|
String HOVER_OPACITY = "hover-opacity";
|
||||||
|
|
||||||
|
/** CSS 滚动面板的滚动条左侧边框 */
|
||||||
|
String SP_BORDER = "sp-border";
|
||||||
|
|
||||||
|
/** CSS 可编辑表格 */
|
||||||
|
String EDITABLE_TABLE = "editable-table";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author 夜雨
|
||||||
|
* @version 2024-04-13 11:53
|
||||||
|
*/
|
||||||
|
interface BG {
|
||||||
|
|
||||||
|
/** FX 默认背景(#F4F4F4) */
|
||||||
|
Background DEFAULT = new BgFill(Colorful.GRAY_WHITE).build();
|
||||||
|
|
||||||
|
/** 灰色背景 */
|
||||||
|
Background GRAY = new BgFill(Colorful.GRAY).build();
|
||||||
|
|
||||||
|
/** 亮灰背景 */
|
||||||
|
Background LIGHT_GRAY = new BgFill(Colorful.LIGHT_GRAY).build();
|
||||||
|
|
||||||
|
/** 纯黑背景 */
|
||||||
|
Background BLACK = new BgFill(Colorful.BLACK).build();
|
||||||
|
|
||||||
|
/** 纯白背景 */
|
||||||
|
Background WHITE = new BgFill(Colorful.WHITE).build();
|
||||||
|
|
||||||
|
/** 透明背景 */
|
||||||
|
Background TRANSPARENT = new BgFill(Colorful.TRANSPARENT).build();
|
||||||
|
|
||||||
|
/** 淡蓝背景 */
|
||||||
|
Background LIGHT_BLUE = new BgFill(Colorful.LIGHT_BLUE).build();
|
||||||
|
|
||||||
|
/** 聚焦色背景 */
|
||||||
|
Background FOCUSED = new BgFill(Colorful.FOCUSED_DEFAULT).build();
|
||||||
|
|
||||||
|
/** 指向背景,通常用于提示组件尺寸响应拖动 */
|
||||||
|
Background HOVER = new BgFill("#0007").build();
|
||||||
|
|
||||||
|
/** 渐变的标题背景 */
|
||||||
|
Background TITLE = new BgFill("#DDD", "#F4F4F400").toRight().build();
|
||||||
|
|
||||||
|
/** 填充的标题背景 */
|
||||||
|
Background TITLE_FILL = new BgFill("#DDD").build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author 夜雨
|
||||||
|
* @version 2024-04-13 11:55
|
||||||
|
*/
|
||||||
|
interface Shadow {
|
||||||
|
|
||||||
|
Insets PADDING = new Insets(8);
|
||||||
|
|
||||||
|
/** 窗体投影 */
|
||||||
|
DropShadow POPUP = new DropShadow() {{
|
||||||
|
setRadius(8);
|
||||||
|
setOffsetX(0);
|
||||||
|
setOffsetY(0);
|
||||||
|
setSpread(.05);
|
||||||
|
setColor(Color.valueOf("#3337"));
|
||||||
|
}};
|
||||||
|
|
||||||
|
/** 图片投影 */
|
||||||
|
DropShadow IMAGE = new DropShadow() {{
|
||||||
|
setRadius(6);
|
||||||
|
setOffsetX(0);
|
||||||
|
setOffsetY(0);
|
||||||
|
setSpread(.05);
|
||||||
|
setColor(Color.valueOf("#3336"));
|
||||||
|
}};
|
||||||
|
|
||||||
|
/** 下边投影 */
|
||||||
|
DropShadow DOWN = new DropShadow() {{
|
||||||
|
setRadius(6);
|
||||||
|
setOffsetX(0);
|
||||||
|
setOffsetY(2);
|
||||||
|
setSpread(.05);
|
||||||
|
setColor(Color.valueOf("#3334"));
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快速构建菜单分割线
|
||||||
|
*
|
||||||
|
* @return 菜单分割线
|
||||||
|
*/
|
||||||
|
static SeparatorMenuItem sep() {
|
||||||
|
return new SeparatorMenuItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建通用灰色标签
|
||||||
|
*
|
||||||
|
* @return 灰色标签
|
||||||
|
*/
|
||||||
|
static Label label() {
|
||||||
|
return label("");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建通用灰色标签
|
||||||
|
*
|
||||||
|
* @param text 标签文本
|
||||||
|
* @return 灰色标签
|
||||||
|
*/
|
||||||
|
static Label label(String text) {
|
||||||
|
Label label = new Label(text);
|
||||||
|
label.setTextFill(Colorful.GRAY);
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建通用标题标签
|
||||||
|
*
|
||||||
|
* @param text 标题文本
|
||||||
|
* @return 标签
|
||||||
|
*/
|
||||||
|
static Label title(String text) {
|
||||||
|
return title(text, Border.EMPTY, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建通用标题标签
|
||||||
|
*
|
||||||
|
* @param text 标题文本
|
||||||
|
* @param border 边框
|
||||||
|
* @return 标签
|
||||||
|
*/
|
||||||
|
static Label title(String text, Border border) {
|
||||||
|
return title(text, border, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建通用标题标签
|
||||||
|
*
|
||||||
|
* @param text 标题文本
|
||||||
|
* @param border 边框
|
||||||
|
* @param icon 图标
|
||||||
|
* @return 标签
|
||||||
|
*/
|
||||||
|
static Label title(String text, Border border, Node icon) {
|
||||||
|
Label label = new Label(text, icon);
|
||||||
|
label.setBorder(border);
|
||||||
|
label.setPadding(new Insets(4, 6, 4, 6));
|
||||||
|
label.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
label.setBackground(BG.TITLE);
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建空的表格列,通常用于触发事件
|
||||||
|
*
|
||||||
|
* @param width 预设宽度
|
||||||
|
* @param sClass 表格数据类对象
|
||||||
|
* @param tClass 列数据类对象
|
||||||
|
* @param <S> 表格数据类
|
||||||
|
* @param <T> 列数据类
|
||||||
|
* @return 列对象
|
||||||
|
*/
|
||||||
|
static <S, T> TableColumn<S, T> emptyTableColumn(double width, Class<S> sClass, Class<T> tClass) {
|
||||||
|
TableColumn<S, T> col = new TableColumn<>();
|
||||||
|
col.setSortable(false);
|
||||||
|
col.setResizable(false);
|
||||||
|
col.setReorderable(false);
|
||||||
|
col.setPrefWidth(width);
|
||||||
|
return col;
|
||||||
|
}
|
||||||
|
}
|
||||||
153
src/main/java/com/imyeyu/fx/ui/components/CheckBoxPicker.java
Normal file
153
src/main/java/com/imyeyu/fx/ui/components/CheckBoxPicker.java
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
package com.imyeyu.fx.ui.components;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import com.imyeyu.java.bean.CallbackArgReturn;
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.collections.ListChangeListener;
|
||||||
|
import javafx.collections.ObservableList;
|
||||||
|
import javafx.geometry.Bounds;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.control.CheckBox;
|
||||||
|
import javafx.scene.control.TextField;
|
||||||
|
import javafx.scene.layout.TilePane;
|
||||||
|
import javafx.stage.Popup;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 多选选择器,文本框弹出复选框组件进行多项选择
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2021-12-29 17:05
|
||||||
|
*/
|
||||||
|
public class CheckBoxPicker<T> extends TextField implements TimiFXUI {
|
||||||
|
|
||||||
|
private final ObservableList<T> items;
|
||||||
|
private final CheckBoxListPopup<T> checkBoxListPopup;
|
||||||
|
|
||||||
|
/** 默认构造器 */
|
||||||
|
public CheckBoxPicker() {
|
||||||
|
items = FXCollections.observableList(new ArrayList<>());
|
||||||
|
|
||||||
|
checkBoxListPopup = new CheckBoxListPopup<>(items);
|
||||||
|
|
||||||
|
setEditable(false);
|
||||||
|
|
||||||
|
// ---------- 事件 ----------
|
||||||
|
|
||||||
|
setOnMouseClicked(e -> {
|
||||||
|
Bounds b = localToScreen(getLayoutBounds());
|
||||||
|
checkBoxListPopup.show(this, b.getMinX() - 5, b.getMaxY() - 6);
|
||||||
|
});
|
||||||
|
|
||||||
|
checkBoxListPopup.root.prefWidthProperty().bind(widthProperty());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置复选框工厂,工厂入参数据对象,返回复选框组件
|
||||||
|
*
|
||||||
|
* @param factory 复选框工厂
|
||||||
|
*/
|
||||||
|
public void setCheckBoxFactory(CallbackArgReturn<T, CheckBox> factory) {
|
||||||
|
checkBoxListPopup.checkBoxFactory = factory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取已选项
|
||||||
|
*
|
||||||
|
* @return 已选项
|
||||||
|
*/
|
||||||
|
public ObservableList<T> getSelectedItems() {
|
||||||
|
return checkBoxListPopup.selectedItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取数据列表
|
||||||
|
*
|
||||||
|
* @return 数据列表
|
||||||
|
*/
|
||||||
|
public ObservableList<T> getItems() {
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 复选框列表弹窗
|
||||||
|
*
|
||||||
|
* @param <T> 数据类型
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2021-12-29 19:40
|
||||||
|
*/
|
||||||
|
private static class CheckBoxListPopup<T> extends Popup implements TimiFXUI {
|
||||||
|
|
||||||
|
private static final Insets PADDING = new Insets(6, 8, 6, 8);
|
||||||
|
|
||||||
|
private final TilePane root;
|
||||||
|
private final Map<T, CheckBox> cache; // 数据映射缓存
|
||||||
|
|
||||||
|
final ObservableList<T> selectedItems;
|
||||||
|
CallbackArgReturn<T, CheckBox> checkBoxFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认构造
|
||||||
|
*
|
||||||
|
* @param items 数据列表
|
||||||
|
*/
|
||||||
|
public CheckBoxListPopup(ObservableList<T> items) {
|
||||||
|
cache = new HashMap<>();
|
||||||
|
selectedItems = FXCollections.observableList(new ArrayList<>());
|
||||||
|
|
||||||
|
root = new TilePane();
|
||||||
|
root.setHgap(8);
|
||||||
|
root.setEffect(Shadow.POPUP);
|
||||||
|
root.setBorder(Stroke.DEFAULT);
|
||||||
|
root.setPadding(PADDING);
|
||||||
|
root.setMinWidth(300);
|
||||||
|
root.setBackground(BG.DEFAULT);
|
||||||
|
root.setTileAlignment(Pos.CENTER_LEFT);
|
||||||
|
|
||||||
|
setAutoHide(true);
|
||||||
|
getContent().add(root);
|
||||||
|
|
||||||
|
// ---------- 事件 ----------
|
||||||
|
|
||||||
|
// 列表更新
|
||||||
|
items.addListener((ListChangeListener<T>) c -> {
|
||||||
|
if (c.next()) {
|
||||||
|
if (c.wasAdded()) {
|
||||||
|
// 添加
|
||||||
|
List<? extends T> list = c.getAddedSubList();
|
||||||
|
for (T t : list) {
|
||||||
|
CheckBox box;
|
||||||
|
if (checkBoxFactory != null) {
|
||||||
|
box = checkBoxFactory.handler(t);
|
||||||
|
} else {
|
||||||
|
box = new CheckBox(t.toString());
|
||||||
|
}
|
||||||
|
box.selectedProperty().addListener((obs, o, isSelected) -> {
|
||||||
|
if (isSelected) {
|
||||||
|
selectedItems.add(t);
|
||||||
|
} else {
|
||||||
|
selectedItems.remove(t);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cache.put(t, box);
|
||||||
|
root.getChildren().add(box);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (c.wasRemoved()) {
|
||||||
|
// 移除
|
||||||
|
List<? extends T> list = c.getRemoved();
|
||||||
|
for (int i = 0; i < list.size(); i++) {
|
||||||
|
root.getChildren().remove(cache.get(list.get(i)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
63
src/main/java/com/imyeyu/fx/ui/components/ContextMenu.java
Normal file
63
src/main/java/com/imyeyu/fx/ui/components/ContextMenu.java
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
package com.imyeyu.fx.ui.components;
|
||||||
|
|
||||||
|
import javafx.collections.ListChangeListener;
|
||||||
|
import javafx.collections.ObservableList;
|
||||||
|
import javafx.scene.control.Menu;
|
||||||
|
import javafx.scene.control.MenuItem;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支持最小尺寸的菜单,默认 90
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2023-03-10 11:14
|
||||||
|
*/
|
||||||
|
public class ContextMenu extends javafx.scene.control.ContextMenu {
|
||||||
|
|
||||||
|
/** 当菜单项 {@link Menu#getProperties()} 携带此标记时,该菜单不继承最小宽度属性 */
|
||||||
|
public static final String NOT_EXTENDS_FLAG = "NOT_EXTENDS_FLAG";
|
||||||
|
|
||||||
|
private static final String STYLE_TEMPLATE = "-fx-min-width: %s; -fx-pref-width: %s";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标准构造
|
||||||
|
*
|
||||||
|
* @param items 菜单项
|
||||||
|
*/
|
||||||
|
public ContextMenu(MenuItem... items) {
|
||||||
|
super(items);
|
||||||
|
|
||||||
|
getItems().addListener((ListChangeListener<MenuItem>) c -> {
|
||||||
|
while (c.next()) {
|
||||||
|
if (c.wasAdded()) {
|
||||||
|
updateMinWidth(getItems());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
minWidthProperty().addListener((obs, o, n) -> updateMinWidth(getItems()));
|
||||||
|
setMinWidth(90);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateMinWidth(List<MenuItem> items) {
|
||||||
|
for (int i = 0; i < items.size(); i++) {
|
||||||
|
if (items.get(i) instanceof Menu menu) {
|
||||||
|
if (!menu.getProperties().containsKey(NOT_EXTENDS_FLAG)) {
|
||||||
|
boolean isItemsMenu = false; // 为 true 时表示子菜单是一般菜单项,继续应用最小宽度
|
||||||
|
ObservableList<MenuItem> subItems = menu.getItems();
|
||||||
|
for (int j = 0; j < subItems.size(); j++) {
|
||||||
|
if (subItems.get(j).getClass().equals(MenuItem.class)) {
|
||||||
|
isItemsMenu = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isItemsMenu) {
|
||||||
|
updateMinWidth(menu.getItems());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
items.get(i).setStyle(STYLE_TEMPLATE.formatted(getMinWidth(), getMinWidth()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
282
src/main/java/com/imyeyu/fx/ui/components/DateTimePicker.java
Normal file
282
src/main/java/com/imyeyu/fx/ui/components/DateTimePicker.java
Normal file
@ -0,0 +1,282 @@
|
|||||||
|
package com.imyeyu.fx.ui.components;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.TimiFX;
|
||||||
|
import com.imyeyu.fx.icon.TimiFXIcon;
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import com.imyeyu.java.bean.Callback;
|
||||||
|
import com.imyeyu.utils.Time;
|
||||||
|
import com.sun.javafx.scene.control.DatePickerContent;
|
||||||
|
import javafx.beans.property.LongProperty;
|
||||||
|
import javafx.beans.property.SimpleLongProperty;
|
||||||
|
import javafx.event.EventHandler;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.Cursor;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.DatePicker;
|
||||||
|
import javafx.scene.control.ListView;
|
||||||
|
import javafx.scene.control.Skin;
|
||||||
|
import javafx.scene.control.skin.DatePickerSkin;
|
||||||
|
import javafx.scene.input.ScrollEvent;
|
||||||
|
import javafx.scene.layout.BorderPane;
|
||||||
|
import javafx.scene.layout.GridPane;
|
||||||
|
import javafx.scene.layout.Region;
|
||||||
|
import javafx.util.StringConverter;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 详细时间选择器
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2021-12-27 01:02
|
||||||
|
*/
|
||||||
|
public class DateTimePicker extends Region implements TimiFXUI, TimiFXUI.Colorful {
|
||||||
|
|
||||||
|
private static final String STYLE_CLASS = "date-time-picker";
|
||||||
|
|
||||||
|
/** 选择器 */
|
||||||
|
private final DatePicker datePicker;
|
||||||
|
|
||||||
|
/** 当前值 */
|
||||||
|
private final LongProperty value;
|
||||||
|
|
||||||
|
private final BorderPane timePane;
|
||||||
|
private final ListView<String> hour, minute, second;
|
||||||
|
|
||||||
|
/** 默认构造器 */
|
||||||
|
public DateTimePicker() {
|
||||||
|
value = new SimpleLongProperty(-1);
|
||||||
|
|
||||||
|
// 时间选择
|
||||||
|
hour = new ListView<>();
|
||||||
|
minute = new ListView<>();
|
||||||
|
second = new ListView<>();
|
||||||
|
|
||||||
|
hour.getStyleClass().add(CSS.BORDER_RB);
|
||||||
|
minute.getStyleClass().add(CSS.BORDER_RB);
|
||||||
|
second.getStyleClass().add(CSS.BORDER_B);
|
||||||
|
|
||||||
|
GridPane hmsPane = new GridPane();
|
||||||
|
hmsPane.addRow(0, hour, minute, second);
|
||||||
|
|
||||||
|
Button now = new Button(TimiFXUI.MULTILINGUAL.text("now_tick"));
|
||||||
|
now.getStyleClass().add(CSS.BORDER_L);
|
||||||
|
|
||||||
|
timePane = new BorderPane(hmsPane);
|
||||||
|
timePane.setBorder(Stroke.EX_LEFT);
|
||||||
|
timePane.setEffect(Shadow.IMAGE);
|
||||||
|
timePane.setPrefWidth(140);
|
||||||
|
timePane.setBackground(BG.DEFAULT);
|
||||||
|
timePane.setTranslateY(-1);
|
||||||
|
timePane.setBottom(now);
|
||||||
|
BorderPane.setAlignment(now, Pos.CENTER_RIGHT);
|
||||||
|
|
||||||
|
// 日期组件降权
|
||||||
|
datePicker = new DatePicker() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Skin<?> createDefaultSkin() {
|
||||||
|
DatePickerSkin skin = new DatePickerSkin(this) {
|
||||||
|
|
||||||
|
private BorderPane root;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Node getPopupContent() {
|
||||||
|
Node popupContent = super.getPopupContent();
|
||||||
|
if (popupContent instanceof DatePickerContent content) {
|
||||||
|
if (root == null) {
|
||||||
|
// 插入时间选择
|
||||||
|
timePane.prefHeightProperty().bind(content.heightProperty());
|
||||||
|
|
||||||
|
root = new BorderPane();
|
||||||
|
root.setCenter(popupContent);
|
||||||
|
root.setRight(timePane);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void hide() {
|
||||||
|
// 失焦关闭而非选择后关闭
|
||||||
|
}
|
||||||
|
};
|
||||||
|
IconButton clear = new IconButton(TimiFXIcon.fromName("FAIL", GRAY));
|
||||||
|
clear.getStyleClass().add(CSS.BORDER_ALL);
|
||||||
|
clear.translateXProperty().bind(widthProperty().subtract(40));
|
||||||
|
clear.setTranslateY(15);
|
||||||
|
clear.visibleProperty().bind(valueProperty().isNotNull());
|
||||||
|
clear.setOnAction(e -> clear());
|
||||||
|
skin.getChildren().add(clear);
|
||||||
|
return skin;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
datePicker.setEditable(false);
|
||||||
|
datePicker.prefWidthProperty().bind(widthProperty());
|
||||||
|
datePicker.prefHeightProperty().bind(heightProperty());
|
||||||
|
datePicker.getEditor().setCursor(Cursor.DEFAULT);
|
||||||
|
datePicker.setConverter(new StringConverter<>() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString(LocalDate object) {
|
||||||
|
Long unixTime = value.getValue();
|
||||||
|
if (object == null || unixTime == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return Time.toDateTime(unixTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LocalDate fromString(String string) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
datePicker.setOnShown(e -> {
|
||||||
|
hour.scrollTo(hour.getSelectionModel().getSelectedIndex() - 3);
|
||||||
|
minute.scrollTo(minute.getSelectionModel().getSelectedIndex() - 3);
|
||||||
|
second.scrollTo(second.getSelectionModel().getSelectedIndex() - 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
getStyleClass().add(STYLE_CLASS);
|
||||||
|
setPrefWidth(200);
|
||||||
|
getChildren().add(datePicker);
|
||||||
|
|
||||||
|
// ---------- 事件 ----------
|
||||||
|
|
||||||
|
// 时间选择数据
|
||||||
|
for (int i = 0; i < 24; i++) {
|
||||||
|
hour.getItems().add(String.format("%02d", i));
|
||||||
|
}
|
||||||
|
for (int i = 0; i < 60; i++) {
|
||||||
|
minute.getItems().add(String.format("%02d", i));
|
||||||
|
}
|
||||||
|
second.getItems().addAll(minute.getItems());
|
||||||
|
|
||||||
|
// 时间滚动居中
|
||||||
|
hour.getSelectionModel().select(0);
|
||||||
|
minute.getSelectionModel().select(0);
|
||||||
|
second.getSelectionModel().select(0);
|
||||||
|
EventHandler<ScrollEvent> middleScroll = e -> {
|
||||||
|
if (e.getSource() instanceof ListView<?> list) {
|
||||||
|
if (e.getDeltaY() < 0) {
|
||||||
|
if (list.getSelectionModel().getSelectedIndex() < list.getItems().size() - 1) {
|
||||||
|
list.getSelectionModel().selectNext();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (0 < list.getSelectionModel().getSelectedIndex()) {
|
||||||
|
list.getSelectionModel().selectPrevious();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
list.scrollTo(list.getSelectionModel().getSelectedIndex() - 3);
|
||||||
|
e.consume();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
hour.addEventFilter(ScrollEvent.SCROLL, middleScroll);
|
||||||
|
minute.addEventFilter(ScrollEvent.SCROLL, middleScroll);
|
||||||
|
second.addEventFilter(ScrollEvent.SCROLL, middleScroll);
|
||||||
|
|
||||||
|
TimiFX.hoverFocus(hour);
|
||||||
|
TimiFX.hoverFocus(minute);
|
||||||
|
TimiFX.hoverFocus(second);
|
||||||
|
|
||||||
|
// 此刻
|
||||||
|
now.setOnAction(e -> setValue(Time.now()));
|
||||||
|
|
||||||
|
// ---------- 解析 ----------
|
||||||
|
|
||||||
|
Callback parseDate = () -> {
|
||||||
|
LocalDate date = datePicker.getValue();
|
||||||
|
if (date == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int h = hour.getSelectionModel().getSelectedIndex();
|
||||||
|
int m = minute.getSelectionModel().getSelectedIndex();
|
||||||
|
int s = second.getSelectionModel().getSelectedIndex();
|
||||||
|
|
||||||
|
value.set(Time.fromLocalDateTime(date.atTime(LocalTime.of(h, m, s))));
|
||||||
|
|
||||||
|
datePicker.getEditor().setText(Time.toDateTime(value.get()));
|
||||||
|
};
|
||||||
|
datePicker.valueProperty().addListener((obs, o, newDate) -> parseDate.handler());
|
||||||
|
|
||||||
|
Callback parseTime = () -> {
|
||||||
|
LocalDate date = datePicker.getValue();
|
||||||
|
if (date == null) {
|
||||||
|
datePicker.setValue(LocalDate.now());
|
||||||
|
}
|
||||||
|
parseDate.handler();
|
||||||
|
};
|
||||||
|
hour.getSelectionModel().selectedIndexProperty().addListener((obs, o, n) -> parseTime.handler());
|
||||||
|
minute.getSelectionModel().selectedIndexProperty().addListener((obs, o, n) -> parseTime.handler());
|
||||||
|
second.getSelectionModel().selectedIndexProperty().addListener((obs, o, n) -> parseTime.handler());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 清除值 */
|
||||||
|
public void clear() {
|
||||||
|
hour.getSelectionModel().select(0);
|
||||||
|
minute.getSelectionModel().select(0);
|
||||||
|
second.getSelectionModel().select(0);
|
||||||
|
hour.scrollTo(0);
|
||||||
|
minute.scrollTo(0);
|
||||||
|
second.scrollTo(0);
|
||||||
|
|
||||||
|
datePicker.setValue(null);
|
||||||
|
value.setValue(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置选择时间戳
|
||||||
|
*
|
||||||
|
* @param unixTime 选择时间戳
|
||||||
|
*/
|
||||||
|
public void setValue(Long unixTime) {
|
||||||
|
if (unixTime == null || unixTime < 0) {
|
||||||
|
clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.value.set(unixTime);
|
||||||
|
|
||||||
|
LocalDate date = Time.toLocalDateTime(unixTime).toLocalDate();
|
||||||
|
datePicker.setValue(date);
|
||||||
|
|
||||||
|
int s = (int) ((unixTime - Time.fromLocalDate(date)) / 1000);
|
||||||
|
int h = s / 60 / 60;
|
||||||
|
hour.getSelectionModel().select(h);
|
||||||
|
minute.getSelectionModel().select(s / 60 - h * 60);
|
||||||
|
second.getSelectionModel().select(s % 60);
|
||||||
|
|
||||||
|
hour.scrollTo(hour.getSelectionModel().getSelectedIndex() - 3);
|
||||||
|
minute.scrollTo(minute.getSelectionModel().getSelectedIndex() - 3);
|
||||||
|
second.scrollTo(second.getSelectionModel().getSelectedIndex() - 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取选择时间戳
|
||||||
|
*
|
||||||
|
* @return 选择时间戳
|
||||||
|
*/
|
||||||
|
public Long getValue() {
|
||||||
|
return -1 < value.getValue() ? value.getValue() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取选择时间戳监听
|
||||||
|
*
|
||||||
|
* @return 选择时间戳监听
|
||||||
|
*/
|
||||||
|
public LongProperty valueProperty() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取日期选择器
|
||||||
|
*
|
||||||
|
* @return 日期选择器
|
||||||
|
*/
|
||||||
|
public DatePicker getDatePicker() {
|
||||||
|
return datePicker;
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/main/java/com/imyeyu/fx/ui/components/EnumListCell.java
Normal file
32
src/main/java/com/imyeyu/fx/ui/components/EnumListCell.java
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package com.imyeyu.fx.ui.components;
|
||||||
|
|
||||||
|
import com.imyeyu.java.ref.Ref;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.control.ListCell;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用枚举列表项,默认反射 name 字段
|
||||||
|
*
|
||||||
|
* @param <T> 枚举类型,必须含有 name 字段
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2023-06-08 17:01
|
||||||
|
*/
|
||||||
|
public class EnumListCell<T extends Enum<?>> extends ListCell<T> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void updateItem(T item, boolean empty) {
|
||||||
|
super.updateItem(item, empty);
|
||||||
|
if (empty || item == null) {
|
||||||
|
setText("");
|
||||||
|
setGraphic(null);
|
||||||
|
} else {
|
||||||
|
setAlignment(Pos.CENTER);
|
||||||
|
try {
|
||||||
|
setText(Ref.getFieldValue(item, "name", String.class));
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
473
src/main/java/com/imyeyu/fx/ui/components/FileTreeView.java
Normal file
473
src/main/java/com/imyeyu/fx/ui/components/FileTreeView.java
Normal file
@ -0,0 +1,473 @@
|
|||||||
|
package com.imyeyu.fx.ui.components;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.ObservableUtils;
|
||||||
|
import com.imyeyu.fx.icon.TimiFXIcon;
|
||||||
|
import com.imyeyu.fx.task.RunAsync;
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import com.imyeyu.fx.ui.components.alert.AlertButton;
|
||||||
|
import com.imyeyu.fx.ui.components.alert.AlertConfirm;
|
||||||
|
import com.imyeyu.fx.ui.components.alert.AlertTextField;
|
||||||
|
import com.imyeyu.fx.ui.components.alert.AlertTips;
|
||||||
|
import com.imyeyu.io.IO;
|
||||||
|
import com.imyeyu.java.TimiJava;
|
||||||
|
import com.imyeyu.java.bean.CallbackArgReturn;
|
||||||
|
import com.imyeyu.utils.OS;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.beans.binding.BooleanBinding;
|
||||||
|
import javafx.beans.property.BooleanProperty;
|
||||||
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.collections.ObservableList;
|
||||||
|
import javafx.scene.control.ContextMenu;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.MenuItem;
|
||||||
|
import javafx.scene.control.TreeCell;
|
||||||
|
import javafx.scene.control.TreeItem;
|
||||||
|
import javafx.scene.input.KeyCode;
|
||||||
|
import javafx.scene.input.KeyEvent;
|
||||||
|
import javafx.scene.text.Text;
|
||||||
|
|
||||||
|
import javax.naming.NoPermissionException;
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件目录树组件
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-05-26 14:32
|
||||||
|
*/
|
||||||
|
public class FileTreeView extends XTreeView<File> implements TimiFXUI.Colorful {
|
||||||
|
|
||||||
|
/** 正在查找节点监听 */
|
||||||
|
protected final BooleanBinding findingItem;
|
||||||
|
|
||||||
|
/** 显示隐藏文件监听 */
|
||||||
|
protected final BooleanProperty showHide;
|
||||||
|
|
||||||
|
/** 选择队列,左进左出,右边为深度路径,不为空时表正在查找节点 */
|
||||||
|
protected final ObservableList<File> selectDeque = FXCollections.synchronizedObservableList(FXCollections.observableArrayList());
|
||||||
|
|
||||||
|
/** 过滤列表,返回 true 时创建该节点 */
|
||||||
|
protected final List<CallbackArgReturn<File, Boolean>> itemFilters;
|
||||||
|
|
||||||
|
/** 默认构造器 */
|
||||||
|
public FileTreeView() {
|
||||||
|
showHide = new SimpleBooleanProperty(false);
|
||||||
|
itemFilters = new ArrayList<>();
|
||||||
|
findingItem = Bindings.isNotEmpty(selectDeque);
|
||||||
|
|
||||||
|
disableProperty().bind(findingItem);
|
||||||
|
setCellFactory(cell -> new TreeCell<>() {
|
||||||
|
|
||||||
|
final Label loading = new Label(TimiFXUI.MULTILINGUAL.text("loading"));
|
||||||
|
final Text iconFile = TimiFXIcon.fromName("FILE");
|
||||||
|
final Text iconDirectory = TimiFXIcon.fromName("FOLDER");
|
||||||
|
|
||||||
|
{
|
||||||
|
iconFile.fillProperty().bind(Bindings.when(FileTreeView.this.focusedProperty().and(selectedProperty())).then(GRAY_WHITE).otherwise(GRAY));
|
||||||
|
iconDirectory.fillProperty().bind(Bindings.when(FileTreeView.this.focusedProperty().and(selectedProperty())).then(GRAY_WHITE).otherwise(GRAY));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void updateItem(File file, boolean empty) {
|
||||||
|
super.updateItem(file, empty);
|
||||||
|
if (empty) {
|
||||||
|
setText("");
|
||||||
|
setGraphic(null);
|
||||||
|
} else {
|
||||||
|
if (file == null) {
|
||||||
|
setGraphic(loading);
|
||||||
|
} else {
|
||||||
|
if (TimiJava.isEmpty(file.getName())) {
|
||||||
|
setText(file.toString());
|
||||||
|
} else {
|
||||||
|
setText(file.getName());
|
||||||
|
}
|
||||||
|
setGraphic(file.isFile() ? iconFile : iconDirectory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 右键菜单
|
||||||
|
MenuItem menuMkdir = new MenuItem(TimiFXUI.MULTILINGUAL.text("file.mkdir"), TimiFXIcon.fromName("FOLDER_ADD"));
|
||||||
|
MenuItem menuRename = new MenuItem(TimiFXUI.MULTILINGUAL.text("rename"));
|
||||||
|
MenuItem menuRefresh = new MenuItem(TimiFXUI.MULTILINGUAL.text("refresh"), TimiFXIcon.fromName("REFRESH"));
|
||||||
|
MenuItem menuDestroy = new MenuItem(TimiFXUI.MULTILINGUAL.text("delete"), TimiFXIcon.fromName("FAIL", RED));
|
||||||
|
|
||||||
|
setContextMenu(new ContextMenu(menuMkdir, menuRename, menuRefresh, TimiFXUI.sep(), menuDestroy));
|
||||||
|
|
||||||
|
// ---------- 事件 ----------
|
||||||
|
|
||||||
|
// 新建文件夹
|
||||||
|
menuMkdir.disableProperty().bind(menuRefresh.disableProperty());
|
||||||
|
menuMkdir.setOnAction(e -> mkdir(getSelectionModel().getSelectedItem()));
|
||||||
|
|
||||||
|
// 重命名
|
||||||
|
menuRename.disableProperty().bind(ObservableUtils.onlyOnceInList(getSelectionModel().getSelectedItems()).not());
|
||||||
|
menuRename.setOnAction(e -> rename(getSelectionModel().getSelectedItem()));
|
||||||
|
|
||||||
|
// 刷新
|
||||||
|
menuRefresh.disableProperty().bind(Bindings.createBooleanBinding(() -> {
|
||||||
|
List<TreeItem<File>> items = getSelectionModel().getSelectedItems();
|
||||||
|
// 没有选择、多选、选的不是文件时禁用
|
||||||
|
return items == null || items.size() != 1 || items.get(0).getValue().isFile();
|
||||||
|
}, getSelectionModel().selectedItemProperty()));
|
||||||
|
menuRefresh.setOnAction(e -> refreshItem(getSelectionModel().getSelectedItem()));
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
List<File> roots = List.of(File.listRoots());
|
||||||
|
menuDestroy.disableProperty().bind(Bindings.createBooleanBinding(() -> {
|
||||||
|
ObservableList<TreeItem<File>> items = getSelectionModel().getSelectedItems();
|
||||||
|
if (items.isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < items.size(); i++) {
|
||||||
|
if (roots.contains(items.get(i).getValue())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}, getSelectionModel().getSelectedItems()));
|
||||||
|
menuDestroy.setOnAction(e -> destroy(getSelectionModel().getSelectedItems()));
|
||||||
|
|
||||||
|
// 快捷键
|
||||||
|
addEventFilter(KeyEvent.KEY_RELEASED, e -> {
|
||||||
|
boolean control = e.isControlDown();
|
||||||
|
boolean shift = e.isShiftDown();
|
||||||
|
boolean alt = e.isAltDown();
|
||||||
|
KeyCode code = e.getCode();
|
||||||
|
|
||||||
|
if (!control && !shift && !alt) {
|
||||||
|
switch (code) {
|
||||||
|
case F2 -> menuRename.fire();
|
||||||
|
case F5 -> menuRefresh.fire();
|
||||||
|
case DELETE -> menuDestroy.fire();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (control && shift && !alt && code == KeyCode.N) {
|
||||||
|
menuMkdir.fire();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------- 就绪 ----------
|
||||||
|
|
||||||
|
// 过滤隐藏文件
|
||||||
|
CallbackArgReturn<File, Boolean> filterHidden = file -> !file.isHidden();
|
||||||
|
itemFilters.add(filterHidden);
|
||||||
|
showHide.addListener((obs, o, n) -> {
|
||||||
|
if (isShowHide()) {
|
||||||
|
itemFilters.remove(filterHidden);
|
||||||
|
} else {
|
||||||
|
itemFilters.add(filterHidden);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 默认磁盘根目录
|
||||||
|
for (int i = 0; i < roots.size(); i++) {
|
||||||
|
getRoots().add(new FileItem(roots.get(i)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建文件夹
|
||||||
|
*
|
||||||
|
* @param base 基于文件夹
|
||||||
|
*/
|
||||||
|
public void mkdir(TreeItem<File> base) {
|
||||||
|
if (base == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AlertTextField alert = new AlertTextField(TimiFXUI.MULTILINGUAL.text("file.mkdir"));
|
||||||
|
alert.setTips(TimiFXUI.MULTILINGUAL.text("name"));
|
||||||
|
alert.setOnActionEvent(action -> {
|
||||||
|
if (action == AlertButton.Action.CONFIRM) {
|
||||||
|
try {
|
||||||
|
IO.dir(IO.fitPath(base.getValue().getAbsolutePath()) + alert.getText());
|
||||||
|
refreshItem(getSelectionModel().getSelectedItem());
|
||||||
|
} catch (NoPermissionException ex) {
|
||||||
|
ex.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
alert.autoSize().showRelativeCenter(getScene().getWindow());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重命名
|
||||||
|
*
|
||||||
|
* @param file 文件或文件夹
|
||||||
|
*/
|
||||||
|
public void rename(TreeItem<File> file) {
|
||||||
|
if (file == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AlertTextField alert = new AlertTextField(TimiFXUI.MULTILINGUAL.text("file.rename"));
|
||||||
|
alert.setTips(TimiFXUI.MULTILINGUAL.text("name"));
|
||||||
|
alert.setOnActionEvent(action -> {
|
||||||
|
if (action == AlertButton.Action.CONFIRM) {
|
||||||
|
IO.rename(getSelectionModel().getSelectedItem().getValue(), alert.getText());
|
||||||
|
refreshItem(getSelectionModel().getSelectedItem().getParent());
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
alert.autoSize().showRelativeCenter(getScene().getWindow());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 销毁文件
|
||||||
|
*
|
||||||
|
* @param files 文件节点列表
|
||||||
|
*/
|
||||||
|
public void destroy(List<TreeItem<File>> files) {
|
||||||
|
if (TimiJava.isEmpty(files)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
new AlertConfirm(TimiFXUI.MULTILINGUAL.text("file.destroy")) {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onConfirm() {
|
||||||
|
new RunAsync<TreeItem<File>>() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected TreeItem<File> call() {
|
||||||
|
List<File> items = files.stream().map(TreeItem::getValue).toList();
|
||||||
|
for (int i = 0; i < items.size(); i++) {
|
||||||
|
IO.destroy(items.get(i));
|
||||||
|
}
|
||||||
|
// 查找最高级节点刷新
|
||||||
|
int l = Integer.MAX_VALUE;
|
||||||
|
TreeItem<File> item = null;
|
||||||
|
for (int i = 0, j; i < items.size(); i++) {
|
||||||
|
j = getTreeItemLevel(files.get(i));
|
||||||
|
if (j < l) {
|
||||||
|
l = j;
|
||||||
|
item = files.get(i);
|
||||||
|
if (item.getParent() != null) {
|
||||||
|
item = item.getParent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onFinish(TreeItem<File> item) {
|
||||||
|
if (item != null) {
|
||||||
|
refreshItem(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onException(Throwable e) {
|
||||||
|
AlertTips.error(getScene().getWindow(), TimiFXUI.MULTILINGUAL.text("file.tips.destroy_fail"));
|
||||||
|
}
|
||||||
|
}.start();
|
||||||
|
}
|
||||||
|
}.autoSize().showRelativeCenter(getScene().getWindow());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新子节点
|
||||||
|
*
|
||||||
|
* @param treeItem 父级节点
|
||||||
|
*/
|
||||||
|
public void refreshItem(TreeItem<File> treeItem) {
|
||||||
|
if (treeItem instanceof FileItem fileItem) {
|
||||||
|
fileItem.getChildren().clear();
|
||||||
|
fileItem.getChildren().add(new FileItem());
|
||||||
|
fileItem.asyncLoadChildren();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新子节点
|
||||||
|
*
|
||||||
|
* @param fileItem 父级节点
|
||||||
|
*/
|
||||||
|
public void refreshItem(FileItem fileItem) {
|
||||||
|
fileItem.getChildren().clear();
|
||||||
|
fileItem.getChildren().add(new FileItem());
|
||||||
|
fileItem.asyncLoadChildren();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选择目标路径
|
||||||
|
*
|
||||||
|
* @param path 路径
|
||||||
|
*/
|
||||||
|
public void selectItem(String path) {
|
||||||
|
if (TimiJava.isEmpty(path)) {
|
||||||
|
path = "./";
|
||||||
|
}
|
||||||
|
selectItem(new File(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选择目标文件
|
||||||
|
*
|
||||||
|
* @param file 目标文件
|
||||||
|
*/
|
||||||
|
public void selectItem(File file) {
|
||||||
|
File parent = file.getAbsoluteFile();
|
||||||
|
if (!parent.exists()) {
|
||||||
|
parent = new File("./").getAbsoluteFile();
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
selectDeque.add(0, parent);
|
||||||
|
} while ((parent = parent.getParentFile()) != null);
|
||||||
|
|
||||||
|
if (TimiJava.isNotEmpty(selectDeque)) {
|
||||||
|
ObservableList<TreeItem<File>> roots = getRoots();
|
||||||
|
for (int i = 0; i < roots.size(); i++) {
|
||||||
|
roots.get(i).setExpanded(false);
|
||||||
|
if (roots.get(i).getValue().equals(selectDeque.get(0))) {
|
||||||
|
selectDeque.remove(0);
|
||||||
|
roots.get(i).setExpanded(true);
|
||||||
|
if (TimiJava.isEmpty(selectDeque)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加构建节点过滤器,返回 false 时不创建该节点
|
||||||
|
*
|
||||||
|
* @param itemFilter 节点过滤器
|
||||||
|
*/
|
||||||
|
public void addItemFilter(CallbackArgReturn<File, Boolean> itemFilter) {
|
||||||
|
itemFilters.add(itemFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除构建节点过滤器
|
||||||
|
*
|
||||||
|
* @param itemFilter 节点过滤器
|
||||||
|
*/
|
||||||
|
public void removeItemFilter(CallbackArgReturn<File, Boolean> itemFilter) {
|
||||||
|
itemFilters.remove(itemFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置是否显示隐藏文件
|
||||||
|
*
|
||||||
|
* @param showHide true 为显示隐藏文件
|
||||||
|
*/
|
||||||
|
public void setShowHide(boolean showHide) {
|
||||||
|
this.showHide.set(showHide);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前是否显示隐藏文件
|
||||||
|
*
|
||||||
|
* @return true 为显示隐藏文件
|
||||||
|
*/
|
||||||
|
public boolean isShowHide() {
|
||||||
|
return showHide.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取显示隐藏文件监听
|
||||||
|
*
|
||||||
|
* @return 显示隐藏文件监听
|
||||||
|
*/
|
||||||
|
public BooleanProperty showHideProperty() {
|
||||||
|
return showHide;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取正在查找节点监听,此时属于被动展开,用于阻止主动展开的加载节点
|
||||||
|
*
|
||||||
|
* @return 正在查找节点监听
|
||||||
|
*/
|
||||||
|
public BooleanBinding findingItemProperty() {
|
||||||
|
return findingItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件节点
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2023-03-16 00:23
|
||||||
|
*/
|
||||||
|
public final class FileItem extends TreeItem<File> {
|
||||||
|
|
||||||
|
/** 默认构造,此时为占位节点 */
|
||||||
|
FileItem() {
|
||||||
|
this(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标准构造
|
||||||
|
*
|
||||||
|
* @param file 文件
|
||||||
|
*/
|
||||||
|
FileItem(File file) {
|
||||||
|
super(file);
|
||||||
|
|
||||||
|
if (file != null && file.isDirectory()) {
|
||||||
|
getChildren().add(new FileItem(null)); // 占位
|
||||||
|
}
|
||||||
|
|
||||||
|
// 展开
|
||||||
|
expandedProperty().addListener((obs, o, isExpanded) -> {
|
||||||
|
if (isExpanded) {
|
||||||
|
getSelectionModel().clearSelection();
|
||||||
|
getSelectionModel().select(this);
|
||||||
|
asyncLoadChildren();
|
||||||
|
} else {
|
||||||
|
getChildren().add(new FileItem());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 异步加载子节点 */
|
||||||
|
void asyncLoadChildren() {
|
||||||
|
RunAsync.callbackReturn(() -> {
|
||||||
|
List<FileItem> fileItems = new ArrayList<>();
|
||||||
|
File[] files = getValue().listFiles();
|
||||||
|
if (files != null) {
|
||||||
|
// 排序
|
||||||
|
List<File> fileList = Arrays.stream(files).sorted(OS.FileSystem.COMPARATOR_FILE_NAME).toList();
|
||||||
|
|
||||||
|
// 过滤
|
||||||
|
list:
|
||||||
|
for (int i = 0; i < fileList.size(); i++) {
|
||||||
|
for (int j = 0; j < itemFilters.size(); j++) {
|
||||||
|
if (!itemFilters.get(j).handler(fileList.get(i))) {
|
||||||
|
continue list;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fileItems.add(new FileItem(fileList.get(i)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fileItems;
|
||||||
|
}, items -> {
|
||||||
|
getChildren().setAll(items);
|
||||||
|
|
||||||
|
if (TimiJava.isNotEmpty(selectDeque)) {
|
||||||
|
File file = selectDeque.remove(0);
|
||||||
|
for (int i = 0; i < items.size(); i++) {
|
||||||
|
if (items.get(i).getValue().equals(file)) {
|
||||||
|
if (items.get(i).getValue().isDirectory()) {
|
||||||
|
items.get(i).setExpanded(true);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (TimiJava.isEmpty(selectDeque)) {
|
||||||
|
// 执行选中
|
||||||
|
Platform.runLater(() -> scrollTo(getSelectionModel().getSelectedIndex() - 5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
208
src/main/java/com/imyeyu/fx/ui/components/IconButton.java
Normal file
208
src/main/java/com/imyeyu/fx/ui/components/IconButton.java
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
package com.imyeyu.fx.ui.components;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.TimiFX;
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import com.imyeyu.java.TimiJava;
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.beans.property.BooleanProperty;
|
||||||
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.Skin;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
|
import javafx.scene.paint.Paint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图标按钮,可选图片、SVG 路径或 {@link com.imyeyu.fx.icon.TimiFXIcon} 的字体图标
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2021-12-23 17:58
|
||||||
|
*/
|
||||||
|
public class IconButton extends Button implements TimiFXUI, TimiFXUI.Colorful {
|
||||||
|
|
||||||
|
private static final String STYLE_CLASS = "icon-button";
|
||||||
|
|
||||||
|
/** 是否自适应尺寸 */
|
||||||
|
private final BooleanProperty autoSize;
|
||||||
|
|
||||||
|
private Insets iconPadding, iconTextPadding; // 缓存单独图标内边距和图标文本混合的内边距
|
||||||
|
|
||||||
|
/** 默认构造器 */
|
||||||
|
public IconButton() {
|
||||||
|
this("", (Node) null);
|
||||||
|
setAlignment(Pos.CENTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 图片图标 ----------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造图片图标按钮
|
||||||
|
*
|
||||||
|
* @param img 图片
|
||||||
|
*/
|
||||||
|
public IconButton(Image img) {
|
||||||
|
this("", new ImageView(img));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造图片图标按钮
|
||||||
|
*
|
||||||
|
* @param text 按钮文本
|
||||||
|
* @param img 图片
|
||||||
|
*/
|
||||||
|
public IconButton(String text, Image img) {
|
||||||
|
this(text, new ImageView(img));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- SVG 图标 ----------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造 SVG 图标按钮
|
||||||
|
*
|
||||||
|
* @param svgPath SVG 路径
|
||||||
|
*/
|
||||||
|
public IconButton(String svgPath) {
|
||||||
|
this(svgPath, ICON);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造 SVG 图标按钮
|
||||||
|
*
|
||||||
|
* @param svgPath SVG 路径
|
||||||
|
* @param fill 填充颜色
|
||||||
|
*/
|
||||||
|
public IconButton(String svgPath, Paint fill) {
|
||||||
|
this("", new SVGIcon(svgPath, fill));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造 SVG 图标按钮
|
||||||
|
*
|
||||||
|
* @param text 按钮文本
|
||||||
|
* @param svgPath SVG 路径
|
||||||
|
* @param fill 填充颜色
|
||||||
|
*/
|
||||||
|
public IconButton(String text, String svgPath, Paint fill) {
|
||||||
|
this(text, new SVGIcon(svgPath, fill));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 默认构造 ----------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造自定义节点按钮
|
||||||
|
*
|
||||||
|
* @param node 节点
|
||||||
|
*/
|
||||||
|
public IconButton(Node node) {
|
||||||
|
this("", node);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造自定义节点按钮
|
||||||
|
*
|
||||||
|
* @param text 文本
|
||||||
|
* @param node 节点
|
||||||
|
*/
|
||||||
|
public IconButton(String text, Node node) {
|
||||||
|
super(text);
|
||||||
|
autoSize = new SimpleBooleanProperty(false);
|
||||||
|
|
||||||
|
getStyleClass().setAll(CSS.MINECRAFT, STYLE_CLASS);
|
||||||
|
setAlignment(Pos.CENTER);
|
||||||
|
setMaxHeight(Double.MAX_VALUE);
|
||||||
|
setPickOnBounds(true);
|
||||||
|
|
||||||
|
if (node != null) {
|
||||||
|
setGraphic(node);
|
||||||
|
}
|
||||||
|
TimiFX.hoverOpacity(this);
|
||||||
|
|
||||||
|
// 自适应尺寸、单独图标、图标文本混合时设置不同的内边距
|
||||||
|
paddingProperty().bind(Bindings.createObjectBinding(() -> {
|
||||||
|
if (autoSize.get()) {
|
||||||
|
return Insets.EMPTY;
|
||||||
|
} else {
|
||||||
|
if (TimiJava.isEmpty(getText())) {
|
||||||
|
return iconPadding == null ? Insets.EMPTY : iconPadding;
|
||||||
|
} else {
|
||||||
|
return iconTextPadding == null ? Insets.EMPTY : iconTextPadding;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, textProperty(), autoSize, skinProperty()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加按钮背景
|
||||||
|
*
|
||||||
|
* @return 本实例
|
||||||
|
*/
|
||||||
|
public IconButton withBackground() {
|
||||||
|
return withBackground(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加按钮背景
|
||||||
|
*
|
||||||
|
* @param borderClass 边框类
|
||||||
|
* @return 本实例
|
||||||
|
*/
|
||||||
|
public IconButton withBackground(String borderClass) {
|
||||||
|
getStyleClass().add(CSS.BG_BUTTON);
|
||||||
|
if (TimiJava.isNotEmpty(borderClass)) {
|
||||||
|
getStyleClass().add(borderClass);
|
||||||
|
}
|
||||||
|
opacityProperty().unbind();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自适应尺寸,不使用内边距填充,图标尺寸决定组件尺寸
|
||||||
|
*
|
||||||
|
* @return 本实例
|
||||||
|
*/
|
||||||
|
public IconButton autoSize() {
|
||||||
|
autoSize.set(true);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前是否自适应尺寸,ture 时图标尺寸决定组件尺寸
|
||||||
|
*
|
||||||
|
* @return true 为自适应尺寸
|
||||||
|
*/
|
||||||
|
public boolean isAutoSize() {
|
||||||
|
return autoSize.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置是否自适应尺寸,ture 时图标尺寸决定组件尺寸
|
||||||
|
*
|
||||||
|
* @param autoSize true 为自适应尺寸
|
||||||
|
*/
|
||||||
|
public void setAutoSize(boolean autoSize) {
|
||||||
|
this.autoSize.set(autoSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取自适应尺寸监听
|
||||||
|
*
|
||||||
|
* @return 自适应尺寸监听
|
||||||
|
*/
|
||||||
|
public BooleanProperty autoSizeProperty() {
|
||||||
|
return autoSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Skin<?> createDefaultSkin() {
|
||||||
|
Skin<?> defaultSkin = super.createDefaultSkin();
|
||||||
|
double h = getFont().getSize() * .382;
|
||||||
|
double v = h * .8;
|
||||||
|
double tv = h * .6;
|
||||||
|
iconPadding = new Insets(v);
|
||||||
|
iconTextPadding = new Insets(tv, h, tv, h);
|
||||||
|
return defaultSkin;
|
||||||
|
}
|
||||||
|
}
|
||||||
207
src/main/java/com/imyeyu/fx/ui/components/IconPicker.java
Normal file
207
src/main/java/com/imyeyu/fx/ui/components/IconPicker.java
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
package com.imyeyu.fx.ui.components;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.icon.TimiFXIcon;
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import com.imyeyu.fx.utils.BgFill;
|
||||||
|
import com.imyeyu.fx.utils.SmoothScroll;
|
||||||
|
import com.imyeyu.java.TimiJava;
|
||||||
|
import com.imyeyu.java.ref.Ref;
|
||||||
|
import com.imyeyu.utils.Collect;
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.geometry.Bounds;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.scene.control.ScrollPane;
|
||||||
|
import javafx.scene.control.Skin;
|
||||||
|
import javafx.scene.control.TextField;
|
||||||
|
import javafx.scene.control.ToggleButton;
|
||||||
|
import javafx.scene.control.ToggleGroup;
|
||||||
|
import javafx.scene.control.skin.TextFieldSkin;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.scene.layout.Background;
|
||||||
|
import javafx.scene.layout.BorderPane;
|
||||||
|
import javafx.scene.layout.Pane;
|
||||||
|
import javafx.scene.layout.StackPane;
|
||||||
|
import javafx.scene.layout.TilePane;
|
||||||
|
import javafx.scene.text.Text;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import javafx.stage.StageStyle;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link TimiFXIcon} 的图标选择器(标签搜索需要联网)
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-08-01 16:08
|
||||||
|
*/
|
||||||
|
public class IconPicker extends TextField implements TimiFXUI {
|
||||||
|
|
||||||
|
private final BorderPane root;
|
||||||
|
|
||||||
|
/** 默认构造 */
|
||||||
|
public IconPicker() {
|
||||||
|
IconPickerPopup popup = new IconPickerPopup();
|
||||||
|
|
||||||
|
// 注入面板
|
||||||
|
root = new BorderPane();
|
||||||
|
|
||||||
|
setEditable(false);
|
||||||
|
|
||||||
|
// ---------- 事件 ----------
|
||||||
|
|
||||||
|
// 更新选择
|
||||||
|
textProperty().addListener((obs, o, value) -> {
|
||||||
|
if (TimiJava.isEmpty(value)) {
|
||||||
|
root.setLeft(null);
|
||||||
|
} else {
|
||||||
|
Text icon = TimiFXIcon.fromName(value);
|
||||||
|
BorderPane.setMargin(icon, new Insets(0, 2, 0, 0));
|
||||||
|
BorderPane.setAlignment(icon, Pos.CENTER);
|
||||||
|
root.setLeft(icon);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 点击显示
|
||||||
|
setOnMouseClicked(e -> {
|
||||||
|
Bounds b = localToScreen(getLayoutBounds());
|
||||||
|
popup.setX(b.getMinX() - 6); // 6 像素投影
|
||||||
|
popup.setY(b.getMaxY() - 6 - 1);
|
||||||
|
if (popup.getOwner() == null) {
|
||||||
|
popup.initOwner(getScene().getWindow());
|
||||||
|
}
|
||||||
|
popup.show();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 选择
|
||||||
|
popup.group.selectedToggleProperty().addListener((obs, o, newToggle) -> {
|
||||||
|
if (newToggle == null) {
|
||||||
|
clear();
|
||||||
|
} else if (newToggle instanceof ToggleIcon icon) {
|
||||||
|
setText(icon.name.toUpperCase());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Skin<?> createDefaultSkin() {
|
||||||
|
Skin<?> skin = super.createDefaultSkin();
|
||||||
|
if (skin instanceof TextFieldSkin textFieldSkin) {
|
||||||
|
try {
|
||||||
|
Pane textGroup = Ref.getFieldValue(textFieldSkin, "textGroup", Pane.class);
|
||||||
|
|
||||||
|
root.setCenter(textGroup);
|
||||||
|
|
||||||
|
textFieldSkin.getChildren().setAll(root);
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return skin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取选择的图标图像
|
||||||
|
*
|
||||||
|
* @return 图标图像
|
||||||
|
*/
|
||||||
|
public Image getValue() {
|
||||||
|
return TimiFXIcon.imageFromName(getText());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图标选择弹窗
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-08-01 17:12
|
||||||
|
*/
|
||||||
|
private static class IconPickerPopup extends Stage {
|
||||||
|
|
||||||
|
private static final String SEARCH_API = "https://api.timiserver.imyeyu.net/icon/search/label";
|
||||||
|
|
||||||
|
final ToggleGroup group;
|
||||||
|
final List<ToggleIcon> icons;
|
||||||
|
final Map<String, Character> nameMapping = TimiFXIcon.getNameMapping();
|
||||||
|
|
||||||
|
public IconPickerPopup() {
|
||||||
|
// 图标
|
||||||
|
TilePane tile = new TilePane();
|
||||||
|
tile.setPadding(new Insets(6));
|
||||||
|
|
||||||
|
icons = new ArrayList<>();
|
||||||
|
group = new ToggleGroup();
|
||||||
|
Map<String, Character> items = Collect.sortMapByStringKeyASC(nameMapping);
|
||||||
|
for (Map.Entry<String, Character> item : items.entrySet()) {
|
||||||
|
icons.add(new ToggleIcon(group, item.getKey()));
|
||||||
|
}
|
||||||
|
tile.getChildren().addAll(icons);
|
||||||
|
|
||||||
|
focusedProperty().addListener((obs, o, isFocused) -> {
|
||||||
|
if (!isFocused) {
|
||||||
|
hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Scene scene = new Scene(new StackPane() {{
|
||||||
|
setPadding(Shadow.PADDING);
|
||||||
|
setBackground(BG.TRANSPARENT);
|
||||||
|
getChildren().add(new BorderPane() {{
|
||||||
|
setEffect(Shadow.POPUP);
|
||||||
|
setBorder(Stroke.DEFAULT);
|
||||||
|
setMaxHeight(280);
|
||||||
|
setBackground(BG.DEFAULT);
|
||||||
|
setCenter(new ScrollPane() {{
|
||||||
|
setContent(tile);
|
||||||
|
setPadding(new Insets(6, 8, 6, 8));
|
||||||
|
setFitToWidth(true);
|
||||||
|
|
||||||
|
SmoothScroll.scrollPane(this);
|
||||||
|
}});
|
||||||
|
}});
|
||||||
|
}});
|
||||||
|
scene.setFill(null);
|
||||||
|
scene.getStylesheets().addAll(CSS_STYLE, CSS_FONT);
|
||||||
|
setScene(scene);
|
||||||
|
setWidth(360);
|
||||||
|
setHeight(240);
|
||||||
|
initStyle(StageStyle.TRANSPARENT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图标按钮
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-08-01 17:38
|
||||||
|
*/
|
||||||
|
private static class ToggleIcon extends ToggleButton {
|
||||||
|
|
||||||
|
private static final Background SELECTED = new BgFill("#99D1FF").build();
|
||||||
|
|
||||||
|
/** 图标名称 */
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
public ToggleIcon(ToggleGroup ownerGroup, String name) {
|
||||||
|
super("");
|
||||||
|
this.name = name;
|
||||||
|
|
||||||
|
setGraphic(TimiFXIcon.fromName(name));
|
||||||
|
getStyleClass().clear();
|
||||||
|
managedProperty().bind(visibleProperty());
|
||||||
|
borderProperty().bind(Bindings.when(hoverProperty()).then(Stroke.FOCUSED).otherwise(Stroke.TP));
|
||||||
|
backgroundProperty().bind(Bindings.when(selectedProperty()).then(SELECTED).otherwise(BG.TRANSPARENT));
|
||||||
|
|
||||||
|
ownerGroup.getToggles().add(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Skin<?> createDefaultSkin() {
|
||||||
|
Skin<?> defaultSkin = super.createDefaultSkin();
|
||||||
|
setPadding(new Insets(getFont().getSize() * .25));
|
||||||
|
return defaultSkin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
138
src/main/java/com/imyeyu/fx/ui/components/LabelProgressBar.java
Normal file
138
src/main/java/com/imyeyu/fx/ui/components/LabelProgressBar.java
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
package com.imyeyu.fx.ui.components;
|
||||||
|
|
||||||
|
import com.imyeyu.java.bean.CallbackArgReturn;
|
||||||
|
import javafx.beans.property.DoubleProperty;
|
||||||
|
import javafx.beans.property.StringProperty;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.ProgressBar;
|
||||||
|
import javafx.scene.layout.StackPane;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标签进度。如果继承进度组件使用反射注入标签组件时,在设置标签文本会导致标签消失
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-08-09 10:48
|
||||||
|
*/
|
||||||
|
public class LabelProgressBar extends StackPane {
|
||||||
|
|
||||||
|
private static final String STYLE_CLASS = "label-progress-bar";
|
||||||
|
|
||||||
|
/** 标签 */
|
||||||
|
protected final Label label;
|
||||||
|
|
||||||
|
/** 进度 */
|
||||||
|
protected final ProgressBar bar;
|
||||||
|
|
||||||
|
/** 标签转换 */
|
||||||
|
protected CallbackArgReturn<Double, String> converter;
|
||||||
|
|
||||||
|
/** 默认构造 */
|
||||||
|
public LabelProgressBar() {
|
||||||
|
label = new Label();
|
||||||
|
bar = new ProgressBar();
|
||||||
|
bar.prefWidthProperty().bind(widthProperty());
|
||||||
|
bar.getStyleClass().add(STYLE_CLASS);
|
||||||
|
|
||||||
|
getChildren().addAll(bar, label);
|
||||||
|
|
||||||
|
bar.progressProperty().addListener((obs, o, p) -> {
|
||||||
|
if (converter != null) {
|
||||||
|
if (p == null) {
|
||||||
|
label.setText(converter.handler(-1D));
|
||||||
|
} else {
|
||||||
|
label.setText(converter.handler(p.doubleValue()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前标签文本
|
||||||
|
*
|
||||||
|
* @return 标签文本
|
||||||
|
*/
|
||||||
|
public String getText() {
|
||||||
|
return label.getText();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置标签文本
|
||||||
|
*
|
||||||
|
* @param text 标签文本
|
||||||
|
*/
|
||||||
|
public void setText(String text) {
|
||||||
|
label.setText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取标签文本监听
|
||||||
|
*
|
||||||
|
* @return 标签文本监听
|
||||||
|
*/
|
||||||
|
public StringProperty textProperty() {
|
||||||
|
return label.textProperty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取标签组件
|
||||||
|
*
|
||||||
|
* @return 标签组件
|
||||||
|
*/
|
||||||
|
public Label getLabel() {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置进度值
|
||||||
|
*
|
||||||
|
* @param progress 进度值,取值范围 [0, 1]
|
||||||
|
*/
|
||||||
|
public void setProgress(double progress) {
|
||||||
|
bar.setProgress(progress);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取进度值
|
||||||
|
*
|
||||||
|
* @return 进度值
|
||||||
|
*/
|
||||||
|
public double getProgress() {
|
||||||
|
return bar.getProgress();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取进度监听
|
||||||
|
*
|
||||||
|
* @return 进度监听
|
||||||
|
*/
|
||||||
|
public DoubleProperty progressProperty() {
|
||||||
|
return bar.progressProperty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取进度组件
|
||||||
|
*
|
||||||
|
* @return 进度组件
|
||||||
|
*/
|
||||||
|
public ProgressBar getBar() {
|
||||||
|
return bar;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取标签转换回调
|
||||||
|
*
|
||||||
|
* @return 转换回调
|
||||||
|
*/
|
||||||
|
public CallbackArgReturn<Double, String> getConverter() {
|
||||||
|
return converter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置标签转换回调,进度变换时触发回调
|
||||||
|
*
|
||||||
|
* @param converter 标签转换回调
|
||||||
|
*/
|
||||||
|
public void setConverter(CallbackArgReturn<Double, String> converter) {
|
||||||
|
this.converter = converter;
|
||||||
|
}
|
||||||
|
}
|
||||||
243
src/main/java/com/imyeyu/fx/ui/components/Navigation.java
Normal file
243
src/main/java/com/imyeyu/fx/ui/components/Navigation.java
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
package com.imyeyu.fx.ui.components;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.TimiFX;
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import com.imyeyu.fx.utils.SmoothScroll;
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.collections.ListChangeListener;
|
||||||
|
import javafx.collections.ObservableList;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.ScrollPane;
|
||||||
|
import javafx.scene.control.TitledPane;
|
||||||
|
import javafx.scene.control.ToggleButton;
|
||||||
|
import javafx.scene.control.ToggleGroup;
|
||||||
|
import javafx.scene.input.MouseEvent;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 纵向导航组件,可实现二级导航,折叠导航
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-02-17 00:11
|
||||||
|
*/
|
||||||
|
public class Navigation extends ScrollPane implements TimiFXUI {
|
||||||
|
|
||||||
|
/** 导航列表项 */
|
||||||
|
protected final ObservableList<ToggleButton> items;
|
||||||
|
|
||||||
|
/** 已选中监听 */
|
||||||
|
protected final ObjectProperty<ToggleButton> selectedItem;
|
||||||
|
|
||||||
|
/** 默认构造器 */
|
||||||
|
public Navigation() {
|
||||||
|
items = FXCollections.observableArrayList();
|
||||||
|
selectedItem = new SimpleObjectProperty<>();
|
||||||
|
|
||||||
|
VBox root = new VBox();
|
||||||
|
root.setBorder(Stroke.BOTTOM);
|
||||||
|
|
||||||
|
getStyleClass().addAll("navigation", "sp-border");
|
||||||
|
setMaxWidth(Double.MAX_VALUE);
|
||||||
|
setVbarPolicy(ScrollBarPolicy.NEVER);
|
||||||
|
setFitToWidth(true);
|
||||||
|
setContent(root);
|
||||||
|
|
||||||
|
SmoothScroll.scrollPaneV(this);
|
||||||
|
|
||||||
|
// ---------- 事件 ----------
|
||||||
|
|
||||||
|
ToggleGroup group = new ToggleGroup();
|
||||||
|
|
||||||
|
// 主动选择(代码触发)
|
||||||
|
selectedItem.addListener((obs, o, newSelectedItem) -> group.selectToggle(newSelectedItem));
|
||||||
|
|
||||||
|
// 被动选择(操作触发)
|
||||||
|
group.selectedToggleProperty().addListener((obs, o, toggle) -> {
|
||||||
|
if (toggle instanceof ToggleButton btn) {
|
||||||
|
selectedItem.set(btn);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ObservableList<Node> childrens = root.getChildren();
|
||||||
|
|
||||||
|
// 响应 TimiFXUI
|
||||||
|
items.addListener((ListChangeListener<ToggleButton>) c -> {
|
||||||
|
while (c.next()) {
|
||||||
|
if (c.wasAdded()) {
|
||||||
|
// 添加
|
||||||
|
List<? extends ToggleButton> list = c.getAddedSubList();
|
||||||
|
for (int i = 0; i < list.size(); i++) {
|
||||||
|
list.get(i).setMaxWidth(Double.MAX_VALUE);
|
||||||
|
list.get(i).getStyleClass().setAll(CSS.MINECRAFT, "navigation-button");
|
||||||
|
list.get(i).addEventFilter(MouseEvent.MOUSE_PRESSED, TimiFX.EVENT_CONSUME_TG_BTN);
|
||||||
|
|
||||||
|
if (list.get(i).getProperties().get("OWNER") instanceof TitledPane pane) {
|
||||||
|
// 存在所属组
|
||||||
|
if (!childrens.contains(pane)) {
|
||||||
|
// 未添加组
|
||||||
|
childrens.add(pane);
|
||||||
|
}
|
||||||
|
if (pane.getContent() instanceof VBox box) {
|
||||||
|
box.getChildren().add(list.get(i));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 单独项
|
||||||
|
if (!childrens.isEmpty()) {
|
||||||
|
if (childrens.get(childrens.size() - 1) instanceof TitledPane) {
|
||||||
|
// 上一项是组导航,添加上边框
|
||||||
|
list.get(i).getStyleClass().add("after-group");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
childrens.add(list.get(i));
|
||||||
|
}
|
||||||
|
// 归组
|
||||||
|
group.getToggles().add(list.get(i));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (c.wasRemoved()) {
|
||||||
|
// 移除
|
||||||
|
List<? extends ToggleButton> list = c.getRemoved();
|
||||||
|
for (int i = 0; i < list.size(); i++) {
|
||||||
|
if (list.get(i).getProperties().get("OWNER") instanceof TitledPane pane) {
|
||||||
|
// 存在所属组
|
||||||
|
if (pane.getContent() instanceof VBox box) {
|
||||||
|
box.getChildren().remove(list.get(i));
|
||||||
|
if (box.getChildren().isEmpty()) {
|
||||||
|
// 该组已没有列表项
|
||||||
|
childrens.remove(box);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 单独项
|
||||||
|
childrens.remove(list.get(i));
|
||||||
|
}
|
||||||
|
// 从组移除
|
||||||
|
group.getToggles().remove(list.get(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加导航按钮
|
||||||
|
*
|
||||||
|
* @param buttons 导航按钮
|
||||||
|
*/
|
||||||
|
public void add(ToggleButton... buttons) {
|
||||||
|
getItems().addAll(buttons);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加默认没有展开的导航组
|
||||||
|
*
|
||||||
|
* @param title 标题
|
||||||
|
* @param buttons 导航项
|
||||||
|
* @return 构造的折叠面板
|
||||||
|
*/
|
||||||
|
public TitledPane addGroup(String title, ToggleButton... buttons) {
|
||||||
|
return addGroup(title, false, buttons);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加默认展开的导航组
|
||||||
|
*
|
||||||
|
* @param title 标题
|
||||||
|
* @param buttons 导航项
|
||||||
|
* @return 构造的折叠面板
|
||||||
|
*/
|
||||||
|
public TitledPane addExpandedGroup(String title, ToggleButton... buttons) {
|
||||||
|
return addGroup(title, true, buttons);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加导航组
|
||||||
|
*
|
||||||
|
* @param title 标题
|
||||||
|
* @param isExpanded true 为默认展开
|
||||||
|
* @param buttons 导航项
|
||||||
|
* @return 构造的折叠面板
|
||||||
|
*/
|
||||||
|
public TitledPane addGroup(String title, boolean isExpanded, ToggleButton... buttons) {
|
||||||
|
TitledPane pane = new TitledPane();
|
||||||
|
pane.setText(title);
|
||||||
|
return addGroup(pane, isExpanded, buttons);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加导航组
|
||||||
|
*
|
||||||
|
* @param pane 所属组
|
||||||
|
* @param isExpanded true 为默认展开
|
||||||
|
* @param buttons 导航项
|
||||||
|
* @return 原折叠面板
|
||||||
|
*/
|
||||||
|
public TitledPane addGroup(TitledPane pane, boolean isExpanded, ToggleButton... buttons) {
|
||||||
|
VBox content = new VBox();
|
||||||
|
content.setPadding(Insets.EMPTY);
|
||||||
|
|
||||||
|
pane.setContent(content);
|
||||||
|
pane.setExpanded(isExpanded);
|
||||||
|
pane.getStyleClass().add("group-pane");
|
||||||
|
|
||||||
|
for (int i = 0; i < buttons.length; i++) {
|
||||||
|
buttons[i].getProperties().put("OWNER", pane);
|
||||||
|
items.add(buttons[i]);
|
||||||
|
}
|
||||||
|
return pane;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取该按钮所属组
|
||||||
|
*
|
||||||
|
* @param btn 按钮
|
||||||
|
* @return 所属组,null 时为不属于任何组
|
||||||
|
*/
|
||||||
|
public TitledPane getGroup(ToggleButton btn) {
|
||||||
|
if (btn.getProperties().get("OWNER") instanceof TitledPane pane) {
|
||||||
|
return pane;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置当前激活导航项
|
||||||
|
*
|
||||||
|
* @param button 导航项
|
||||||
|
*/
|
||||||
|
public void setSelectedItem(ToggleButton button) {
|
||||||
|
selectedItem.set(button);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前激活的导航项
|
||||||
|
*
|
||||||
|
* @return 当前激活的导航项
|
||||||
|
*/
|
||||||
|
public ToggleButton getSelectedItem() {
|
||||||
|
return selectedItem.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取激活导航项监听
|
||||||
|
*
|
||||||
|
* @return 激活导航项监听
|
||||||
|
*/
|
||||||
|
public ObjectProperty<ToggleButton> selectedItem() {
|
||||||
|
return selectedItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取导航数据列表
|
||||||
|
*
|
||||||
|
* @return 导航数据列表
|
||||||
|
*/
|
||||||
|
public ObservableList<ToggleButton> getItems() {
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
}
|
||||||
152
src/main/java/com/imyeyu/fx/ui/components/NumberField.java
Normal file
152
src/main/java/com/imyeyu/fx/ui/components/NumberField.java
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
package com.imyeyu.fx.ui.components;
|
||||||
|
|
||||||
|
import com.imyeyu.java.TimiJava;
|
||||||
|
import com.imyeyu.utils.Calc;
|
||||||
|
import javafx.beans.property.DoubleProperty;
|
||||||
|
import javafx.beans.property.SimpleDoubleProperty;
|
||||||
|
import javafx.scene.control.TextField;
|
||||||
|
import javafx.scene.control.TextFormatter;
|
||||||
|
import javafx.scene.input.KeyCode;
|
||||||
|
import javafx.scene.input.KeyEvent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数字输入框
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2021-12-29 21:59
|
||||||
|
*/
|
||||||
|
public class NumberField extends TextField {
|
||||||
|
|
||||||
|
/** 数值 */
|
||||||
|
private final DoubleProperty value;
|
||||||
|
|
||||||
|
private boolean isBackSpace = false;
|
||||||
|
|
||||||
|
/** 默认构造 */
|
||||||
|
public NumberField() {
|
||||||
|
this("");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数字输入组件构造
|
||||||
|
*
|
||||||
|
* @param text 默认数值字符串
|
||||||
|
*/
|
||||||
|
public NumberField(String text) {
|
||||||
|
super(text);
|
||||||
|
|
||||||
|
value = new SimpleDoubleProperty();
|
||||||
|
value.addListener((obs, o, newValue) -> {
|
||||||
|
if (newValue.doubleValue() % 1 == 0) {
|
||||||
|
setText(String.valueOf(newValue.intValue()));
|
||||||
|
} else {
|
||||||
|
setText(String.valueOf(newValue.doubleValue()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
textProperty().addListener((obs, o, newText) -> {
|
||||||
|
if (Calc.isNumber(newText)) {
|
||||||
|
if (getDouble() % 1 == 0) {
|
||||||
|
value.set(getInt());
|
||||||
|
} else {
|
||||||
|
value.set(getDouble());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
addEventFilter(KeyEvent.KEY_PRESSED, e -> isBackSpace = e.getCode() == KeyCode.BACK_SPACE);
|
||||||
|
setTextFormatter(new TextFormatter<>(c -> {
|
||||||
|
String newText = c.getControlNewText();
|
||||||
|
if (!newText.equals("")) {
|
||||||
|
if (newText.equals("+") || newText.equals("-")) {
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
if (Calc.isNumber(newText)) {
|
||||||
|
if (isBackSpace && newText.endsWith(".")) {
|
||||||
|
c.setRange(c.getRangeStart() - 1, c.getRangeEnd());
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
return c;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置当前值
|
||||||
|
*
|
||||||
|
* @param number 当前值
|
||||||
|
*/
|
||||||
|
public void setValue(Number number) {
|
||||||
|
setText(String.valueOf(number));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取为双精度浮点值
|
||||||
|
*
|
||||||
|
* @return 双精度浮点值
|
||||||
|
*/
|
||||||
|
public double getDouble() {
|
||||||
|
return getValue().doubleValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取为长整值
|
||||||
|
*
|
||||||
|
* @return 长整值
|
||||||
|
*/
|
||||||
|
public long getLong() {
|
||||||
|
return getValue().longValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取为短整值
|
||||||
|
*
|
||||||
|
* @return 短整值
|
||||||
|
*/
|
||||||
|
public int getInt() {
|
||||||
|
return getValue().intValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取为单精度浮点值
|
||||||
|
*
|
||||||
|
* @return 单精度浮点值
|
||||||
|
*/
|
||||||
|
public float getFloat() {
|
||||||
|
return getValue().floatValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取数字对象
|
||||||
|
*
|
||||||
|
* @return 数字对象
|
||||||
|
*/
|
||||||
|
public Number getValue() {
|
||||||
|
if (TimiJava.isEmpty(getText())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Double.parseDouble(getText());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取数值属性
|
||||||
|
*
|
||||||
|
* @return 数值属性
|
||||||
|
*/
|
||||||
|
public DoubleProperty valueProperty() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置值
|
||||||
|
*
|
||||||
|
* @param value 值
|
||||||
|
*/
|
||||||
|
public void setValue(double value) {
|
||||||
|
this.value.set(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
package com.imyeyu.fx.ui.components;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import com.imyeyu.java.ref.Ref;
|
||||||
|
import javafx.scene.control.ProgressBar;
|
||||||
|
import javafx.scene.control.Skin;
|
||||||
|
import javafx.scene.control.Slider;
|
||||||
|
import javafx.scene.control.skin.SliderSkin;
|
||||||
|
import javafx.scene.layout.StackPane;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 带有进度的滑动选中,通常是媒体播放进度或音量进度(未对纵向滑动组件测试)
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2021-11-09 21:34
|
||||||
|
*/
|
||||||
|
public class ProgressSlider extends Slider implements TimiFXUI {
|
||||||
|
|
||||||
|
private static final String STYLE_CLASS = "progress-slider";
|
||||||
|
|
||||||
|
/** 默认构造,范围 [0, 1],默认值 0 */
|
||||||
|
public ProgressSlider() {
|
||||||
|
this(0, 1, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标准构造
|
||||||
|
*
|
||||||
|
* @param min 最小值
|
||||||
|
* @param max 最大值
|
||||||
|
* @param value 当前值
|
||||||
|
*/
|
||||||
|
public ProgressSlider(double min, double max, double value) {
|
||||||
|
super(min, max, value);
|
||||||
|
getStyleClass().add(STYLE_CLASS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Skin<?> createDefaultSkin() {
|
||||||
|
Skin<?> skin = super.createDefaultSkin();
|
||||||
|
if (skin instanceof SliderSkin) {
|
||||||
|
try {
|
||||||
|
StackPane track = Ref.getFieldValue(skin, "track", StackPane.class);
|
||||||
|
ProgressBar pb = new ProgressBar();
|
||||||
|
pb.progressProperty().bind(valueProperty().subtract(minProperty()).divide(maxProperty().subtract(minProperty())));
|
||||||
|
pb.prefWidthProperty().bind(track.widthProperty());
|
||||||
|
pb.setMouseTransparent(true);
|
||||||
|
track.getChildren().add(0, pb);
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return skin;
|
||||||
|
}
|
||||||
|
}
|
||||||
69
src/main/java/com/imyeyu/fx/ui/components/SVGIcon.java
Normal file
69
src/main/java/com/imyeyu/fx/ui/components/SVGIcon.java
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
package com.imyeyu.fx.ui.components;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import javafx.scene.paint.Paint;
|
||||||
|
import javafx.scene.shape.SVGPath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SVG 图标,主要简化 SVGPath 构造函数和可克隆({@link #renew()})
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2021-02-13 13:35
|
||||||
|
*/
|
||||||
|
public class SVGIcon extends SVGPath implements TimiFXUI, TimiFXUI.Colorful {
|
||||||
|
|
||||||
|
private final Paint color;
|
||||||
|
private final String path;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造 SVG 图标
|
||||||
|
*
|
||||||
|
* @param path SVG 路径
|
||||||
|
*/
|
||||||
|
public SVGIcon(String path) {
|
||||||
|
this(path, ICON);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造 SVG 图标
|
||||||
|
*
|
||||||
|
* @param path SVG 路径
|
||||||
|
* @param color 颜色
|
||||||
|
*/
|
||||||
|
public SVGIcon(String path, String color) {
|
||||||
|
this(path, Paint.valueOf(color));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造 SVG 图标
|
||||||
|
*
|
||||||
|
* @param color 颜色
|
||||||
|
* @param path SVG 路径
|
||||||
|
*/
|
||||||
|
public SVGIcon(Paint color, String path) {
|
||||||
|
this(path, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造 SVG 图标
|
||||||
|
*
|
||||||
|
* @param path SVG 路径
|
||||||
|
* @param color 颜色
|
||||||
|
*/
|
||||||
|
public SVGIcon(String path, Paint color) {
|
||||||
|
this.path = path;
|
||||||
|
this.color = color;
|
||||||
|
|
||||||
|
setFill(color);
|
||||||
|
setContent(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定克隆
|
||||||
|
*
|
||||||
|
* @return 克隆对象
|
||||||
|
*/
|
||||||
|
public SVGIcon renew() {
|
||||||
|
return new SVGIcon(path, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
172
src/main/java/com/imyeyu/fx/ui/components/SelectableLabel.java
Normal file
172
src/main/java/com/imyeyu/fx/ui/components/SelectableLabel.java
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
package com.imyeyu.fx.ui.components;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import com.imyeyu.java.ref.Ref;
|
||||||
|
import com.sun.javafx.scene.control.skin.Utils;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
|
import javafx.collections.ListChangeListener;
|
||||||
|
import javafx.scene.Cursor;
|
||||||
|
import javafx.scene.Group;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.ScrollPane;
|
||||||
|
import javafx.scene.control.Skin;
|
||||||
|
import javafx.scene.control.TextArea;
|
||||||
|
import javafx.scene.control.skin.TextAreaSkin;
|
||||||
|
import javafx.scene.layout.Region;
|
||||||
|
import javafx.scene.paint.Paint;
|
||||||
|
import javafx.scene.text.Text;
|
||||||
|
import javafx.scene.text.TextAlignment;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 可选中的标签组件,实际上是无样式文本域,此组件适应布局最大宽度
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-01-29 01:24
|
||||||
|
*/
|
||||||
|
public class SelectableLabel extends TextArea implements TimiFXUI, TimiFXUI.Colorful {
|
||||||
|
|
||||||
|
private static final String STYLE_CLASS = "selectable-label";
|
||||||
|
|
||||||
|
private final ObjectProperty<Paint> textFillProperty;
|
||||||
|
private final ObjectProperty<TextAlignment> textAlignmentProperty;
|
||||||
|
|
||||||
|
/** 默认构造 */
|
||||||
|
public SelectableLabel() {
|
||||||
|
this("");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造器
|
||||||
|
*
|
||||||
|
* @param text 文本内容
|
||||||
|
*/
|
||||||
|
public SelectableLabel(String text) {
|
||||||
|
super(text);
|
||||||
|
|
||||||
|
textFillProperty = new SimpleObjectProperty<>(BLACK);
|
||||||
|
textAlignmentProperty = new SimpleObjectProperty<>(TextAlignment.LEFT);
|
||||||
|
|
||||||
|
setCursor(Cursor.TEXT);
|
||||||
|
setEditable(false);
|
||||||
|
setWrapText(true);
|
||||||
|
setMaxWidth(Double.MAX_VALUE);
|
||||||
|
setMinSize(Double.MIN_VALUE, Double.MIN_VALUE);
|
||||||
|
setPrefHeight(0);
|
||||||
|
setPrefSize(0, 0);
|
||||||
|
getStyleClass().setAll(STYLE_CLASS, CSS.MINECRAFT);
|
||||||
|
|
||||||
|
focusedProperty().addListener((obs, o, isFocused) -> {
|
||||||
|
if (!isFocused) {
|
||||||
|
deselect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Skin<?> createDefaultSkin() {
|
||||||
|
Skin<?> defaultSkin = super.createDefaultSkin();
|
||||||
|
if (defaultSkin instanceof TextAreaSkin skin) {
|
||||||
|
try {
|
||||||
|
ScrollPane sp = Ref.getFieldValue(skin, "scrollPane", ScrollPane.class);
|
||||||
|
sp.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||||
|
if (sp.getContent() instanceof Region region) {
|
||||||
|
region.heightProperty().addListener((obs, o, newHeight) -> Platform.runLater(() -> {
|
||||||
|
// 需要 runLater,因为是 Region 适应 Skin 变化
|
||||||
|
setPrefHeight(newHeight.doubleValue());
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
Group paragraphNodes = Ref.getFieldValue(skin, "paragraphNodes", Group.class);
|
||||||
|
paragraphNodes.getChildren().addListener((ListChangeListener<Node>) c -> {
|
||||||
|
if (c.next()) {
|
||||||
|
if (c.wasAdded()) {
|
||||||
|
bindTextStyle(c.getAddedSubList());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
bindTextStyle(paragraphNodes.getChildren());
|
||||||
|
widthProperty().addListener((obs, oldValue, newValue) -> {
|
||||||
|
if (oldValue.doubleValue() < newValue.doubleValue()) {
|
||||||
|
if (paragraphNodes.getChildren().get(0) instanceof Text text) {
|
||||||
|
setPrefHeight(Utils.computeTextHeight(getFont(), getText(), getWidth(), text.getBoundsType()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultSkin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定文本样式
|
||||||
|
*
|
||||||
|
* @param list 文本节点,必须是 {@link Text}
|
||||||
|
*/
|
||||||
|
private void bindTextStyle(List<? extends Node> list) {
|
||||||
|
for (int i = 0; i < list.size(); i++) {
|
||||||
|
if (list.get(i) instanceof Text text) {
|
||||||
|
text.fillProperty().bind(textFillProperty);
|
||||||
|
text.textAlignmentProperty().bind(textAlignmentProperty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置字体颜色
|
||||||
|
*
|
||||||
|
* @param textFill 字体颜色
|
||||||
|
*/
|
||||||
|
public void setTextFill(Paint textFill) {
|
||||||
|
this.textFillProperty.set(textFill);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取字体颜色
|
||||||
|
*
|
||||||
|
* @return 字体颜色
|
||||||
|
*/
|
||||||
|
public Paint getTextFill() {
|
||||||
|
return textFillProperty.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取字体颜色绑定
|
||||||
|
*
|
||||||
|
* @return 字体颜色绑定
|
||||||
|
*/
|
||||||
|
public ObjectProperty<Paint> textFillProperty() {
|
||||||
|
return textFillProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置文本对齐方式
|
||||||
|
*
|
||||||
|
* @param textAlignment 文本对齐方式
|
||||||
|
*/
|
||||||
|
public void setTextAlignment(TextAlignment textAlignment) {
|
||||||
|
this.textAlignmentProperty.set(textAlignment);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文本对齐方式
|
||||||
|
*
|
||||||
|
* @return 文本对齐方式
|
||||||
|
*/
|
||||||
|
public TextAlignment getTextAlignment() {
|
||||||
|
return textAlignmentProperty.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文本对齐方式绑定
|
||||||
|
*
|
||||||
|
* @return 文本对齐方式绑定
|
||||||
|
*/
|
||||||
|
public ObjectProperty<TextAlignment> textAlignmentProperty() {
|
||||||
|
return textAlignmentProperty;
|
||||||
|
}
|
||||||
|
}
|
||||||
773
src/main/java/com/imyeyu/fx/ui/components/TextAreaEditor.java
Normal file
773
src/main/java/com/imyeyu/fx/ui/components/TextAreaEditor.java
Normal file
@ -0,0 +1,773 @@
|
|||||||
|
package com.imyeyu.fx.ui.components;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.icon.TimiFXIcon;
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import com.imyeyu.fx.ui.components.popup.PopupTipsService;
|
||||||
|
import com.imyeyu.fx.utils.Anchor;
|
||||||
|
import com.imyeyu.fx.utils.Column;
|
||||||
|
import com.imyeyu.fx.utils.SmoothScroll;
|
||||||
|
import com.imyeyu.java.TimiJava;
|
||||||
|
import com.imyeyu.java.bean.Callback;
|
||||||
|
import com.imyeyu.java.ref.Ref;
|
||||||
|
import com.sun.javafx.scene.control.skin.Utils;
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.beans.property.BooleanProperty;
|
||||||
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.collections.ListChangeListener;
|
||||||
|
import javafx.collections.ObservableMap;
|
||||||
|
import javafx.event.Event;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.geometry.NodeOrientation;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.Cursor;
|
||||||
|
import javafx.scene.Group;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.IndexRange;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.ScrollBar;
|
||||||
|
import javafx.scene.control.ScrollPane;
|
||||||
|
import javafx.scene.control.Skin;
|
||||||
|
import javafx.scene.control.TextArea;
|
||||||
|
import javafx.scene.control.TextField;
|
||||||
|
import javafx.scene.control.TextInputControl;
|
||||||
|
import javafx.scene.control.skin.TextAreaSkin;
|
||||||
|
import javafx.scene.control.skin.TextFieldSkin;
|
||||||
|
import javafx.scene.control.skin.TextInputControlSkin;
|
||||||
|
import javafx.scene.input.ContextMenuEvent;
|
||||||
|
import javafx.scene.input.KeyCode;
|
||||||
|
import javafx.scene.input.KeyEvent;
|
||||||
|
import javafx.scene.input.MouseEvent;
|
||||||
|
import javafx.scene.layout.AnchorPane;
|
||||||
|
import javafx.scene.layout.BorderPane;
|
||||||
|
import javafx.scene.layout.GridPane;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.scene.layout.Pane;
|
||||||
|
import javafx.scene.layout.StackPane;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
import javafx.scene.shape.Path;
|
||||||
|
import javafx.scene.text.Text;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 复杂文本域编辑器,显示按钮操作文本域,文本域显示行号,可自定义操作功能
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-07-11 15:47
|
||||||
|
*/
|
||||||
|
public class TextAreaEditor extends TextArea implements TimiFXUI {
|
||||||
|
|
||||||
|
private static final String STYLE_CLASS = "text-area-editor";
|
||||||
|
|
||||||
|
/** 主要控制区,此面板在 {@link #header} 的中部 */
|
||||||
|
protected final HBox ctrl;
|
||||||
|
|
||||||
|
/** 顶部控制区 */
|
||||||
|
protected final BorderPane header;
|
||||||
|
|
||||||
|
/** 撤销按钮 */
|
||||||
|
protected final IconButton undo;
|
||||||
|
|
||||||
|
/** 重做按钮 */
|
||||||
|
protected final IconButton redo;
|
||||||
|
|
||||||
|
/** 复制按钮 */
|
||||||
|
protected final IconButton copy;
|
||||||
|
|
||||||
|
/** 剪切按钮 */
|
||||||
|
protected final IconButton cut;
|
||||||
|
|
||||||
|
/** 粘贴按钮 */
|
||||||
|
protected final IconButton paste;
|
||||||
|
|
||||||
|
/** 换行按钮 */
|
||||||
|
protected final ToggleIcon wrap;
|
||||||
|
|
||||||
|
/** 显示行号 */
|
||||||
|
protected final BooleanProperty showLineNumber;
|
||||||
|
|
||||||
|
/** 显示查找面板 */
|
||||||
|
protected BooleanProperty visibleFindPaneProperty;
|
||||||
|
|
||||||
|
/** 发生换行的文本节点 Map<行号, 换行次数> */
|
||||||
|
private final Map<Integer, Integer> wraps;
|
||||||
|
|
||||||
|
/** 行号组件 */
|
||||||
|
private LineNumber lineNumber;
|
||||||
|
|
||||||
|
/** 根布局 */
|
||||||
|
private final BorderPane root;
|
||||||
|
|
||||||
|
/** 默认构造 */
|
||||||
|
public TextAreaEditor() {
|
||||||
|
wraps = new HashMap<>();
|
||||||
|
showLineNumber = new SimpleBooleanProperty(true);
|
||||||
|
|
||||||
|
// 撤销
|
||||||
|
undo = new IconButton(TimiFXIcon.fromName("ARROW_0_W")).withBackground(CSS.BORDER_R);
|
||||||
|
undo.disableProperty().bind(undoableProperty().not());
|
||||||
|
PopupTipsService.installText(undo, TimiFXUI.MULTILINGUAL.text("undo"));
|
||||||
|
|
||||||
|
// 重做
|
||||||
|
redo = new IconButton(TimiFXIcon.fromName("ARROW_0_E")).withBackground(CSS.BORDER_R);
|
||||||
|
redo.disableProperty().bind(redoableProperty().not());
|
||||||
|
PopupTipsService.installText(redo, TimiFXUI.MULTILINGUAL.text("redo"));
|
||||||
|
|
||||||
|
// 复制
|
||||||
|
copy = new IconButton(TimiFXIcon.fromName("COPY")).withBackground(CSS.BORDER_R);
|
||||||
|
PopupTipsService.installText(copy, TimiFXUI.MULTILINGUAL.text("copy"));
|
||||||
|
|
||||||
|
// 剪切
|
||||||
|
cut = new IconButton(TimiFXIcon.fromName("CUT")).withBackground(CSS.BORDER_R);
|
||||||
|
cut.disableProperty().bind(editableProperty().not());
|
||||||
|
PopupTipsService.installText(cut, TimiFXUI.MULTILINGUAL.text("cut"));
|
||||||
|
|
||||||
|
// 粘贴
|
||||||
|
paste = new IconButton(TimiFXIcon.fromName("PASTE")).withBackground(CSS.BORDER_R);
|
||||||
|
paste.disableProperty().bind(editableProperty().not());
|
||||||
|
PopupTipsService.installText(paste, TimiFXUI.MULTILINGUAL.text("paste"));
|
||||||
|
|
||||||
|
// 换行
|
||||||
|
wrap = new ToggleIcon(TimiFXIcon.fromName("WRAP"));
|
||||||
|
wrap.setBorder(Stroke.RIGHT);
|
||||||
|
PopupTipsService.installText(wrap, TimiFXUI.MULTILINGUAL.text("wrap"));
|
||||||
|
|
||||||
|
ctrl = new HBox();
|
||||||
|
ctrl.setPickOnBounds(false);
|
||||||
|
ctrl.getChildren().addAll(undo, redo, copy, cut, paste, wrap);
|
||||||
|
|
||||||
|
header = new BorderPane();
|
||||||
|
header.setCenter(ctrl);
|
||||||
|
header.setPickOnBounds(false);
|
||||||
|
|
||||||
|
// 顶部背景(阻止触发文本域选择)
|
||||||
|
Button headerBackground = new Button(" ");
|
||||||
|
headerBackground.getStyleClass().addAll(CSS.BORDER_N, CSS.BG_TP);
|
||||||
|
headerBackground.setBackground(BG.DEFAULT);
|
||||||
|
|
||||||
|
AnchorPane headerPane = new AnchorPane();
|
||||||
|
Anchor.def(header);
|
||||||
|
Anchor.def(headerBackground);
|
||||||
|
headerPane.getChildren().setAll(headerBackground, header);
|
||||||
|
headerPane.setBorder(Stroke.BOTTOM);
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
FindPane findPane = new FindPane(this);
|
||||||
|
visibleFindPaneProperty = findPane.visibleProperty();
|
||||||
|
findPane.managedProperty().bind(visibleFindPaneProperty);
|
||||||
|
findPane.setVisible(false);
|
||||||
|
|
||||||
|
root = new BorderPane();
|
||||||
|
root.setTop(new VBox() {{
|
||||||
|
getChildren().addAll(headerPane, findPane);
|
||||||
|
}});
|
||||||
|
|
||||||
|
getStyleClass().add(STYLE_CLASS);
|
||||||
|
|
||||||
|
// ---------- 事件 ----------
|
||||||
|
|
||||||
|
undo.setOnAction(e -> undo());
|
||||||
|
redo.setOnAction(e -> redo());
|
||||||
|
copy.setOnAction(e -> copy());
|
||||||
|
cut.setOnAction(e -> cut());
|
||||||
|
paste.setOnAction(e -> paste());
|
||||||
|
wrapTextProperty().bindBidirectional(wrap.selectedProperty());
|
||||||
|
|
||||||
|
// 键盘事件
|
||||||
|
addEventFilter(KeyEvent.KEY_RELEASED, e -> {
|
||||||
|
boolean control = e.isControlDown();
|
||||||
|
boolean shift = e.isShiftDown();
|
||||||
|
boolean alt = e.isAltDown();
|
||||||
|
KeyCode code = e.getCode();
|
||||||
|
|
||||||
|
if (control && shift && !alt) {
|
||||||
|
switch (code) {
|
||||||
|
case ENTER -> {
|
||||||
|
// 向上开新行
|
||||||
|
int start = getText().lastIndexOf("\n", getCaretPosition() - 1);
|
||||||
|
if (start == -1) {
|
||||||
|
insertText(0, "\n");
|
||||||
|
positionCaret(0);
|
||||||
|
} else {
|
||||||
|
insertText(start, "\n");
|
||||||
|
positionCaret(start + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case U -> {
|
||||||
|
// 切换大小写
|
||||||
|
selectPreviousWord();
|
||||||
|
positionCaret(getSelection().getStart());
|
||||||
|
selectEndOfNextWord();
|
||||||
|
|
||||||
|
IndexRange range = getSelection();
|
||||||
|
String text = getSelectedText();
|
||||||
|
if (65 <= text.charAt(0) && text.charAt(0) <= 90) {
|
||||||
|
// 当前大小
|
||||||
|
replaceSelection(text.toLowerCase());
|
||||||
|
} else {
|
||||||
|
// 当前小写
|
||||||
|
replaceSelection(text.toUpperCase());
|
||||||
|
}
|
||||||
|
selectRange(range.getStart(), range.getEnd());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (control && !shift && !alt) {
|
||||||
|
switch (code) {
|
||||||
|
case F -> {
|
||||||
|
// 打开查找面板
|
||||||
|
findPane.setVisible(!findPane.isVisible());
|
||||||
|
findPane.keyword.setText(getSelectedText());
|
||||||
|
}
|
||||||
|
case D -> {
|
||||||
|
// 删除聚焦行
|
||||||
|
int start = getText().lastIndexOf("\n", getCaretPosition() - 1);
|
||||||
|
start = Math.max(start, 0);
|
||||||
|
int end = getText().indexOf("\n", getCaretPosition());
|
||||||
|
end = end < 0 ? getText().length() : end;
|
||||||
|
|
||||||
|
deleteText(start, end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!control && shift && !alt && code == KeyCode.ENTER) {
|
||||||
|
// 向下开新行
|
||||||
|
int end = getText().indexOf("\n", getCaretPosition());
|
||||||
|
end = end < 0 ? getText().length() : end;
|
||||||
|
|
||||||
|
insertText(end, "\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!control && !shift && !alt && code == KeyCode.ESCAPE) {
|
||||||
|
// 隐藏查找面板
|
||||||
|
findPane.setVisible(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Skin<?> createDefaultSkin() {
|
||||||
|
TextAreaEditSkin skin = new TextAreaEditSkin(this);
|
||||||
|
try {
|
||||||
|
// 嵌入面板
|
||||||
|
ScrollPane scrollPane = Ref.getClassFieldValue(skin, TextAreaSkin.class, "scrollPane", ScrollPane.class);
|
||||||
|
scrollPane.getStyleClass().add(CSS.SP_BORDER);
|
||||||
|
// 锐化光标
|
||||||
|
Path caret = Ref.getClassFieldValue(skin, TextInputControlSkin.class, "caretPath", Path.class);
|
||||||
|
caret.setSmooth(false);
|
||||||
|
// 插入行号
|
||||||
|
double fontSize = skin.getSkinnable().getFont().getSize() + 2;
|
||||||
|
lineNumber = new LineNumber(scrollPane, fontSize);
|
||||||
|
lineNumber.visibleProperty().bind(showLineNumber);
|
||||||
|
lineNumber.managedProperty().bind(showLineNumber);
|
||||||
|
|
||||||
|
// 内容监听,计算行号
|
||||||
|
Group paragraphNodes = Ref.getClassFieldValue(skin, TextAreaSkin.class, "paragraphNodes", Group.class);
|
||||||
|
Callback lineNumberParser = () -> {
|
||||||
|
wraps.clear();
|
||||||
|
if (isWrapText() && paragraphNodes.getChildren().get(0) instanceof Text text) {
|
||||||
|
// 计算段落被渲染换行数
|
||||||
|
double wrappingWidth = scrollPane.getWidth() - 12;
|
||||||
|
for (int i = 0, l = getParagraphs().size(); i < l; i++) {
|
||||||
|
int wrap = (int) (Utils.computeTextHeight(getFont(), getParagraphs().get(i).toString(), wrappingWidth, text.getBoundsType()) / fontSize);
|
||||||
|
if (wrap != 1) {
|
||||||
|
wraps.put(i, wrap - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lineNumber.render(getParagraphs().size());
|
||||||
|
};
|
||||||
|
getParagraphs().addListener((ListChangeListener<CharSequence>) c -> {
|
||||||
|
if (c.next()) {
|
||||||
|
lineNumberParser.handler();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
wrapTextProperty().addListener((obs, o, n) -> lineNumberParser.handler());
|
||||||
|
widthProperty().addListener((obs, o, n) -> lineNumberParser.handler());
|
||||||
|
// 适应底部滚动条
|
||||||
|
scrollPane.skinProperty().addListener((obs, o, spSkin) -> {
|
||||||
|
try {
|
||||||
|
ScrollBar hsb = Ref.getFieldValue(spSkin, "hsb", ScrollBar.class);
|
||||||
|
lineNumber.scrollPane.paddingProperty().bind(Bindings.createObjectBinding(() -> {
|
||||||
|
if (hsb.isVisible()) {
|
||||||
|
double height = Ref.getFieldValue(spSkin, "hsbHeight", Double.class);
|
||||||
|
return new Insets(0, 0, height, 0);
|
||||||
|
} else {
|
||||||
|
return Insets.EMPTY;
|
||||||
|
}
|
||||||
|
}, hsb.visibleProperty()));
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 平滑滚动
|
||||||
|
SmoothScroll.scrollPaneV(scrollPane);
|
||||||
|
|
||||||
|
root.setLeft(lineNumber);
|
||||||
|
root.setCenter(scrollPane);
|
||||||
|
skin.getChildren().setAll(root);
|
||||||
|
|
||||||
|
lineNumber.render(getParagraphs().size());
|
||||||
|
} catch (NoSuchFieldException | IllegalAccessException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
return skin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取控制区面板,此面板在 {@link #getHeader()} 的中部
|
||||||
|
*
|
||||||
|
* @return 控制区面板
|
||||||
|
*/
|
||||||
|
public HBox getCtrl() {
|
||||||
|
return ctrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取顶部控制区面板
|
||||||
|
*
|
||||||
|
* @return 顶部控制区面板
|
||||||
|
*/
|
||||||
|
public BorderPane getHeader() {
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取撤销按钮
|
||||||
|
*
|
||||||
|
* @return 撤销按钮
|
||||||
|
*/
|
||||||
|
public IconButton getUndo() {
|
||||||
|
return undo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取重做按钮
|
||||||
|
*
|
||||||
|
* @return 重做按钮
|
||||||
|
*/
|
||||||
|
public IconButton getRedo() {
|
||||||
|
return redo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取复制按钮
|
||||||
|
*
|
||||||
|
* @return 复制按钮
|
||||||
|
*/
|
||||||
|
public IconButton getCopy() {
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取剪切按钮
|
||||||
|
*
|
||||||
|
* @return 剪切按钮
|
||||||
|
*/
|
||||||
|
public IconButton getCut() {
|
||||||
|
return cut;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取粘贴按钮
|
||||||
|
*
|
||||||
|
* @return 粘贴按钮
|
||||||
|
*/
|
||||||
|
public IconButton getPaste() {
|
||||||
|
return paste;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取换行按钮
|
||||||
|
*
|
||||||
|
* @return 换行按钮
|
||||||
|
*/
|
||||||
|
public ToggleIcon getWrap() {
|
||||||
|
return wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取是否显示行号
|
||||||
|
*
|
||||||
|
* @return true 为显示行号
|
||||||
|
*/
|
||||||
|
public boolean isShowLineNumber() {
|
||||||
|
return showLineNumber.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置是否显示行号
|
||||||
|
*
|
||||||
|
* @param showLineNumber true 为显示行号
|
||||||
|
*/
|
||||||
|
public void setShowLineNumber(boolean showLineNumber) {
|
||||||
|
this.showLineNumber.set(showLineNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取是否显示行号监听
|
||||||
|
*
|
||||||
|
* @return 显示行号监听
|
||||||
|
*/
|
||||||
|
public BooleanProperty showLineNumberProperty() {
|
||||||
|
return showLineNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取是否显示查找面板
|
||||||
|
*
|
||||||
|
* @return true 为显示查找面板
|
||||||
|
*/
|
||||||
|
public boolean isVisibleFindPane() {
|
||||||
|
return visibleFindPaneProperty.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置是否显示查找面板
|
||||||
|
*
|
||||||
|
* @param visibleFindPane true 为显示查找面板
|
||||||
|
*/
|
||||||
|
public void setVisibleFindPane(boolean visibleFindPane) {
|
||||||
|
this.visibleFindPaneProperty.set(visibleFindPane);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取是否显示查找面板监听
|
||||||
|
*
|
||||||
|
* @return 显示查找面板监听
|
||||||
|
*/
|
||||||
|
public BooleanProperty visibleFindPaneProperty() {
|
||||||
|
return visibleFindPaneProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字符串查找面板
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-08-26 18:01
|
||||||
|
*/
|
||||||
|
private class FindPane extends GridPane {
|
||||||
|
|
||||||
|
final Callback fetch;
|
||||||
|
final Label result;
|
||||||
|
final TextField keyword;
|
||||||
|
|
||||||
|
// Map<下标, 起始选择>
|
||||||
|
private final ObservableMap<Integer, Integer> selects;
|
||||||
|
|
||||||
|
// 当前查找结果选中下标
|
||||||
|
private int nearestI;
|
||||||
|
|
||||||
|
private ToggleIcon toggleCase;
|
||||||
|
|
||||||
|
public FindPane(TextInputControl inputControl) {
|
||||||
|
selects = FXCollections.observableHashMap();
|
||||||
|
|
||||||
|
// 查找
|
||||||
|
Text icon = TimiFXIcon.fromName("MAGNIFIER");
|
||||||
|
keyword = new TextField() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Skin<?> createDefaultSkin() {
|
||||||
|
Skin<?> skin = super.createDefaultSkin();
|
||||||
|
if (skin instanceof TextFieldSkin textFieldSkin) {
|
||||||
|
try {
|
||||||
|
|
||||||
|
// 输入
|
||||||
|
Pane textGroup = Ref.getFieldValue(textFieldSkin, "textGroup", Pane.class);
|
||||||
|
textGroup.setTranslateY(-.5);
|
||||||
|
|
||||||
|
// 匹配大小写
|
||||||
|
toggleCase = new ToggleIcon(TimiFXIcon.fromName("FONTSIZE"));
|
||||||
|
toggleCase.setBorder(Stroke.LEFT);
|
||||||
|
toggleCase.setCursor(Cursor.DEFAULT);
|
||||||
|
toggleCase.setFocusTraversable(false);
|
||||||
|
toggleCase.selectedProperty().addListener((obs, o, n) -> fetch.handler());
|
||||||
|
|
||||||
|
BorderPane root = new BorderPane();
|
||||||
|
root.setCenter(textGroup);
|
||||||
|
root.setRight(new HBox(toggleCase));
|
||||||
|
|
||||||
|
textFieldSkin.getChildren().setAll(root);
|
||||||
|
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return skin;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
keyword.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
|
||||||
|
keyword.getStyleClass().addAll("find-field", CSS.BORDER_R, CSS.PADDING_N);
|
||||||
|
|
||||||
|
// 查找结果
|
||||||
|
result = TimiFXUI.label(" ");
|
||||||
|
result.setPadding(new Insets(0, 4, 0, 4));
|
||||||
|
IconButton prev = new IconButton(TimiFXIcon.fromName("ARROW_0_N")).withBackground();
|
||||||
|
prev.getStyleClass().add(CSS.BORDER_R);
|
||||||
|
prev.setFocusTraversable(false);
|
||||||
|
IconButton next = new IconButton(TimiFXIcon.fromName("ARROW_0_S")).withBackground();
|
||||||
|
next.getStyleClass().add(CSS.BORDER_R);
|
||||||
|
next.setFocusTraversable(false);
|
||||||
|
|
||||||
|
// 关闭
|
||||||
|
IconButton close = new IconButton(TimiFXIcon.fromName("FAIL")).withBackground();
|
||||||
|
close.getStyleClass().add(CSS.BORDER_L);
|
||||||
|
close.setFocusTraversable(false);
|
||||||
|
|
||||||
|
// 替换值
|
||||||
|
TextField replaceValue = new TextField();
|
||||||
|
replaceValue.getStyleClass().addAll("replace-field", CSS.BORDER_TR);
|
||||||
|
replaceValue.disableProperty().bind(editableProperty().not());
|
||||||
|
Button replace = new Button(TimiFXUI.MULTILINGUAL.text("replace"));
|
||||||
|
replace.getStyleClass().add(CSS.BORDER_R);
|
||||||
|
replace.setFocusTraversable(false);
|
||||||
|
replace.disableProperty().bind(editableProperty().not().or(replaceValue.textProperty().isEmpty().or(Bindings.isEmpty(selects))));
|
||||||
|
replace.addEventFilter(MouseEvent.MOUSE_DRAGGED, e -> {
|
||||||
|
if (replaceValue.isDisabled()) {
|
||||||
|
e.consume();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Button replaceAll = new Button(TimiFXUI.MULTILINGUAL.text("replace_all"));
|
||||||
|
replaceAll.getStyleClass().add(CSS.BORDER_R);
|
||||||
|
replaceAll.setFocusTraversable(false);
|
||||||
|
replaceAll.disableProperty().bind(replace.disabledProperty());
|
||||||
|
|
||||||
|
getStyleClass().add("find-pane");
|
||||||
|
getColumnConstraints().addAll(Column.build(), Column.build().width(260), Column.VALUE_FILL);
|
||||||
|
setBorder(Stroke.BOTTOM);
|
||||||
|
setBackground(BG.DEFAULT);
|
||||||
|
|
||||||
|
addRow(0, new StackPane() {{
|
||||||
|
setPadding(new Insets(0, 2, 0, 2));
|
||||||
|
setBackground(BG.WHITE);
|
||||||
|
getChildren().add(icon);
|
||||||
|
}}, keyword, new BorderPane() {{
|
||||||
|
setCenter(new HBox() {{
|
||||||
|
setAlignment(Pos.CENTER_LEFT);
|
||||||
|
getChildren().addAll(prev, next, result);
|
||||||
|
}});
|
||||||
|
setRight(close);
|
||||||
|
}});
|
||||||
|
|
||||||
|
add(replaceValue, 0, 1, 2, 1);
|
||||||
|
addRow(1, new HBox() {{
|
||||||
|
setBorder(Stroke.TOP);
|
||||||
|
getChildren().addAll(replace, replaceAll);
|
||||||
|
}});
|
||||||
|
|
||||||
|
// ---------- 事件 ----------
|
||||||
|
|
||||||
|
// 忽略拖拽
|
||||||
|
addEventFilter(MouseEvent.MOUSE_DRAGGED, e -> {
|
||||||
|
if (!keyword.isFocused() && !replaceValue.isFocused()) {
|
||||||
|
e.consume();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 刷新搜索结果
|
||||||
|
Callback updateResult = () -> {
|
||||||
|
if (TimiJava.isEmpty(selects) || nearestI == -1) {
|
||||||
|
result.setText(" ");
|
||||||
|
} else {
|
||||||
|
inputControl.selectRange(selects.get(nearestI), selects.get(nearestI) + keyword.getLength());
|
||||||
|
result.setText("%s / %s".formatted(nearestI + 1, selects.size()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
fetch = () -> {
|
||||||
|
inputControl.deselect();
|
||||||
|
selects.clear();
|
||||||
|
if (TimiJava.isEmpty(keyword.getText())) {
|
||||||
|
result.setText(" ");
|
||||||
|
} else {
|
||||||
|
String text, keywordValue;
|
||||||
|
if (toggleCase.isSelected()) {
|
||||||
|
text = inputControl.getText();
|
||||||
|
keywordValue = keyword.getText();
|
||||||
|
} else {
|
||||||
|
text = inputControl.getText().toLowerCase();
|
||||||
|
keywordValue = keyword.getText().toLowerCase();
|
||||||
|
}
|
||||||
|
int keywordLength = keywordValue.length();
|
||||||
|
|
||||||
|
nearestI = -1;
|
||||||
|
boolean isFoundNearestI = false;
|
||||||
|
for (int i = 0, l = text.length(); i < l; i += keywordLength) {
|
||||||
|
i = text.indexOf(keywordValue, i);
|
||||||
|
if (i == -1) {
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
selects.put(selects.size(), i);
|
||||||
|
if (!isFoundNearestI && inputControl.getCaretPosition() < i) {
|
||||||
|
nearestI = selects.size() - 1;
|
||||||
|
isFoundNearestI = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isFoundNearestI && TimiJava.isNotEmpty(selects)) {
|
||||||
|
nearestI = 0;
|
||||||
|
}
|
||||||
|
updateResult.handler();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 查找
|
||||||
|
keyword.textProperty().addListener((obs, o, newKeyword) -> fetch.handler());
|
||||||
|
|
||||||
|
// 更新查找
|
||||||
|
inputControl.textProperty().addListener((obs, o, n) -> {
|
||||||
|
if (TimiJava.isNotEmpty(keyword.getText()) && isVisible()) {
|
||||||
|
fetch.handler();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 选中上一个
|
||||||
|
prev.setOnAction(e -> {
|
||||||
|
if (TimiJava.isNotEmpty(selects)) {
|
||||||
|
nearestI--;
|
||||||
|
if (nearestI < 0) {
|
||||||
|
nearestI = selects.size() - 1;
|
||||||
|
}
|
||||||
|
updateResult.handler();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 选中下一个
|
||||||
|
next.setOnAction(e -> {
|
||||||
|
if (TimiJava.isNotEmpty(selects)) {
|
||||||
|
nearestI++;
|
||||||
|
if (selects.size() - 1 < nearestI) {
|
||||||
|
nearestI = 0;
|
||||||
|
}
|
||||||
|
updateResult.handler();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 替换
|
||||||
|
replace.setOnAction(e -> {
|
||||||
|
if (inputControl.getSelectedText().equals(keyword.getText())) {
|
||||||
|
inputControl.replaceSelection(replaceValue.getText());
|
||||||
|
} else {
|
||||||
|
int i = inputControl.getText().indexOf(keyword.getText(), inputControl.getCaretPosition());
|
||||||
|
if (i == -1) {
|
||||||
|
i = inputControl.getText().indexOf(keyword.getText());
|
||||||
|
}
|
||||||
|
if (i != -1) {
|
||||||
|
inputControl.replaceText(i, i + keyword.getLength(), replaceValue.getText());
|
||||||
|
}
|
||||||
|
inputControl.positionCaret(i + replaceValue.getLength());
|
||||||
|
}
|
||||||
|
fetch.handler();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 替换全部
|
||||||
|
replaceAll.setOnAction(e -> {
|
||||||
|
inputControl.setText(inputControl.getText().replace(keyword.getText(), replaceValue.getText()));
|
||||||
|
fetch.handler();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 关闭
|
||||||
|
close.setOnAction(e -> setVisible(false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 行号文本域
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-07-12 13:05
|
||||||
|
*/
|
||||||
|
private class LineNumber extends TextArea {
|
||||||
|
|
||||||
|
/** 行号滚动面板 */
|
||||||
|
private ScrollPane scrollPane;
|
||||||
|
|
||||||
|
/** 字号 */
|
||||||
|
private final double fontSize;
|
||||||
|
|
||||||
|
/** 所属文本域滚动面板 */
|
||||||
|
private final ScrollPane ownerScrollPane;
|
||||||
|
|
||||||
|
public LineNumber(ScrollPane ownerScrollPane, double fontSize) {
|
||||||
|
this.fontSize = fontSize;
|
||||||
|
this.ownerScrollPane = ownerScrollPane;
|
||||||
|
this.ownerScrollPane.vvalueProperty().addListener((obs, o, newV) -> {
|
||||||
|
// 不可单向绑定,TextArea 内部需要调度 setVvalue,也不可双向绑定,会造成滚动错位
|
||||||
|
scrollPane.setVvalue(newV.doubleValue());
|
||||||
|
});
|
||||||
|
getStyleClass().add(CSS.BORDER_L);
|
||||||
|
setEditable(false);
|
||||||
|
setNodeOrientation(NodeOrientation.RIGHT_TO_LEFT);
|
||||||
|
addEventFilter(MouseEvent.ANY, Event::consume);
|
||||||
|
addEventFilter(ContextMenuEvent.CONTEXT_MENU_REQUESTED, Event::consume);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 行号渲染
|
||||||
|
*
|
||||||
|
* @param size 行数
|
||||||
|
*/
|
||||||
|
private void render(int size) {
|
||||||
|
clear();
|
||||||
|
for (int i = 0; i < size; i++) {
|
||||||
|
appendText(String.valueOf(i + 1));
|
||||||
|
if (wraps.containsKey(i)) {
|
||||||
|
// 发生换行
|
||||||
|
appendText("\n".repeat(Math.max(0, wraps.get(i))));
|
||||||
|
}
|
||||||
|
if (i < size - 1) {
|
||||||
|
appendText("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 行号宽度
|
||||||
|
int lineL = 0;
|
||||||
|
for (long i = size; i != 0; i *= .01) {
|
||||||
|
lineL++;
|
||||||
|
}
|
||||||
|
setPrefWidth(lineL * fontSize + 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Skin<?> createDefaultSkin() {
|
||||||
|
Skin<?> skin = super.createDefaultSkin();
|
||||||
|
try {
|
||||||
|
// 同步滚动
|
||||||
|
scrollPane = Ref.getClassFieldValue(skin, TextAreaSkin.class, "scrollPane", ScrollPane.class);
|
||||||
|
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||||
|
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||||
|
scrollPane.vvalueProperty().addListener((obs, o, n) -> scrollPane.setVvalue(ownerScrollPane.getVvalue()));
|
||||||
|
scrollPane.getContent().setCursor(Cursor.DEFAULT);
|
||||||
|
|
||||||
|
SmoothScroll.scrollPaneV(scrollPane);
|
||||||
|
} catch (NoSuchFieldException | IllegalAccessException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
return skin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重写样式 layoutChildren,让滚动面板适应本组件的功能组件注入
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-07-11 16:56
|
||||||
|
*/
|
||||||
|
private static class TextAreaEditSkin extends TextAreaSkin {
|
||||||
|
|
||||||
|
public TextAreaEditSkin(TextAreaEditor control) {
|
||||||
|
super(control);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void layoutChildren(double contentX, double contentY, double contentWidth, double contentHeight) {
|
||||||
|
getChildren().get(0).resizeRelocate(contentX, contentY, contentWidth, contentHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,173 @@
|
|||||||
|
package com.imyeyu.fx.ui.components;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.TimiFX;
|
||||||
|
import com.imyeyu.fx.icon.TimiFXIcon;
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import com.imyeyu.java.bean.CallbackArg;
|
||||||
|
import com.imyeyu.java.ref.Ref;
|
||||||
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
|
import javafx.beans.property.StringProperty;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.scene.Cursor;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.scene.control.Skin;
|
||||||
|
import javafx.scene.control.TextField;
|
||||||
|
import javafx.scene.control.skin.TextFieldSkin;
|
||||||
|
import javafx.scene.layout.BorderPane;
|
||||||
|
import javafx.scene.layout.Pane;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 复杂文本域编辑器 {@link TextAreaEditor} 的文本框显示方式,需要时弹出文本域编辑器
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-07-26 16:42
|
||||||
|
*/
|
||||||
|
public class TextAreaEditorField extends TextField implements TimiFXUI {
|
||||||
|
|
||||||
|
/** 显示编辑器事件 */
|
||||||
|
private CallbackArg<Stage> onShowEditorEvent;
|
||||||
|
|
||||||
|
/** 编辑器窗体 */
|
||||||
|
private final EditorStage editorStage;
|
||||||
|
|
||||||
|
/** 标题 */
|
||||||
|
private final StringProperty title;
|
||||||
|
|
||||||
|
/** 默认构造 */
|
||||||
|
public TextAreaEditorField() {
|
||||||
|
this("");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造文本编辑器型文本框(可打开文本编辑器)
|
||||||
|
*
|
||||||
|
* @param text 文本内容
|
||||||
|
*/
|
||||||
|
public TextAreaEditorField(String text) {
|
||||||
|
super(text);
|
||||||
|
title = new SimpleStringProperty();
|
||||||
|
|
||||||
|
editorStage = new EditorStage();
|
||||||
|
editorStage.titleProperty().bind(title);
|
||||||
|
editorStage.editor.textProperty().bindBidirectional(textProperty());
|
||||||
|
editorStage.editor.editableProperty().bind(editableProperty());
|
||||||
|
|
||||||
|
getStyleClass().add(CSS.PADDING_N);
|
||||||
|
setPrefHeight(28);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Skin<?> createDefaultSkin() {
|
||||||
|
Skin<?> skin = super.createDefaultSkin();
|
||||||
|
if (skin instanceof TextFieldSkin textFieldSkin) {
|
||||||
|
try {
|
||||||
|
Pane textGroup = Ref.getFieldValue(textFieldSkin, "textGroup", Pane.class);
|
||||||
|
textGroup.setTranslateY(-.5);
|
||||||
|
|
||||||
|
// 插入编辑器图标
|
||||||
|
IconButton editor = new IconButton(TimiFXIcon.fromName("WRITING")).withBackground();
|
||||||
|
editor.getStyleClass().add(CSS.BORDER_L);
|
||||||
|
editor.setCursor(Cursor.DEFAULT);
|
||||||
|
|
||||||
|
BorderPane root = new BorderPane();
|
||||||
|
BorderPane.setMargin(textGroup, new Insets(0, 0, 0, 4));
|
||||||
|
root.setCenter(textGroup);
|
||||||
|
root.setRight(editor);
|
||||||
|
|
||||||
|
textFieldSkin.getChildren().setAll(root);
|
||||||
|
|
||||||
|
// ---------- 事件 ----------
|
||||||
|
|
||||||
|
editor.setOnAction(e -> {
|
||||||
|
if (onShowEditorEvent != null) {
|
||||||
|
onShowEditorEvent.handler(editorStage);
|
||||||
|
}
|
||||||
|
TimiFX.showCenter(getScene().getWindow(), editorStage);
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return skin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取显示编辑器回调事件
|
||||||
|
*
|
||||||
|
* @return 显示编辑器回调事件
|
||||||
|
*/
|
||||||
|
public CallbackArg<Stage> getOnShowEditorEvent() {
|
||||||
|
return onShowEditorEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置显示编辑器回调事件,触发时窗体并未显示
|
||||||
|
*
|
||||||
|
* @param onShowEditorEvent 显示编辑器回调事件
|
||||||
|
*/
|
||||||
|
public void setOnShowEditorEvent(CallbackArg<Stage> onShowEditorEvent) {
|
||||||
|
this.onShowEditorEvent = onShowEditorEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取编辑器的弹窗
|
||||||
|
*
|
||||||
|
* @return 编辑器弹窗
|
||||||
|
*/
|
||||||
|
public EditorStage getEditorStage() {
|
||||||
|
return editorStage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取编辑器标题
|
||||||
|
*
|
||||||
|
* @return 编辑器标题
|
||||||
|
*/
|
||||||
|
public String getTitle() {
|
||||||
|
return title.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取编辑器标题属性
|
||||||
|
*
|
||||||
|
* @return 编辑器标题属性
|
||||||
|
*/
|
||||||
|
public StringProperty titleProperty() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置编辑器标题
|
||||||
|
*
|
||||||
|
* @param title 编辑器标题
|
||||||
|
*/
|
||||||
|
public void setTitle(String title) {
|
||||||
|
this.title.set(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑器弹窗
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-07-26 16:54
|
||||||
|
*/
|
||||||
|
public static class EditorStage extends Stage {
|
||||||
|
|
||||||
|
/** 编辑器 */
|
||||||
|
final TextAreaEditor editor;
|
||||||
|
|
||||||
|
EditorStage() {
|
||||||
|
editor = new TextAreaEditor();
|
||||||
|
editor.getStyleClass().add(CSS.BORDER_T);
|
||||||
|
|
||||||
|
Scene scene = new Scene(editor);
|
||||||
|
scene.getStylesheets().addAll(CSS_STYLE, CSS_FONT);
|
||||||
|
setScene(scene);
|
||||||
|
getIcons().add(TimiFXIcon.iconFromName("WRITING"));
|
||||||
|
setWidth(850);
|
||||||
|
setHeight(620);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
334
src/main/java/com/imyeyu/fx/ui/components/TextFlower.java
Normal file
334
src/main/java/com/imyeyu/fx/ui/components/TextFlower.java
Normal file
@ -0,0 +1,334 @@
|
|||||||
|
package com.imyeyu.fx.ui.components;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.icon.TimiFXIcon;
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import com.imyeyu.java.TimiJava;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.paint.Paint;
|
||||||
|
import javafx.scene.text.Text;
|
||||||
|
import javafx.scene.text.TextFlow;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文本流组件,支持插入超链文本,解析富文本:{@link #matcher(String)}
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-09-05 10:41
|
||||||
|
*/
|
||||||
|
public class TextFlower extends TextFlow implements TimiFXUI {
|
||||||
|
|
||||||
|
/** 默认构造器 */
|
||||||
|
public TextFlower() {
|
||||||
|
getStyleClass().add(CSS.MINECRAFT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文本开始,添加一个制表符
|
||||||
|
*
|
||||||
|
* @return 本实例
|
||||||
|
*/
|
||||||
|
public TextFlower textStart() {
|
||||||
|
getChildren().add(new Text("\t"));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 追加文本,左侧补充一个制表符,通常是段落开始
|
||||||
|
*
|
||||||
|
* @param text 文本
|
||||||
|
* @return 本实例
|
||||||
|
*/
|
||||||
|
public TextFlower textStart(String text) {
|
||||||
|
getChildren().add(new Text("\t" + text));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 追加文本
|
||||||
|
*
|
||||||
|
* @param text 文本
|
||||||
|
* @return 本实例
|
||||||
|
*/
|
||||||
|
public TextFlower text(String text) {
|
||||||
|
getChildren().add(new Text(text));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 追加文本,左侧补充空格
|
||||||
|
*
|
||||||
|
* @param text 文本
|
||||||
|
* @return 本实例
|
||||||
|
*/
|
||||||
|
public TextFlower textLSP(String text) {
|
||||||
|
getChildren().add(new Text(" " + text));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 追加文本,右侧补充空格
|
||||||
|
*
|
||||||
|
* @param text 文本
|
||||||
|
* @return 本实例
|
||||||
|
*/
|
||||||
|
public TextFlower textRSP(String text) {
|
||||||
|
getChildren().add(new Text(text + " "));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 追加链接文本
|
||||||
|
*
|
||||||
|
* @param text 显示文本
|
||||||
|
* @param link 访问链接
|
||||||
|
* @return 本实例
|
||||||
|
*/
|
||||||
|
public TextFlower link(String text, String link) {
|
||||||
|
getChildren().add(new XHyperlink(text, link));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 追加链接文本
|
||||||
|
*
|
||||||
|
* @param icon 图标
|
||||||
|
* @param text 显示文本
|
||||||
|
* @param link 访问链接
|
||||||
|
* @return 本实例
|
||||||
|
*/
|
||||||
|
public TextFlower link(Node icon, String text, String link) {
|
||||||
|
getChildren().add(new XHyperlink(icon, text, link));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 追加链接,显示文本为链接
|
||||||
|
*
|
||||||
|
* @param value 内容
|
||||||
|
* @return 本实例
|
||||||
|
*/
|
||||||
|
public TextFlower syncLink(String value) {
|
||||||
|
getChildren().add(new XHyperlink(value, value));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置最后一个文本节点为链接
|
||||||
|
*
|
||||||
|
* @param link 链接
|
||||||
|
* @return 本实例
|
||||||
|
*/
|
||||||
|
public TextFlower asLink(String link) {
|
||||||
|
Node text = getChildren().remove(getChildren().size() - 1);
|
||||||
|
if (text instanceof Text t) {
|
||||||
|
getChildren().add(new XHyperlink(t.getText(), link));
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 富文本匹配解析字符串,转义使用 '\'
|
||||||
|
* <br>
|
||||||
|
* <p>格式标准:
|
||||||
|
* <ul>
|
||||||
|
* <li>超链:[可选文本,连接]</li>
|
||||||
|
* <li>图标:<可选颜色, timi-fx-icon 图标名称></li>
|
||||||
|
* <li>重点:`[可选样式,内容]`</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @param value 字符串
|
||||||
|
* @return 本实例
|
||||||
|
*/
|
||||||
|
public TextFlower matcher(String value) {
|
||||||
|
if (TimiJava.isNotEmpty(value)) {
|
||||||
|
getChildren().addAll(RichMatcher.parse(value));
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 富文本匹配
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-10-10 11:23
|
||||||
|
*/
|
||||||
|
private static class RichMatcher {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 匹配正则
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-10-10 11:58
|
||||||
|
*/
|
||||||
|
private enum Regex {
|
||||||
|
|
||||||
|
LINK("\\[(.*?)]"),
|
||||||
|
|
||||||
|
ICON("<(.*?)>"),
|
||||||
|
|
||||||
|
SPAN("`(.*?)`");
|
||||||
|
|
||||||
|
final Pattern pattern;
|
||||||
|
|
||||||
|
Regex(String regex) {
|
||||||
|
this.pattern = Pattern.compile(regex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重点内容样式
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-10-10 11:58
|
||||||
|
*/
|
||||||
|
private enum Style {
|
||||||
|
|
||||||
|
UNDERLINE("u", "underline");
|
||||||
|
|
||||||
|
private final String[] matches;
|
||||||
|
|
||||||
|
Style(String... matches) {
|
||||||
|
this.matches = matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Style fromMatcher(String matcher) {
|
||||||
|
Style[] values = values();
|
||||||
|
for (int i = 0; i < values.length; i++) {
|
||||||
|
String[] matches = values[i].matches;
|
||||||
|
for (int j = 0; j < matches.length; j++) {
|
||||||
|
if (matches[j].equalsIgnoreCase(matcher)) {
|
||||||
|
return values[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析富文本匹配
|
||||||
|
*
|
||||||
|
* @param data 文本内容
|
||||||
|
* @return 匹配节点列表
|
||||||
|
*/
|
||||||
|
static List<Node> parse(String data) {
|
||||||
|
List<Node> result = new ArrayList<>();
|
||||||
|
|
||||||
|
Regex[] regexes = Regex.values();
|
||||||
|
|
||||||
|
Map<String, Node> nodeMap = new HashMap<>();
|
||||||
|
|
||||||
|
int[] insertI = {0};
|
||||||
|
String value = data;
|
||||||
|
Matcher matcher;
|
||||||
|
for (int i = 0; i < regexes.length; i++) {
|
||||||
|
final int j = i;
|
||||||
|
matcher = regexes[i].pattern.matcher(value);
|
||||||
|
|
||||||
|
value = matcher.replaceAll(matchResult -> {
|
||||||
|
if (matchResult.start() - 1 != -1) {
|
||||||
|
if (data.charAt(matchResult.start() - 1) == '\\') {
|
||||||
|
return matchResult.group();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodeMap.put(String.valueOf(insertI[0]), node(regexes[j], value(regexes[j], matchResult.group())));
|
||||||
|
return "[" + insertI[0]++ + "]";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 二次解析
|
||||||
|
matcher = Pattern.compile("\\[(.*?)]").matcher(value);
|
||||||
|
int end = 0;
|
||||||
|
while (matcher.find()) {
|
||||||
|
if (matcher.start() - 1 != -1) {
|
||||||
|
if (data.charAt(matcher.start() - 1) == '\\') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (end != matcher.start()) {
|
||||||
|
result.add(new Text(value.substring(end, matcher.start())));
|
||||||
|
}
|
||||||
|
result.add(nodeMap.get(value(Regex.LINK, matcher.group())));
|
||||||
|
end = matcher.end();
|
||||||
|
}
|
||||||
|
result.add(new Text(value.substring(end)));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析正则匹配值
|
||||||
|
*
|
||||||
|
* @param regex 匹配正则
|
||||||
|
* @param matcherResult 匹配结果
|
||||||
|
* @return 匹配值
|
||||||
|
*/
|
||||||
|
private static String value(Regex regex, String matcherResult) {
|
||||||
|
return switch (regex) {
|
||||||
|
case LINK, ICON, SPAN -> matcherResult.substring(1, matcherResult.length() - 1);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析节点
|
||||||
|
*
|
||||||
|
* @param regex 匹配正则
|
||||||
|
* @param value 匹配值
|
||||||
|
* @return 节点
|
||||||
|
*/
|
||||||
|
private static Node node(Regex regex, String value) {
|
||||||
|
return switch (regex) {
|
||||||
|
// 超链
|
||||||
|
case LINK -> {
|
||||||
|
int sp = value.indexOf(",");
|
||||||
|
if (sp == -1) {
|
||||||
|
yield new XHyperlink(value.trim());
|
||||||
|
} else {
|
||||||
|
while (value.charAt(sp - 1) == '\\') {
|
||||||
|
sp = value.indexOf(",", sp + 1);
|
||||||
|
}
|
||||||
|
yield new XHyperlink(value.substring(0, sp).trim(), value.substring(sp + 1).trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 图标
|
||||||
|
case ICON -> {
|
||||||
|
Text icon;
|
||||||
|
int sp = value.lastIndexOf(",");
|
||||||
|
if (sp == -1) {
|
||||||
|
icon = TimiFXIcon.fromName(value);
|
||||||
|
} else {
|
||||||
|
icon = TimiFXIcon.fromName(value.substring(sp + 1).trim());
|
||||||
|
icon.setFill(Paint.valueOf(value.substring(0, sp).trim()));
|
||||||
|
}
|
||||||
|
yield icon;
|
||||||
|
}
|
||||||
|
// 重点内容
|
||||||
|
case SPAN -> {
|
||||||
|
int sp = value.indexOf(",");
|
||||||
|
if (sp == -1) {
|
||||||
|
yield new Text(value);
|
||||||
|
} else {
|
||||||
|
Text text = new Text(value.substring(sp + 1).trim());
|
||||||
|
String[] styles = value.substring(0, sp).trim().split(" ");
|
||||||
|
for (int i = 0; i < styles.length; i++) {
|
||||||
|
Style style = Style.fromMatcher(styles[i]);
|
||||||
|
if (style == null) {
|
||||||
|
text.setFill(Paint.valueOf(styles[i]));
|
||||||
|
} else {
|
||||||
|
if (style == Style.UNDERLINE) {
|
||||||
|
text.setUnderline(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
yield text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
201
src/main/java/com/imyeyu/fx/ui/components/TimePicker.java
Normal file
201
src/main/java/com/imyeyu/fx/ui/components/TimePicker.java
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
package com.imyeyu.fx.ui.components;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.TimiFX;
|
||||||
|
import com.imyeyu.fx.icon.TimiFXIcon;
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import com.imyeyu.utils.Time;
|
||||||
|
import javafx.beans.property.IntegerProperty;
|
||||||
|
import javafx.beans.property.SimpleIntegerProperty;
|
||||||
|
import javafx.event.Event;
|
||||||
|
import javafx.event.EventHandler;
|
||||||
|
import javafx.geometry.Bounds;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.ListView;
|
||||||
|
import javafx.scene.control.TextField;
|
||||||
|
import javafx.scene.input.ScrollEvent;
|
||||||
|
import javafx.scene.layout.BorderPane;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.stage.Popup;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 时间选择器
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-11-09 14:25
|
||||||
|
*/
|
||||||
|
public class TimePicker extends HBox implements TimiFXUI {
|
||||||
|
|
||||||
|
private static final String STYLE_CLASS = "time-picker";
|
||||||
|
|
||||||
|
private final Popup popup;
|
||||||
|
private final TextField textField;
|
||||||
|
private final ListView<String> hour, minute, second;
|
||||||
|
|
||||||
|
/** 值 */
|
||||||
|
private final IntegerProperty value;
|
||||||
|
|
||||||
|
/** 默认构造器 */
|
||||||
|
public TimePicker() {
|
||||||
|
value = new SimpleIntegerProperty(-1);
|
||||||
|
|
||||||
|
textField = new TextField();
|
||||||
|
textField.setEditable(false);
|
||||||
|
textField.setAlignment(Pos.CENTER);
|
||||||
|
textField.setPrefWidth(70);
|
||||||
|
textField.setOnContextMenuRequested(Event::consume);
|
||||||
|
IconButton button = new IconButton(TimiFXIcon.fromName("CLOCK")).withBackground();
|
||||||
|
button.getStyleClass().add(CSS.BORDER_TRB);
|
||||||
|
|
||||||
|
// 时间选择
|
||||||
|
hour = new ListView<>();
|
||||||
|
minute = new ListView<>();
|
||||||
|
second = new ListView<>();
|
||||||
|
|
||||||
|
hour.getStyleClass().add(CSS.BORDER_RB);
|
||||||
|
minute.getStyleClass().add(CSS.BORDER_RB);
|
||||||
|
second.getStyleClass().add(CSS.BORDER_B);
|
||||||
|
|
||||||
|
// 此刻
|
||||||
|
Button now = new Button(TimiFXUI.MULTILINGUAL.text("now"));
|
||||||
|
now.getStyleClass().add(CSS.BORDER_L);
|
||||||
|
|
||||||
|
BorderPane root = new BorderPane();
|
||||||
|
BorderPane.setAlignment(now, Pos.CENTER_RIGHT);
|
||||||
|
root.setEffect(Shadow.POPUP);
|
||||||
|
root.setBorder(Stroke.DEFAULT);
|
||||||
|
root.setPrefSize(140, 211);
|
||||||
|
root.setBackground(BG.DEFAULT);
|
||||||
|
root.setCenter(new HBox(hour, minute, second));
|
||||||
|
root.setBottom(now);
|
||||||
|
|
||||||
|
popup = new Popup();
|
||||||
|
popup.getContent().setAll(root);
|
||||||
|
button.setOnAction(e -> {
|
||||||
|
hour.scrollTo(hour.getSelectionModel().getSelectedIndex() - 3);
|
||||||
|
minute.scrollTo(minute.getSelectionModel().getSelectedIndex() - 3);
|
||||||
|
second.scrollTo(second.getSelectionModel().getSelectedIndex() - 3);
|
||||||
|
|
||||||
|
Bounds bounds = textField.localToScreen(textField.getLayoutBounds());
|
||||||
|
popup.setAutoHide(true);
|
||||||
|
popup.show(textField, bounds.getMinX() - 5, bounds.getMaxY() - 6);
|
||||||
|
});
|
||||||
|
|
||||||
|
getStyleClass().add(STYLE_CLASS);
|
||||||
|
setAlignment(Pos.CENTER_LEFT);
|
||||||
|
getChildren().addAll(textField, button);
|
||||||
|
|
||||||
|
// ---------- 事件 ----------
|
||||||
|
|
||||||
|
for (int i = 0; i < 24; i++) {
|
||||||
|
hour.getItems().add(String.format("%02d", i));
|
||||||
|
}
|
||||||
|
for (int i = 0; i < 60; i++) {
|
||||||
|
minute.getItems().add(String.format("%02d", i));
|
||||||
|
}
|
||||||
|
second.getItems().addAll(minute.getItems());
|
||||||
|
|
||||||
|
// 时间滚动居中
|
||||||
|
hour.getSelectionModel().select(0);
|
||||||
|
minute.getSelectionModel().select(0);
|
||||||
|
second.getSelectionModel().select(0);
|
||||||
|
|
||||||
|
EventHandler<ScrollEvent> middleScroll = e -> {
|
||||||
|
if (e.getSource() instanceof ListView<?> list) {
|
||||||
|
if (e.getDeltaY() < 0) {
|
||||||
|
if (list.getSelectionModel().getSelectedIndex() < list.getItems().size() - 1) {
|
||||||
|
list.getSelectionModel().selectNext();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (0 < list.getSelectionModel().getSelectedIndex()) {
|
||||||
|
list.getSelectionModel().selectPrevious();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
list.scrollTo(list.getSelectionModel().getSelectedIndex() - 3);
|
||||||
|
|
||||||
|
int h = hour.getSelectionModel().getSelectedIndex() * Time.HI;
|
||||||
|
int m = minute.getSelectionModel().getSelectedIndex() * Time.MI;
|
||||||
|
int s = second.getSelectionModel().getSelectedIndex() * Time.SI;
|
||||||
|
|
||||||
|
setValue(h + m + s);
|
||||||
|
e.consume();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
hour.addEventFilter(ScrollEvent.SCROLL, middleScroll);
|
||||||
|
minute.addEventFilter(ScrollEvent.SCROLL, middleScroll);
|
||||||
|
second.addEventFilter(ScrollEvent.SCROLL, middleScroll);
|
||||||
|
|
||||||
|
TimiFX.hoverFocus(hour);
|
||||||
|
TimiFX.hoverFocus(minute);
|
||||||
|
TimiFX.hoverFocus(second);
|
||||||
|
|
||||||
|
// 值监听
|
||||||
|
value.addListener((obs, o, n) -> {
|
||||||
|
if (value.get() < 0 || Time.D < value.get()) {
|
||||||
|
throw new IllegalArgumentException("value must in [0, 86400000]");
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDateTime ldt = Time.toLocalDateTime(Time.today() + value.get());
|
||||||
|
textField.setText("%02d:%02d:%02d".formatted(ldt.getHour(), ldt.getMinute(), ldt.getSecond()));
|
||||||
|
|
||||||
|
// 选择器选中
|
||||||
|
int s = value.get() / 1000;
|
||||||
|
int h = s / 60 / 60;
|
||||||
|
|
||||||
|
hour.getSelectionModel().select(h);
|
||||||
|
minute.getSelectionModel().select(s / 60 - h * 60);
|
||||||
|
second.getSelectionModel().select(s % 60);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 此刻
|
||||||
|
now.setOnAction(e -> {
|
||||||
|
value.set((int) (Time.now() - Time.today()));
|
||||||
|
|
||||||
|
LocalDateTime ldt = Time.toLocalDateTime(Time.now());
|
||||||
|
hour.getSelectionModel().select(ldt.getHour());
|
||||||
|
minute.getSelectionModel().select(ldt.getMinute());
|
||||||
|
second.getSelectionModel().select(ldt.getSecond());
|
||||||
|
|
||||||
|
hour.scrollTo(hour.getSelectionModel().getSelectedIndex() - 3);
|
||||||
|
minute.scrollTo(minute.getSelectionModel().getSelectedIndex() - 3);
|
||||||
|
second.scrollTo(second.getSelectionModel().getSelectedIndex() - 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
popup.setOnShown(e -> {
|
||||||
|
hour.scrollTo(hour.getSelectionModel().getSelectedIndex() - 3);
|
||||||
|
minute.scrollTo(minute.getSelectionModel().getSelectedIndex() - 3);
|
||||||
|
second.scrollTo(second.getSelectionModel().getSelectedIndex() - 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
setValue(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前值,距离今天零时的时间戳
|
||||||
|
*
|
||||||
|
* @return 当前值
|
||||||
|
*/
|
||||||
|
public int getValue() {
|
||||||
|
return value.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取值监听
|
||||||
|
*
|
||||||
|
* @return 值监听
|
||||||
|
*/
|
||||||
|
public IntegerProperty valueProperty() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置当前值,取值范围为一天时间戳 [0, 86400000]
|
||||||
|
*
|
||||||
|
* @param value 当前值
|
||||||
|
*/
|
||||||
|
public void setValue(int value) {
|
||||||
|
this.value.set(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
134
src/main/java/com/imyeyu/fx/ui/components/TitleLabel.java
Normal file
134
src/main/java/com/imyeyu/fx/ui/components/TitleLabel.java
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
package com.imyeyu.fx.ui.components;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.TimiFX;
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import com.sun.javafx.scene.control.LabeledText;
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.beans.property.DoubleProperty;
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.beans.property.SimpleDoubleProperty;
|
||||||
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
|
import javafx.collections.ListChangeListener;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.Skin;
|
||||||
|
import javafx.scene.control.skin.LabelSkin;
|
||||||
|
import javafx.scene.paint.Paint;
|
||||||
|
import javafx.scene.shape.Rectangle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标题标签,此组件左侧显示标题,并添加中线分割,产生内容分割并充当标题。组件默认最大化宽度
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-09-06 15:03
|
||||||
|
*/
|
||||||
|
public class TitleLabel extends Label implements TimiFXUI {
|
||||||
|
|
||||||
|
/** 标题与分割线间距 */
|
||||||
|
protected DoubleProperty spacing;
|
||||||
|
|
||||||
|
/** 分割线颜色 */
|
||||||
|
protected ObjectProperty<Paint> lineColor;
|
||||||
|
|
||||||
|
/** 默认构造器 */
|
||||||
|
public TitleLabel() {
|
||||||
|
this("");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标准构造器
|
||||||
|
*
|
||||||
|
* @param text 标题文本
|
||||||
|
*/
|
||||||
|
public TitleLabel(String text) {
|
||||||
|
super(text);
|
||||||
|
|
||||||
|
lineColor = new SimpleObjectProperty<>(Colorful.BORDER);
|
||||||
|
spacing = new SimpleDoubleProperty(6);
|
||||||
|
|
||||||
|
setMaxWidth(Double.MAX_VALUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Skin<?> createDefaultSkin() {
|
||||||
|
Skin<?> defaultSkin = super.createDefaultSkin();
|
||||||
|
if (defaultSkin instanceof LabelSkin skin) {
|
||||||
|
Rectangle line = new Rectangle();
|
||||||
|
line.setHeight(1);
|
||||||
|
line.fillProperty().bind(lineColor);
|
||||||
|
line.translateYProperty().bind(heightProperty().multiply(.5).subtract(1));
|
||||||
|
Node node = skin.getChildren().get(0);
|
||||||
|
if (node instanceof LabeledText text) {
|
||||||
|
line.widthProperty().bind(Bindings.createDoubleBinding(() -> {
|
||||||
|
double textWidth = text.getLayoutBounds().getWidth();
|
||||||
|
return getWidth() - textWidth - spacing.get();
|
||||||
|
}, spacing, widthProperty(), text.layoutBoundsProperty()));
|
||||||
|
line.translateXProperty().bind(Bindings.createDoubleBinding(() -> {
|
||||||
|
double textWidth = text.getLayoutBounds().getWidth();
|
||||||
|
return textWidth + spacing.get();
|
||||||
|
}, spacing, text.layoutBoundsProperty()));
|
||||||
|
}
|
||||||
|
skin.getChildren().addListener((ListChangeListener<Node>) c -> {
|
||||||
|
if (skin.getChildren().get(0) != line) {
|
||||||
|
skin.getChildren().add(0, line);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
skin.getChildren().add(0, line);
|
||||||
|
}
|
||||||
|
return defaultSkin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前分割线颜色
|
||||||
|
*
|
||||||
|
* @return 分割线颜色,默认 {@link TimiFX.Colorful#BORDER}
|
||||||
|
*/
|
||||||
|
public Paint getLineColor() {
|
||||||
|
return lineColor.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置分割线颜色
|
||||||
|
*
|
||||||
|
* @param lineColor 分割线颜色
|
||||||
|
*/
|
||||||
|
public void setLineColor(Paint lineColor) {
|
||||||
|
this.lineColor.set(lineColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取分割线颜色监听
|
||||||
|
*
|
||||||
|
* @return 分割线颜色监听
|
||||||
|
*/
|
||||||
|
public ObjectProperty<Paint> lineColorProperty() {
|
||||||
|
return lineColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前标题文本和分割线的间距
|
||||||
|
*
|
||||||
|
* @return 标题文本和分割线的间距,默认 6
|
||||||
|
*/
|
||||||
|
public double getSpacing() {
|
||||||
|
return spacing.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置标题文本和分割线的间距
|
||||||
|
*
|
||||||
|
* @param spacing 标题文本和分割线的间距
|
||||||
|
*/
|
||||||
|
public void setSpacing(double spacing) {
|
||||||
|
this.spacing.set(spacing);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前标题文本和分割线的间距监听
|
||||||
|
*
|
||||||
|
* @return 标题文本和分割线的间距监听
|
||||||
|
*/
|
||||||
|
public DoubleProperty spacingProperty() {
|
||||||
|
return spacing;
|
||||||
|
}
|
||||||
|
}
|
||||||
215
src/main/java/com/imyeyu/fx/ui/components/ToggleIcon.java
Normal file
215
src/main/java/com/imyeyu/fx/ui/components/ToggleIcon.java
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
package com.imyeyu.fx.ui.components;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.TimiFX;
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import com.imyeyu.fx.utils.BgFill;
|
||||||
|
import com.imyeyu.java.TimiJava;
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.beans.property.BooleanProperty;
|
||||||
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.Skin;
|
||||||
|
import javafx.scene.control.ToggleButton;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
|
import javafx.scene.layout.Background;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图标选择状态按钮,可选图片、SVG 路径或 {@link com.imyeyu.fx.icon.TimiFXIcon} 的字体图标
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-05-18 10:16
|
||||||
|
*/
|
||||||
|
public class ToggleIcon extends ToggleButton implements TimiFXUI {
|
||||||
|
|
||||||
|
private static final String STYLE_CLASS = "toggle-icon";
|
||||||
|
private static final Background BG_SELECTED = new BgFill("#99D1FF").build();
|
||||||
|
|
||||||
|
/** 选中图标 */
|
||||||
|
protected final Node selected;
|
||||||
|
|
||||||
|
/** 非选中图标 */
|
||||||
|
protected final Node otherwise;
|
||||||
|
|
||||||
|
/** 是否自适应尺寸 */
|
||||||
|
protected final BooleanProperty autoSize;
|
||||||
|
|
||||||
|
private Insets iconPadding, iconTextPadding; // 缓存单独图标内边距和图标文本混合的内边距
|
||||||
|
|
||||||
|
// ---------- 图片图标 ----------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造图片图标选择按钮
|
||||||
|
*
|
||||||
|
* @param img 图片
|
||||||
|
*/
|
||||||
|
public ToggleIcon(Image img) {
|
||||||
|
this(img, img);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造图片图标切换按钮
|
||||||
|
*
|
||||||
|
* @param selectedImg 已选图片
|
||||||
|
* @param otherwiseImg 未选图片
|
||||||
|
*/
|
||||||
|
public ToggleIcon(Image selectedImg, Image otherwiseImg) {
|
||||||
|
this(new ImageView(selectedImg), new ImageView(otherwiseImg));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- SVG 图标 ----------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造 SVG 图标选择按钮
|
||||||
|
*
|
||||||
|
* @param svg SVG 路径
|
||||||
|
*/
|
||||||
|
public ToggleIcon(String svg) {
|
||||||
|
this(new SVGIcon(svg));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造 SVG 图标选择按钮
|
||||||
|
*
|
||||||
|
* @param selectedSVG 已选 SVG 路径
|
||||||
|
* @param otherwiseSVG 未选 SVG 路径
|
||||||
|
*/
|
||||||
|
public ToggleIcon(String selectedSVG, String otherwiseSVG) {
|
||||||
|
this(new SVGIcon(selectedSVG), new SVGIcon(otherwiseSVG));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造 SVG 图标选择按钮
|
||||||
|
*
|
||||||
|
* @param icon SVG 图标
|
||||||
|
*/
|
||||||
|
public ToggleIcon(SVGIcon icon) {
|
||||||
|
this(icon, icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 默认构造 ----------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造自定义节点选择按钮
|
||||||
|
*
|
||||||
|
* @param icon 节点
|
||||||
|
*/
|
||||||
|
public ToggleIcon(Node icon) {
|
||||||
|
this(icon, icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造 SVG 图标选择按钮
|
||||||
|
*
|
||||||
|
* @param selected 已选节点
|
||||||
|
* @param otherwise 未选节点
|
||||||
|
*/
|
||||||
|
public ToggleIcon(Node selected, Node otherwise) {
|
||||||
|
this.selected = selected;
|
||||||
|
this.otherwise = otherwise;
|
||||||
|
autoSize = new SimpleBooleanProperty(false);
|
||||||
|
|
||||||
|
if (selected == otherwise) {
|
||||||
|
setGraphic(selected);
|
||||||
|
} else {
|
||||||
|
otherwise.getStyleClass().add("icon");
|
||||||
|
graphicProperty().bind(Bindings.when(selectedProperty()).then(selected).otherwise(otherwise));
|
||||||
|
}
|
||||||
|
selected.getStyleClass().add("icon");
|
||||||
|
TimiFX.hoverOpacity(this);
|
||||||
|
|
||||||
|
getStyleClass().setAll(CSS.MINECRAFT, STYLE_CLASS);
|
||||||
|
setAlignment(Pos.CENTER);
|
||||||
|
setMaxHeight(Double.MAX_VALUE);
|
||||||
|
backgroundProperty().bind(Bindings.when(selectedProperty()).then(BG_SELECTED).otherwise(BG.TRANSPARENT));
|
||||||
|
|
||||||
|
// 自适应尺寸、单独图标、图标文本混合时设置不同的内边距
|
||||||
|
paddingProperty().bind(Bindings.createObjectBinding(() -> {
|
||||||
|
if (autoSize.get()) {
|
||||||
|
return Insets.EMPTY;
|
||||||
|
} else {
|
||||||
|
if (TimiJava.isEmpty(getText())) {
|
||||||
|
return iconPadding == null ? Insets.EMPTY : iconPadding;
|
||||||
|
} else {
|
||||||
|
return iconTextPadding == null ? Insets.EMPTY : iconTextPadding;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, textProperty(), autoSize, skinProperty()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加按钮背景
|
||||||
|
*
|
||||||
|
* @return 本实例
|
||||||
|
*/
|
||||||
|
public ToggleIcon withBackground() {
|
||||||
|
return withBackground(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加按钮背景
|
||||||
|
*
|
||||||
|
* @param borderClass 边框类
|
||||||
|
* @return 本实例
|
||||||
|
*/
|
||||||
|
public ToggleIcon withBackground(String borderClass) {
|
||||||
|
getStyleClass().add(CSS.BG_BUTTON);
|
||||||
|
if (TimiJava.isNotEmpty(borderClass)) {
|
||||||
|
getStyleClass().add(borderClass);
|
||||||
|
}
|
||||||
|
setAlignment(Pos.CENTER);
|
||||||
|
|
||||||
|
backgroundProperty().unbind();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自适应尺寸,不使用内边距填充,图标尺寸决定组件尺寸
|
||||||
|
*
|
||||||
|
* @return 本实例
|
||||||
|
*/
|
||||||
|
public ToggleIcon autoSize() {
|
||||||
|
autoSize.set(true);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前是否自适应尺寸,ture 时图标尺寸决定组件尺寸
|
||||||
|
*
|
||||||
|
* @return true 为自适应尺寸
|
||||||
|
*/
|
||||||
|
public boolean isAutoSize() {
|
||||||
|
return autoSize.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置是否自适应尺寸,ture 时图标尺寸决定组件尺寸
|
||||||
|
*
|
||||||
|
* @param autoSize true 为自适应尺寸
|
||||||
|
*/
|
||||||
|
public void setAutoSize(boolean autoSize) {
|
||||||
|
this.autoSize.set(autoSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取自适应尺寸监听
|
||||||
|
*
|
||||||
|
* @return 自适应尺寸监听
|
||||||
|
*/
|
||||||
|
public BooleanProperty autoSizeProperty() {
|
||||||
|
return autoSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Skin<?> createDefaultSkin() {
|
||||||
|
Skin<?> defaultSkin = super.createDefaultSkin();
|
||||||
|
double h = getFont().getSize() * .382;
|
||||||
|
double v = h * .8;
|
||||||
|
double tv = h * .6;
|
||||||
|
iconPadding = new Insets(v);
|
||||||
|
iconTextPadding = new Insets(tv, h, tv, h);
|
||||||
|
return defaultSkin;
|
||||||
|
}
|
||||||
|
}
|
||||||
395
src/main/java/com/imyeyu/fx/ui/components/TrayFX.java
Normal file
395
src/main/java/com/imyeyu/fx/ui/components/TrayFX.java
Normal file
@ -0,0 +1,395 @@
|
|||||||
|
package com.imyeyu.fx.ui.components;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import com.imyeyu.io.IO;
|
||||||
|
import com.imyeyu.java.bean.CallbackArg;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.beans.property.BooleanProperty;
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||||
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
|
import javafx.beans.property.StringProperty;
|
||||||
|
import javafx.geometry.Rectangle2D;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.scene.control.MenuItem;
|
||||||
|
import javafx.scene.layout.StackPane;
|
||||||
|
import javafx.stage.Screen;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import javafx.stage.StageStyle;
|
||||||
|
|
||||||
|
import javax.swing.SwingUtilities;
|
||||||
|
import java.awt.Image;
|
||||||
|
import java.awt.Point;
|
||||||
|
import java.awt.SystemTray;
|
||||||
|
import java.awt.Toolkit;
|
||||||
|
import java.awt.TrayIcon;
|
||||||
|
import java.awt.event.MouseAdapter;
|
||||||
|
import java.awt.event.MouseEvent;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JavaFX 系统托盘(单例),需要在 FX 线程运行后调用
|
||||||
|
* <pre>
|
||||||
|
* TrayFX trayFX = TrayFX.getInstance();
|
||||||
|
* trayFX.getMenu().getItems().addAll(new MenuItem("menu"));
|
||||||
|
* trayFX.show("icon.png");
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2021-10-30 17:27
|
||||||
|
*/
|
||||||
|
public final class TrayFX implements TimiFXUI {
|
||||||
|
|
||||||
|
private static final String SORT_KEY = "TIMI_FX_TRAY_SORT_KEY";
|
||||||
|
private static final String STYLE_CLASS = "tray-menu";
|
||||||
|
|
||||||
|
private static TrayFX trayFX;
|
||||||
|
|
||||||
|
private final Stage owner;
|
||||||
|
|
||||||
|
/** 菜单寄主窗体 */
|
||||||
|
private final Stage stage;
|
||||||
|
|
||||||
|
private final StackPane root;
|
||||||
|
private final ContextMenu menu;
|
||||||
|
|
||||||
|
/** 托盘对象 */
|
||||||
|
private final SystemTray tray;
|
||||||
|
|
||||||
|
/** 文本提示 */
|
||||||
|
private final StringProperty toolTip;
|
||||||
|
|
||||||
|
/** 显示监听 */
|
||||||
|
private final BooleanProperty showing;
|
||||||
|
|
||||||
|
/** 图标监听 */
|
||||||
|
private final ObjectProperty<Image> icon;
|
||||||
|
|
||||||
|
/** 托盘图标 */
|
||||||
|
private TrayIcon trayIcon;
|
||||||
|
|
||||||
|
private final List<CallbackArg<Stage>> showMenuListeners;
|
||||||
|
private final List<CallbackArg<MouseEvent>> clickListeners;
|
||||||
|
|
||||||
|
private TrayFX() {
|
||||||
|
tray = SystemTray.getSystemTray();
|
||||||
|
|
||||||
|
clickListeners = new ArrayList<>();
|
||||||
|
showMenuListeners = new ArrayList<>();
|
||||||
|
toolTip = new SimpleStringProperty();
|
||||||
|
icon = new SimpleObjectProperty<>();
|
||||||
|
showing = new SimpleBooleanProperty(false);
|
||||||
|
|
||||||
|
// 嵌套舞台去除边框的同时不显示在任务栏
|
||||||
|
Rectangle2D screen = Screen.getPrimary().getBounds();
|
||||||
|
owner = new Stage();
|
||||||
|
owner.initStyle(StageStyle.UTILITY);
|
||||||
|
owner.setOpacity(0);
|
||||||
|
owner.setX(screen.getMaxX() + 10);
|
||||||
|
owner.setY(screen.getMaxY() + 10);
|
||||||
|
|
||||||
|
menu = new ContextMenu();
|
||||||
|
menu.getStyleClass().add(STYLE_CLASS);
|
||||||
|
|
||||||
|
root = new StackPane();
|
||||||
|
|
||||||
|
Scene scene = new Scene(root);
|
||||||
|
scene.getStylesheets().addAll(CSS_STYLE, CSS_FONT);
|
||||||
|
scene.setFill(null);
|
||||||
|
stage = new Stage();
|
||||||
|
stage.setWidth(1);
|
||||||
|
stage.setHeight(1);
|
||||||
|
stage.setScene(scene);
|
||||||
|
stage.initOwner(owner);
|
||||||
|
stage.setResizable(false);
|
||||||
|
stage.setAlwaysOnTop(true);
|
||||||
|
stage.initStyle(StageStyle.TRANSPARENT);
|
||||||
|
|
||||||
|
// 图标
|
||||||
|
icon.addListener((obs, o, img) -> trayIcon.setImage(img));
|
||||||
|
|
||||||
|
// 失焦隐藏
|
||||||
|
stage.focusedProperty().addListener((obs, o, isFocused) -> {
|
||||||
|
if (!isFocused) {
|
||||||
|
stage.hide();
|
||||||
|
owner.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 提示文本
|
||||||
|
toolTip.addListener((obs, o, text) -> {
|
||||||
|
if (trayIcon != null) {
|
||||||
|
if (text != null && !text.trim().equals("")) {
|
||||||
|
trayIcon.setToolTip(text);
|
||||||
|
} else {
|
||||||
|
trayIcon.setToolTip("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 排序
|
||||||
|
menu.setOnShown(e -> menu.getItems().sort((o1, o2) -> {
|
||||||
|
Object o1v = o1.getProperties().get(SORT_KEY);
|
||||||
|
Object o2v = o2.getProperties().get(SORT_KEY);
|
||||||
|
int o1i = o1v == null ? 0 : (int) o1v;
|
||||||
|
int o2i = o2v == null ? 0 : (int) o2v;
|
||||||
|
return Integer.compare(o1i, o2i);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加菜单
|
||||||
|
*
|
||||||
|
* @param menu 菜单
|
||||||
|
*/
|
||||||
|
public void addMenu(MenuItem... menu) {
|
||||||
|
this.menu.getItems().addAll(menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加菜单
|
||||||
|
*
|
||||||
|
* @param sort 排序位置
|
||||||
|
* @param menu 菜单
|
||||||
|
*/
|
||||||
|
public void addMenu(int sort, MenuItem... menu) {
|
||||||
|
for (int i = 0; i < menu.length; i++) {
|
||||||
|
menu[i].getProperties().put(SORT_KEY, sort);
|
||||||
|
}
|
||||||
|
this.menu.getItems().addAll(menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取菜单进行修改(添加菜单建议通过 {@link #addMenu(int, MenuItem...)},可以手动排序
|
||||||
|
*
|
||||||
|
* @return 菜单
|
||||||
|
*/
|
||||||
|
public ContextMenu getMenu() {
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取根节点,修改这个节点的内容可以完全自定义右键菜单内容
|
||||||
|
*
|
||||||
|
* @return 根节点
|
||||||
|
*/
|
||||||
|
public StackPane getRoot() {
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示图标到托盘
|
||||||
|
*
|
||||||
|
* @param path 图标位置
|
||||||
|
*/
|
||||||
|
public void show(String path) {
|
||||||
|
try {
|
||||||
|
show(Toolkit.getDefaultToolkit().createImage(IO.resourceToBytes(getClass(), path)));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示图标到托盘
|
||||||
|
*
|
||||||
|
* @param icon 图标
|
||||||
|
*/
|
||||||
|
public void show(Image icon) {
|
||||||
|
showing.set(true);
|
||||||
|
try {
|
||||||
|
trayIcon = new TrayIcon(icon);
|
||||||
|
// 点击事件
|
||||||
|
trayIcon.addMouseListener(new MouseAdapter() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mouseReleased(MouseEvent e) {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
for (int i = 0; i < clickListeners.size(); i++) {
|
||||||
|
clickListeners.get(i).handler(e);
|
||||||
|
}
|
||||||
|
if (SwingUtilities.isRightMouseButton(e)) {
|
||||||
|
Point p = e.getLocationOnScreen();
|
||||||
|
owner.show();
|
||||||
|
stage.setX(p.getX());
|
||||||
|
stage.setY(p.getY());
|
||||||
|
stage.show();
|
||||||
|
stage.setAlwaysOnTop(true);
|
||||||
|
stage.requestFocus();
|
||||||
|
menu.setX(p.getX());
|
||||||
|
menu.setY(p.getY());
|
||||||
|
menu.show(stage);
|
||||||
|
stage.sizeToScene();
|
||||||
|
for (int i = 0; i < showMenuListeners.size(); i++) {
|
||||||
|
showMenuListeners.get(i).handler(stage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tray.add(trayIcon);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送系统通知
|
||||||
|
*
|
||||||
|
* @param title 标题
|
||||||
|
* @param content 内容
|
||||||
|
* @param type 类型
|
||||||
|
*/
|
||||||
|
public void sendMessage(String title, String content, TrayIcon.MessageType type) {
|
||||||
|
trayIcon.displayMessage(title, content, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 从托盘移除图标(应主动调用,操作系统不会监听程序是否还在运行) */
|
||||||
|
public void remove() {
|
||||||
|
if (trayIcon != null) {
|
||||||
|
tray.remove(trayIcon);
|
||||||
|
trayIcon = null;
|
||||||
|
}
|
||||||
|
showing.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单例对象
|
||||||
|
*
|
||||||
|
* @return 单例对象
|
||||||
|
*/
|
||||||
|
public static synchronized TrayFX getInstance() {
|
||||||
|
if (!SystemTray.isSupported()) {
|
||||||
|
throw new UnsupportedOperationException("The OS is unsupported tray icon.");
|
||||||
|
}
|
||||||
|
if (trayFX == null) {
|
||||||
|
trayFX = new TrayFX();
|
||||||
|
}
|
||||||
|
return trayFX;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取提示文本
|
||||||
|
*
|
||||||
|
* @return 提示文本
|
||||||
|
*/
|
||||||
|
public String getToolTip() {
|
||||||
|
return toolTip.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置提示文本,需在 {@link #show(Image)} 或 {@link #show(String)} 之后调用才有效
|
||||||
|
*
|
||||||
|
* @param text 文本
|
||||||
|
*/
|
||||||
|
public void setToolTip(String text) {
|
||||||
|
toolTip.set(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取提示文本监听
|
||||||
|
*
|
||||||
|
* @return 提示文本监听
|
||||||
|
*/
|
||||||
|
public StringProperty toolTipProperty() {
|
||||||
|
return toolTip;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置图标
|
||||||
|
*
|
||||||
|
* @param path 图标位置
|
||||||
|
*/
|
||||||
|
public void setIcon(String path) {
|
||||||
|
try {
|
||||||
|
setIcon(Toolkit.getDefaultToolkit().createImage(IO.resourceToBytes(getClass(), path)));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置图标
|
||||||
|
*
|
||||||
|
* @param image AWT 图片
|
||||||
|
*/
|
||||||
|
public void setIcon(Image image) {
|
||||||
|
icon.set(image);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前图标
|
||||||
|
*
|
||||||
|
* @return 图标
|
||||||
|
*/
|
||||||
|
public Image getIcon() {
|
||||||
|
return icon.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取图标监听
|
||||||
|
*
|
||||||
|
* @return 图标监听
|
||||||
|
*/
|
||||||
|
public ObjectProperty<Image> iconProperty() {
|
||||||
|
return icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取是否正在显示托盘图标
|
||||||
|
*
|
||||||
|
* @return true 为正在显示托盘图标
|
||||||
|
*/
|
||||||
|
public boolean isShowing() {
|
||||||
|
return showing.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取正在显示托盘图标监听
|
||||||
|
*
|
||||||
|
* @return 正在显示托盘图标监听
|
||||||
|
*/
|
||||||
|
public ReadOnlyBooleanProperty showingProperty() {
|
||||||
|
return showing;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加点击回调
|
||||||
|
*
|
||||||
|
* @param listener 点击监听
|
||||||
|
*/
|
||||||
|
public void addClickListener(CallbackArg<MouseEvent> listener) {
|
||||||
|
clickListeners.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加显示菜单回调
|
||||||
|
*
|
||||||
|
* @param listener 点击监听
|
||||||
|
*/
|
||||||
|
public void addShowMenuListener(CallbackArg<Stage> listener) {
|
||||||
|
showMenuListeners.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取托盘图标
|
||||||
|
*
|
||||||
|
* @return 托盘图标
|
||||||
|
*/
|
||||||
|
public TrayIcon getTrayIcon() {
|
||||||
|
return trayIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取托盘对象
|
||||||
|
*
|
||||||
|
* @return 托盘对象
|
||||||
|
*/
|
||||||
|
public SystemTray getTray() {
|
||||||
|
return tray;
|
||||||
|
}
|
||||||
|
}
|
||||||
208
src/main/java/com/imyeyu/fx/ui/components/VersionLabel.java
Normal file
208
src/main/java/com/imyeyu/fx/ui/components/VersionLabel.java
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
package com.imyeyu.fx.ui.components;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.task.RunAsync;
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import com.imyeyu.utils.Encoder;
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.Cursor;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
import javafx.scene.paint.Color;
|
||||||
|
|
||||||
|
import java.awt.Desktop;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 版本标签,此组件用于显示版本、检查更新和可更新时点击去向,目前可能只适合我使用
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-02-19 18:42
|
||||||
|
*/
|
||||||
|
public abstract class VersionLabel<T> extends VBox implements TimiFXUI, TimiFXUI.Colorful {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-11-27 16:13
|
||||||
|
*/
|
||||||
|
protected enum Status {
|
||||||
|
|
||||||
|
/** 一般 */
|
||||||
|
NORMAL(BLACK),
|
||||||
|
|
||||||
|
/** 正在检查 */
|
||||||
|
CHECKING(ORANGE),
|
||||||
|
|
||||||
|
/** 存在更新 */
|
||||||
|
HAS_UPDATE(GREEN),
|
||||||
|
|
||||||
|
/** 错误 */
|
||||||
|
ERROR(RED);
|
||||||
|
|
||||||
|
Color textColor;
|
||||||
|
|
||||||
|
Status(Color textColor) {
|
||||||
|
this.textColor = textColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置该状态的文本颜色
|
||||||
|
*
|
||||||
|
* @param textColor 颜色
|
||||||
|
*/
|
||||||
|
public void setTextColor(Color textColor) {
|
||||||
|
this.textColor = textColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 更新链接 */
|
||||||
|
protected String updateURL;
|
||||||
|
|
||||||
|
/** 版本标签 */
|
||||||
|
protected Label version;
|
||||||
|
|
||||||
|
/** 其他内容标签 */
|
||||||
|
protected Label content;
|
||||||
|
|
||||||
|
/** 状态 */
|
||||||
|
protected ObjectProperty<Status> status;
|
||||||
|
|
||||||
|
/** 默认构造 */
|
||||||
|
public VersionLabel() {
|
||||||
|
this("");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认构造
|
||||||
|
*
|
||||||
|
* @param text 显示版本
|
||||||
|
*/
|
||||||
|
public VersionLabel(String text) {
|
||||||
|
status = new SimpleObjectProperty<>(Status.NORMAL);
|
||||||
|
|
||||||
|
version = new Label();
|
||||||
|
version.setText(text);
|
||||||
|
version.setWrapText(true);
|
||||||
|
version.textFillProperty().bind(Bindings.createObjectBinding(() -> status.get().textColor, status));
|
||||||
|
|
||||||
|
content = new Label();
|
||||||
|
content.managedProperty().bind(content.textProperty().isNotEmpty());
|
||||||
|
|
||||||
|
setSpacing(3);
|
||||||
|
setAlignment(Pos.CENTER);
|
||||||
|
getChildren().addAll(version, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查版本更新
|
||||||
|
*
|
||||||
|
* @param nowVersion 当前版本
|
||||||
|
*/
|
||||||
|
public void checkVersion(String nowVersion) {
|
||||||
|
status.set(Status.CHECKING);
|
||||||
|
version.setCursor(Cursor.DEFAULT);
|
||||||
|
version.setOnMouseClicked(null);
|
||||||
|
|
||||||
|
// ---------- 事件 ----------
|
||||||
|
|
||||||
|
new RunAsync<T>() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected T call() {
|
||||||
|
return VersionLabel.this.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onFinish(T t) {
|
||||||
|
try {
|
||||||
|
String version = onReturn(t);
|
||||||
|
if (version.equals(nowVersion)) {
|
||||||
|
// 无新版本
|
||||||
|
VersionLabel.this.version.setText(nowVersion);
|
||||||
|
status.set(Status.NORMAL);
|
||||||
|
} else {
|
||||||
|
// 存在新版本
|
||||||
|
VersionLabel.this.version.setText(VersionLabel.this.updateText(version));
|
||||||
|
VersionLabel.this.version.setCursor(Cursor.HAND);
|
||||||
|
VersionLabel.this.version.underlineProperty().bind(VersionLabel.this.version.hoverProperty());
|
||||||
|
VersionLabel.this.version.setOnMouseClicked(event -> {
|
||||||
|
try {
|
||||||
|
Desktop dp = Desktop.getDesktop();
|
||||||
|
if (dp.isSupported(Desktop.Action.BROWSE)) {
|
||||||
|
dp.browse(URI.create(Encoder.url(updateURL)));
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
status.set(Status.HAS_UPDATE);
|
||||||
|
}
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
version.setText(e.getMessage());
|
||||||
|
status.set(Status.ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onException(Throwable e) {
|
||||||
|
version.setText(TimiFXUI.MULTILINGUAL.textArgs("version.fail", nowVersion));
|
||||||
|
version.setOnMouseClicked(event -> checkVersion(nowVersion));
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行查询版本
|
||||||
|
*
|
||||||
|
* @return 执行返回
|
||||||
|
*/
|
||||||
|
protected abstract T run() throws RuntimeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行返回
|
||||||
|
*
|
||||||
|
* @param t 返回数据
|
||||||
|
* @return 具体版本号
|
||||||
|
*/
|
||||||
|
protected abstract String onReturn(T t) throws RuntimeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 存在更新时执行
|
||||||
|
*
|
||||||
|
* @param newVersion 版本
|
||||||
|
* @return 显示文本
|
||||||
|
*/
|
||||||
|
protected abstract String updateText(String newVersion);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行异常的显示文本
|
||||||
|
*
|
||||||
|
* @param e 异常
|
||||||
|
* @return 显示文本
|
||||||
|
*/
|
||||||
|
protected abstract String failText(Throwable e);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取更新链接
|
||||||
|
*
|
||||||
|
* @return 更新链接
|
||||||
|
*/
|
||||||
|
public String getUpdateURL() {
|
||||||
|
return updateURL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置更新链接
|
||||||
|
*
|
||||||
|
* @param updateURL 更新链接
|
||||||
|
*/
|
||||||
|
public void setUpdateURL(String updateURL) {
|
||||||
|
this.updateURL = updateURL;
|
||||||
|
}
|
||||||
|
}
|
||||||
133
src/main/java/com/imyeyu/fx/ui/components/XHyperlink.java
Normal file
133
src/main/java/com/imyeyu/fx/ui/components/XHyperlink.java
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
package com.imyeyu.fx.ui.components;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import com.imyeyu.java.TimiJava;
|
||||||
|
import com.imyeyu.utils.Encoder;
|
||||||
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
|
import javafx.beans.property.StringProperty;
|
||||||
|
import javafx.scene.Cursor;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.input.MouseButton;
|
||||||
|
import javafx.scene.input.MouseEvent;
|
||||||
|
|
||||||
|
import java.awt.Desktop;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 超链标签
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* new XHyperlink("个人博客", "<a href="https://www.imyeyu.net">https://www.imyeyu.net</a>");
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-08-30 10:51
|
||||||
|
*/
|
||||||
|
public class XHyperlink extends Label implements TimiFXUI, TimiFXUI.Colorful {
|
||||||
|
|
||||||
|
private static final Pattern URL_PATTERN = Pattern.compile("https?://(www\\.)?[-a-zA-Z\\d@:%._+~#=]{1,256}\\.[a-zA-Z\\d()]{1,6}\\b([-a-zA-Z\\d()@:%_+.~#?&/=]*)");
|
||||||
|
|
||||||
|
/** 链接监听 */
|
||||||
|
protected final StringProperty url;
|
||||||
|
|
||||||
|
/** 默认构造器 */
|
||||||
|
public XHyperlink() {
|
||||||
|
this("", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造器,链接和文本一致
|
||||||
|
*
|
||||||
|
* @param url 链接
|
||||||
|
*/
|
||||||
|
public XHyperlink(String url) {
|
||||||
|
this(url, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造器
|
||||||
|
*
|
||||||
|
* @param text 显示文本
|
||||||
|
* @param url 链接
|
||||||
|
*/
|
||||||
|
public XHyperlink(String text, String url) {
|
||||||
|
this(null, text, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造器
|
||||||
|
*
|
||||||
|
* @param icon 显示图标
|
||||||
|
* @param text 显示文本
|
||||||
|
* @param url 链接
|
||||||
|
*/
|
||||||
|
public XHyperlink(Node icon, String text, String url) {
|
||||||
|
super(text);
|
||||||
|
|
||||||
|
this.url = new SimpleStringProperty(url);
|
||||||
|
|
||||||
|
setCursor(Cursor.HAND);
|
||||||
|
setGraphic(icon);
|
||||||
|
setTextFill(FOCUSED_DEFAULT);
|
||||||
|
underlineProperty().bind(hoverProperty());
|
||||||
|
|
||||||
|
addEventFilter(MouseEvent.MOUSE_CLICKED, e -> {
|
||||||
|
if (e.getButton() == MouseButton.PRIMARY) {
|
||||||
|
if (TimiJava.isNotEmpty(this.url.get())) {
|
||||||
|
Matcher matcher = URL_PATTERN.matcher(this.url.get());
|
||||||
|
if (matcher.find()) {
|
||||||
|
try {
|
||||||
|
Desktop dp = Desktop.getDesktop();
|
||||||
|
if (dp.isSupported(Desktop.Action.BROWSE)) {
|
||||||
|
dp.browse(URI.create(Encoder.url(this.url.get())));
|
||||||
|
}
|
||||||
|
} catch (IOException ex) {
|
||||||
|
ex.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步设置链接和文本
|
||||||
|
*
|
||||||
|
* @param textUrl 显示文本和链接
|
||||||
|
*/
|
||||||
|
public void sync(String textUrl) {
|
||||||
|
setText(textUrl);
|
||||||
|
url.set(textUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前链接
|
||||||
|
*
|
||||||
|
* @return 链接
|
||||||
|
*/
|
||||||
|
public String getUrl() {
|
||||||
|
return url.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置链接
|
||||||
|
*
|
||||||
|
* @param url 链接
|
||||||
|
*/
|
||||||
|
public void setUrl(String url) {
|
||||||
|
this.url.set(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取链接监听
|
||||||
|
*
|
||||||
|
* @return 链接监听
|
||||||
|
*/
|
||||||
|
public StringProperty urlProperty() {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
337
src/main/java/com/imyeyu/fx/ui/components/XPagination.java
Normal file
337
src/main/java/com/imyeyu/fx/ui/components/XPagination.java
Normal file
@ -0,0 +1,337 @@
|
|||||||
|
package com.imyeyu.fx.ui.components;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.icon.TimiFXIcon;
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.beans.property.IntegerProperty;
|
||||||
|
import javafx.beans.property.LongProperty;
|
||||||
|
import javafx.beans.property.ReadOnlyIntegerProperty;
|
||||||
|
import javafx.beans.property.SimpleIntegerProperty;
|
||||||
|
import javafx.beans.property.SimpleLongProperty;
|
||||||
|
import javafx.beans.value.ChangeListener;
|
||||||
|
import javafx.event.EventHandler;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.ToggleButton;
|
||||||
|
import javafx.scene.control.ToggleGroup;
|
||||||
|
import javafx.scene.input.MouseEvent;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.scene.layout.Region;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页组件,支持省略页,滚动页,如 [<][1]..[5][6][7][8][9]..[20][>]
|
||||||
|
* <pre>
|
||||||
|
* XPagination pagination = new XPagination();
|
||||||
|
* pagination.setSize(10); // 单页数据量
|
||||||
|
* pagination.setLength(200); // 总数据量
|
||||||
|
* pagination.indexProperty((obs, o, newIndex) -> {
|
||||||
|
* // 监听激活下标
|
||||||
|
* });
|
||||||
|
* pagination.setIndex(6); // 激活页下标(0 开始,这是第 7 页)
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2021-12-22 15:25
|
||||||
|
*/
|
||||||
|
public class XPagination extends HBox implements TimiFXUI {
|
||||||
|
|
||||||
|
private static final String STYLE_CLASS = "x-pagination";
|
||||||
|
|
||||||
|
/** 阻止取消选择 */
|
||||||
|
public static final EventHandler<MouseEvent> EVENT_TOGGLE_BUTTON = e -> {
|
||||||
|
if (e.getSource() instanceof ToggleButton btn && btn.isSelected()) {
|
||||||
|
e.consume();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private final IconButton prev, next;
|
||||||
|
|
||||||
|
// 步进翻页动态下标
|
||||||
|
private int prevI, nextI;
|
||||||
|
|
||||||
|
private final LongProperty lp; // lengthProperty 总数据量 [0, N]
|
||||||
|
private final IntegerProperty ip; // indexProperty 激活下标 [0, chunkProperty.value]
|
||||||
|
private final IntegerProperty sp; // sizeProperty 单页数量 [1, N]
|
||||||
|
private final IntegerProperty cp; // chunkProperty 页面数量 [1, N]
|
||||||
|
|
||||||
|
/** 默认构造 */
|
||||||
|
public XPagination() {
|
||||||
|
// 基本参数
|
||||||
|
lp = new SimpleLongProperty();
|
||||||
|
ip = new SimpleIntegerProperty();
|
||||||
|
sp = new SimpleIntegerProperty();
|
||||||
|
cp = new SimpleIntegerProperty();
|
||||||
|
|
||||||
|
// 更新页面大小和总数据量时重新计算页面数量
|
||||||
|
cp.bind(Bindings.createIntegerBinding(() -> {
|
||||||
|
long total = lp.get();
|
||||||
|
int page = sp.get();
|
||||||
|
return (int) Math.ceil(1D * total / page);
|
||||||
|
}, sp, lp));
|
||||||
|
|
||||||
|
// 页面组
|
||||||
|
List<PageButton> pageButtons = new ArrayList<>();
|
||||||
|
PageButton tb;
|
||||||
|
|
||||||
|
// 上一页
|
||||||
|
prev = new IconButton(TimiFXIcon.fromName("ARROW_1_W")).withBackground();
|
||||||
|
prev.getStyleClass().add(CSS.BORDER_N);
|
||||||
|
prev.setMaxHeight(Double.MAX_VALUE);
|
||||||
|
prev.disableProperty().bind(ip.isEqualTo(0));
|
||||||
|
getChildren().add(prev);
|
||||||
|
|
||||||
|
// 前置页 [1, 6]
|
||||||
|
for (int i = 0; i < 6; i++) {
|
||||||
|
tb = new PageButton();
|
||||||
|
tb.indexProperty.set(i);
|
||||||
|
if (0 < i) {
|
||||||
|
|
||||||
|
// 第一页保持显示,非第一页显示条件:
|
||||||
|
// 1. 总页数小于 8,页码大于 i
|
||||||
|
// 2. 总页数大于等于 8,激活下标小于 4(1 - 4 页)
|
||||||
|
tb.visibleProperty().bind(cp.greaterThan(i).and(cp.lessThan(8)).or(cp.greaterThan(7).and(ip.lessThan(4))));
|
||||||
|
} else {
|
||||||
|
tb.getStyleClass().add(CSS.BORDER_LR);
|
||||||
|
}
|
||||||
|
pageButtons.add(tb);
|
||||||
|
getChildren().add(tb);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// 左省略,总页数大于 7(有中间页),激活下标大于 3 时显示(第五页)
|
||||||
|
Label leftEllipsis = new Label("..");
|
||||||
|
leftEllipsis.setAlignment(Pos.CENTER);
|
||||||
|
leftEllipsis.setPrefWidth(32);
|
||||||
|
leftEllipsis.setBorder(Stroke.RIGHT);
|
||||||
|
leftEllipsis.setMaxHeight(Double.MAX_VALUE);
|
||||||
|
leftEllipsis.visibleProperty().bind(cp.greaterThan(7).and(ip.greaterThan(3)));
|
||||||
|
leftEllipsis.managedProperty().bind(leftEllipsis.visibleProperty());
|
||||||
|
getChildren().add(leftEllipsis);
|
||||||
|
|
||||||
|
// 中间页,显示条件:总页数大于 8 ,激活下标大于 3 且小于总页数 - 4(小于 3 时前置页处理,大于总页数 - 4 时后置页处理)
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
tb = new PageButton();
|
||||||
|
tb.indexProperty.bind(ip.add(i - 2)); // 动态数值
|
||||||
|
tb.visibleProperty().bind(cp.greaterThan(8).and(ip.greaterThan(3).and(ip.lessThan(cp.subtract(4)))));
|
||||||
|
|
||||||
|
pageButtons.add(tb);
|
||||||
|
getChildren().add(tb);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 右省略,显示条件:总页数大于 7,激活下标小于总页数 - 4(大于总页数 - 4 时后置页处理,不需要省略)
|
||||||
|
Label rightEllipsis = new Label("..");
|
||||||
|
rightEllipsis.setAlignment(Pos.CENTER);
|
||||||
|
rightEllipsis.setPrefWidth(32);
|
||||||
|
rightEllipsis.setMaxHeight(Double.MAX_VALUE);
|
||||||
|
rightEllipsis.setBorder(Stroke.RIGHT);
|
||||||
|
rightEllipsis.visibleProperty().bind(cp.greaterThan(7).and(ip.lessThan(cp.subtract(4))));
|
||||||
|
rightEllipsis.managedProperty().bind(rightEllipsis.visibleProperty());
|
||||||
|
getChildren().add(rightEllipsis);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 后置页
|
||||||
|
for (int i = 0; i < 6; i++) {
|
||||||
|
tb = new PageButton();
|
||||||
|
tb.indexProperty.bind(cp.add(i - 6)); // 动态数值
|
||||||
|
if (i == 5) {
|
||||||
|
// 页数达到 7 时最后一页始终显示
|
||||||
|
tb.visibleProperty().bind(cp.greaterThan(6));
|
||||||
|
} else {
|
||||||
|
// 其他页显示条件:总页数大于 7,激活下标大于总页数 - 5(倒数第四页)
|
||||||
|
tb.visibleProperty().bind(cp.greaterThan(7).and(ip.greaterThan(cp.subtract(5))));
|
||||||
|
}
|
||||||
|
pageButtons.add(tb);
|
||||||
|
getChildren().add(tb);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下一页
|
||||||
|
next = new IconButton(TimiFXIcon.fromName("ARROW_1_E")).withBackground();
|
||||||
|
next.setMaxHeight(Double.MAX_VALUE);
|
||||||
|
next.getStyleClass().add(CSS.BORDER_N);
|
||||||
|
next.disableProperty().bind(ip.isEqualTo(cp.subtract(1)).or(cp.isEqualTo(0)));
|
||||||
|
|
||||||
|
getStyleClass().add(STYLE_CLASS);
|
||||||
|
setBorder(Stroke.DEFAULT);
|
||||||
|
setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
|
||||||
|
setAlignment(Pos.BOTTOM_CENTER);
|
||||||
|
getChildren().add(next);
|
||||||
|
|
||||||
|
// ---------- 事件 ----------
|
||||||
|
|
||||||
|
new ToggleGroup().getToggles().addAll(pageButtons);
|
||||||
|
for (int i = 0; i < pageButtons.size(); i++) {
|
||||||
|
// 阻止取消选择
|
||||||
|
pageButtons.get(i).addEventFilter(MouseEvent.MOUSE_PRESSED, EVENT_TOGGLE_BUTTON);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 数据变动更新
|
||||||
|
ChangeListener<Number> paramsListener = (obs, o, n) -> {
|
||||||
|
// 步进翻页
|
||||||
|
prevI = ip.get() - 1;
|
||||||
|
nextI = ip.get() + 1;
|
||||||
|
// 重置激活页
|
||||||
|
if (cp.get() - 1 < ip.get()) {
|
||||||
|
ip.set(0);
|
||||||
|
}
|
||||||
|
// 主动选中
|
||||||
|
for (int i = 0; i < pageButtons.size(); i++) {
|
||||||
|
// 分页存在预设页码,只作触发事件用(如前置页的第五第六页),需要主动计算激活的按钮
|
||||||
|
if (ip.get() == pageButtons.get(i).indexProperty.get() && pageButtons.get(i).isVisible()) {
|
||||||
|
pageButtons.get(i).setSelected(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
sp.addListener(paramsListener);
|
||||||
|
ip.addListener(paramsListener);
|
||||||
|
lp.addListener(paramsListener);
|
||||||
|
|
||||||
|
// 步进翻页
|
||||||
|
prev.setOnAction(e -> ip.set(prevI));
|
||||||
|
next.setOnAction(e -> ip.set(nextI));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 选择上一页 */
|
||||||
|
public void prev() {
|
||||||
|
prev.fire();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 选择下一页 */
|
||||||
|
public void next() {
|
||||||
|
next.fire();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置激活页下标,取值范围 [0, {@link #getChunk()}]
|
||||||
|
*
|
||||||
|
* @param index 激活页下标
|
||||||
|
*/
|
||||||
|
public void setIndex(int index) {
|
||||||
|
ip.set(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前激活页下标
|
||||||
|
*
|
||||||
|
* @return 当前激活页下标
|
||||||
|
*/
|
||||||
|
public int getIndex() {
|
||||||
|
return ip.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取激活页监听
|
||||||
|
*
|
||||||
|
* @return 激活页监听
|
||||||
|
*/
|
||||||
|
public IntegerProperty indexProperty() {
|
||||||
|
return ip;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前页面数量
|
||||||
|
*
|
||||||
|
* @return 当前页面数量
|
||||||
|
*/
|
||||||
|
public int getChunk() {
|
||||||
|
return cp.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取页面数量监听
|
||||||
|
*
|
||||||
|
* @return 页面数量监听
|
||||||
|
*/
|
||||||
|
public ReadOnlyIntegerProperty chunkProperty() {
|
||||||
|
return cp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置单页数量,取值范围 [1, Integer.MAX_VALUE]
|
||||||
|
*
|
||||||
|
* @param size 单页数量
|
||||||
|
*/
|
||||||
|
public void setSize(int size) {
|
||||||
|
if (size < 1) {
|
||||||
|
throw new IllegalArgumentException("page size range of [1, Integer.MAX_VALUE]");
|
||||||
|
}
|
||||||
|
sp.set(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前单页数量
|
||||||
|
*
|
||||||
|
* @return 单页数量
|
||||||
|
*/
|
||||||
|
public int getSize() {
|
||||||
|
return sp.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单页数量监听
|
||||||
|
*
|
||||||
|
* @return 单页数量监听
|
||||||
|
*/
|
||||||
|
public IntegerProperty sizeProperty() {
|
||||||
|
return sp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置总数据量,取值范围 [0, Long.MAX_VALUE]
|
||||||
|
*
|
||||||
|
* @param length 总数据量
|
||||||
|
*/
|
||||||
|
public void setLength(long length) {
|
||||||
|
if (length < 0) {
|
||||||
|
throw new IllegalArgumentException("length range of [0, Long.MAX_VALUE]");
|
||||||
|
}
|
||||||
|
lp.set(length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前总数据量
|
||||||
|
*
|
||||||
|
* @return 当前总数据量
|
||||||
|
*/
|
||||||
|
public long getLength() {
|
||||||
|
return lp.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取总数据大小监听
|
||||||
|
*
|
||||||
|
* @return 总数据大小监听
|
||||||
|
*/
|
||||||
|
public LongProperty lengthProperty() {
|
||||||
|
return lp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 页面按钮,分页组件内部调度
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-01-27 01:42
|
||||||
|
*/
|
||||||
|
private class PageButton extends ToggleButton implements TimiFXUI {
|
||||||
|
|
||||||
|
/** 当前页码 */
|
||||||
|
final IntegerProperty indexProperty;
|
||||||
|
|
||||||
|
public PageButton() {
|
||||||
|
indexProperty = new SimpleIntegerProperty();
|
||||||
|
|
||||||
|
getStyleClass().addAll(CSS.BORDER_R, CSS.BG_BUTTON);
|
||||||
|
// 页码即显示内容
|
||||||
|
textProperty().bind(indexProperty.add(1).asString());
|
||||||
|
managedProperty().bind(visibleProperty());
|
||||||
|
// 选中更新激活下标
|
||||||
|
selectedProperty().addListener((obs, o, isSelected) -> {
|
||||||
|
if (isSelected) {
|
||||||
|
XPagination.this.ip.set(indexProperty.get());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
113
src/main/java/com/imyeyu/fx/ui/components/XTabPane.java
Normal file
113
src/main/java/com/imyeyu/fx/ui/components/XTabPane.java
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
package com.imyeyu.fx.ui.components;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.icon.TimiFXIcon;
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import com.imyeyu.java.bean.CallbackArg;
|
||||||
|
import com.imyeyu.java.ref.Ref;
|
||||||
|
import javafx.animation.TranslateTransition;
|
||||||
|
import javafx.collections.ListChangeListener;
|
||||||
|
import javafx.collections.ObservableList;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.Skin;
|
||||||
|
import javafx.scene.control.TabPane;
|
||||||
|
import javafx.scene.control.skin.TabPaneSkin;
|
||||||
|
import javafx.scene.layout.StackPane;
|
||||||
|
import javafx.util.Duration;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选项卡,简化了选区样式,以及在选项卡右侧添加了新增选项卡按钮,可让用户直接添加
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-07-24 10:54
|
||||||
|
*/
|
||||||
|
public class XTabPane extends TabPane implements TimiFXUI, TimiFXUI.Colorful {
|
||||||
|
|
||||||
|
/** 添加按钮 */
|
||||||
|
protected final IconButton add;
|
||||||
|
|
||||||
|
/** 默认构造 */
|
||||||
|
public XTabPane() {
|
||||||
|
add = new IconButton(TimiFXIcon.fromName("PLUS")).withBackground();
|
||||||
|
add.getStyleClass().add(CSS.BORDER_RB);
|
||||||
|
add.setPrefWidth(20);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取添加按钮
|
||||||
|
*
|
||||||
|
* @return 添加按钮
|
||||||
|
*/
|
||||||
|
public IconButton getAdd() {
|
||||||
|
return add;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Skin<?> createDefaultSkin() {
|
||||||
|
Skin<?> skin = super.createDefaultSkin();
|
||||||
|
if (skin instanceof TabPaneSkin tabPaneSkin) {
|
||||||
|
try {
|
||||||
|
StackPane tabHeaderArea = Ref.getFieldValue(tabPaneSkin, "tabHeaderArea", StackPane.class);
|
||||||
|
StackPane headersRegion = Ref.getFieldValue(tabHeaderArea, "headersRegion", StackPane.class);
|
||||||
|
StackPane headersBackground = Ref.getFieldValue(tabHeaderArea, "headerBackground", StackPane.class);
|
||||||
|
|
||||||
|
// 添加按钮
|
||||||
|
TranslateTransition transition = new TranslateTransition();
|
||||||
|
transition.setNode(add);
|
||||||
|
transition.setDuration(Duration.millis(150));
|
||||||
|
headersRegion.widthProperty().addListener((obs, oldWidth, newWidth) -> {
|
||||||
|
if (oldWidth.doubleValue() < newWidth.doubleValue()) {
|
||||||
|
transition.setFromX(add.getTranslateX());
|
||||||
|
transition.setToX(newWidth.intValue());
|
||||||
|
transition.play();
|
||||||
|
} else {
|
||||||
|
add.setTranslateX(newWidth.intValue());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
add.prefHeightProperty().bind(headersBackground.heightProperty());
|
||||||
|
StackPane.setAlignment(add, Pos.CENTER_LEFT);
|
||||||
|
headersBackground.getChildren().add(add);
|
||||||
|
|
||||||
|
// 关闭按钮调整
|
||||||
|
CallbackArg<Node> resizeCloseButton = tabHeaderSkin -> {
|
||||||
|
try {
|
||||||
|
if (tabHeaderSkin instanceof StackPane tabHeaderPane) {
|
||||||
|
StackPane closeBtn = Ref.getFieldValue(tabHeaderSkin, "closeBtn", StackPane.class);
|
||||||
|
closeBtn.setPrefWidth(18);
|
||||||
|
closeBtn.getStyleClass().clear();
|
||||||
|
|
||||||
|
IconButton icon = new IconButton(TimiFXIcon.fromName("FAIL", GRAY));
|
||||||
|
icon.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
icon.setMouseTransparent(true);
|
||||||
|
icon.prefWidthProperty().bind(closeBtn.widthProperty());
|
||||||
|
icon.minHeightProperty().bind(tabHeaderPane.heightProperty());
|
||||||
|
StackPane.setMargin(icon, new Insets(0, 12, 0, 0));
|
||||||
|
|
||||||
|
closeBtn.getChildren().add(icon);
|
||||||
|
}
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ObservableList<Node> tabList = headersRegion.getChildren();
|
||||||
|
for (int i = 0; i < tabList.size(); i++) {
|
||||||
|
resizeCloseButton.handler(tabList.get(i));
|
||||||
|
}
|
||||||
|
headersRegion.getChildren().addListener((ListChangeListener<Node>) c -> {
|
||||||
|
if (c.next()) {
|
||||||
|
List<? extends Node> list = c.getAddedSubList();
|
||||||
|
for (int i = 0; i < list.size(); i++) {
|
||||||
|
resizeCloseButton.handler(list.get(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return skin;
|
||||||
|
}
|
||||||
|
}
|
||||||
45
src/main/java/com/imyeyu/fx/ui/components/XTreeView.java
Normal file
45
src/main/java/com/imyeyu/fx/ui/components/XTreeView.java
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package com.imyeyu.fx.ui.components;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.utils.SmoothScroll;
|
||||||
|
import javafx.collections.ObservableList;
|
||||||
|
import javafx.scene.control.TreeItem;
|
||||||
|
import javafx.scene.control.TreeView;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 不显示根节点的树形结构,实现多个根节点
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2021-04-26 01:34
|
||||||
|
*/
|
||||||
|
public class XTreeView<T> extends TreeView<T> {
|
||||||
|
|
||||||
|
private final TreeItem<T> dummyRoot = new TreeItem<>();
|
||||||
|
|
||||||
|
/** 默认构造 */
|
||||||
|
public XTreeView() {
|
||||||
|
dummyRoot.setExpanded(true);
|
||||||
|
setRoot(dummyRoot);
|
||||||
|
setShowRoot(false);
|
||||||
|
// 平滑滚动
|
||||||
|
SmoothScroll.virtual(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置根节点
|
||||||
|
*
|
||||||
|
* @param roots 根节点
|
||||||
|
*/
|
||||||
|
@SafeVarargs
|
||||||
|
public final void setRoots(TreeItem<T>... roots) {
|
||||||
|
dummyRoot.getChildren().addAll(roots);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取根节点列表
|
||||||
|
*
|
||||||
|
* @return 根节点列表
|
||||||
|
*/
|
||||||
|
public ObservableList<TreeItem<T>> getRoots() {
|
||||||
|
return dummyRoot.getChildren();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,448 @@
|
|||||||
|
package com.imyeyu.fx.ui.components.alert;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.TimiFX;
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import com.imyeyu.fx.utils.ScreenFX;
|
||||||
|
import com.imyeyu.java.bean.CallbackArg;
|
||||||
|
import com.imyeyu.java.bean.CallbackArgReturn;
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.scene.input.KeyCode;
|
||||||
|
import javafx.scene.input.KeyEvent;
|
||||||
|
import javafx.scene.layout.BorderPane;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.stage.Modality;
|
||||||
|
import javafx.stage.Screen;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import javafx.stage.Window;
|
||||||
|
import javafx.stage.WindowEvent;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 抽象弹窗
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-01-07 09:24
|
||||||
|
*/
|
||||||
|
public abstract class AbstractAlert extends Stage implements TimiFXUI, TimiFXUI.Colorful {
|
||||||
|
|
||||||
|
/** 默认按钮边距 */
|
||||||
|
protected static final Insets PADDING_BUTTON = new Insets(12, 16, 12, 16);
|
||||||
|
|
||||||
|
/** 默认内容边距 */
|
||||||
|
protected static final Insets PADDING_CONTENT = new Insets(8, 16, 8, 16);
|
||||||
|
|
||||||
|
/** 左侧按钮 */
|
||||||
|
protected final HBox leftButtons;
|
||||||
|
|
||||||
|
/** 中部按钮 */
|
||||||
|
protected final HBox centerButtons;
|
||||||
|
|
||||||
|
/** 右侧按钮 */
|
||||||
|
protected final HBox rightButtons;
|
||||||
|
|
||||||
|
/** 根面板 */
|
||||||
|
protected final BorderPane root;
|
||||||
|
|
||||||
|
/** 按钮面板,{@link #leftButtons}、{@link #centerButtons} 、{@link #rightButtons} 在此面板中 */
|
||||||
|
protected final BorderPane btnPane;
|
||||||
|
|
||||||
|
private final ObjectProperty<AlertType> typeProperty;
|
||||||
|
private final List<CallbackArg<WindowEvent>> shownListeners;
|
||||||
|
|
||||||
|
private AlertButton.Action action;
|
||||||
|
private CallbackArgReturn<AlertButton.Action, Boolean> onActionEvent;
|
||||||
|
|
||||||
|
/** 窗体尺寸是否适应场景尺寸,显示前设置有效,默认 true */
|
||||||
|
protected boolean enableSizeToScene = true;
|
||||||
|
|
||||||
|
/** 默认构造 */
|
||||||
|
public AbstractAlert() {
|
||||||
|
typeProperty = new SimpleObjectProperty<>();
|
||||||
|
shownListeners = new ArrayList<>();
|
||||||
|
action = AlertButton.Action.CANCEL; // 默认取消
|
||||||
|
|
||||||
|
// 按钮布局
|
||||||
|
leftButtons = new HBox(6);
|
||||||
|
centerButtons = new HBox(6);
|
||||||
|
rightButtons = new HBox(6);
|
||||||
|
|
||||||
|
leftButtons.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
centerButtons.setAlignment(Pos.CENTER);
|
||||||
|
rightButtons.setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
|
||||||
|
// 根布局
|
||||||
|
root = new BorderPane();
|
||||||
|
root.setBorder(Stroke.TOP);
|
||||||
|
root.setBottom(btnPane = new BorderPane() {{
|
||||||
|
setPadding(PADDING_BUTTON);
|
||||||
|
setLeft(leftButtons);
|
||||||
|
setCenter(centerButtons);
|
||||||
|
setRight(rightButtons);
|
||||||
|
}});
|
||||||
|
|
||||||
|
Scene scene = new Scene(root);
|
||||||
|
scene.getStylesheets().addAll(CSS_STYLE, CSS_FONT);
|
||||||
|
initModality(Modality.WINDOW_MODAL);
|
||||||
|
setScene(scene);
|
||||||
|
|
||||||
|
// ---------- 事件 ----------
|
||||||
|
|
||||||
|
// 显示
|
||||||
|
setOnShown(e -> {
|
||||||
|
if (enableSizeToScene) {
|
||||||
|
sizeToScene();
|
||||||
|
}
|
||||||
|
callShownListeners(e);
|
||||||
|
});
|
||||||
|
// 类型变更
|
||||||
|
typeProperty.addListener((obs, o, newType) -> {
|
||||||
|
if (newType != null) {
|
||||||
|
getIcons().setAll(newType.icon);
|
||||||
|
setTitle(newType.title);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
layout(root);
|
||||||
|
|
||||||
|
addEventFilter(KeyEvent.KEY_RELEASED, e -> {
|
||||||
|
boolean control = e.isControlDown();
|
||||||
|
boolean shift = e.isShiftDown();
|
||||||
|
boolean alt = e.isAltDown();
|
||||||
|
KeyCode code = e.getCode();
|
||||||
|
|
||||||
|
if (!control && !shift && !alt) {
|
||||||
|
if (code == KeyCode.ESCAPE) {
|
||||||
|
onEscape();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 回调显示监听
|
||||||
|
*
|
||||||
|
* @param e 显示事件
|
||||||
|
*/
|
||||||
|
private void callShownListeners(WindowEvent e) {
|
||||||
|
for (int i = 0; i < shownListeners.size(); i++) {
|
||||||
|
shownListeners.get(i).handler(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 默认 ESC 键关闭 */
|
||||||
|
protected void onEscape() {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 方便匿名内部类的布局完成回调
|
||||||
|
*
|
||||||
|
* @param root 根布局
|
||||||
|
*/
|
||||||
|
protected void layout(BorderPane root) {
|
||||||
|
// 子类实现
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自适应窗体尺寸
|
||||||
|
*
|
||||||
|
* @return 本实例
|
||||||
|
*/
|
||||||
|
public AbstractAlert autoSize() {
|
||||||
|
sizeToScene();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 相对居中显示,不越出父级标题
|
||||||
|
*
|
||||||
|
* @param owner 父级窗体
|
||||||
|
*/
|
||||||
|
public void showRelativeCenter(Window owner) {
|
||||||
|
if (getOwner() == null) {
|
||||||
|
initOwner(owner);
|
||||||
|
}
|
||||||
|
setOnShown(e -> {
|
||||||
|
TimiFX.relativeCenter(owner, this);
|
||||||
|
callShownListeners(e);
|
||||||
|
});
|
||||||
|
show();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 相对居中显示并等待,不越出父级标题
|
||||||
|
*
|
||||||
|
* @param owner 父级窗体
|
||||||
|
*/
|
||||||
|
public void showAwaitRelativeCenter(Window owner) {
|
||||||
|
if (getOwner() == null) {
|
||||||
|
initOwner(owner);
|
||||||
|
}
|
||||||
|
setOnShown(e -> {
|
||||||
|
TimiFX.relativeCenter(owner, this);
|
||||||
|
callShownListeners(e);
|
||||||
|
});
|
||||||
|
showAndWait();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 相对于主屏幕中间显示 */
|
||||||
|
public void showRelativeCenter4PrimaryScreen() {
|
||||||
|
showRelativeCenter4Screen(ScreenFX.primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 相对于屏幕中间显示
|
||||||
|
*
|
||||||
|
* @param screen 屏幕
|
||||||
|
*/
|
||||||
|
public void showRelativeCenter4Screen(Screen screen) {
|
||||||
|
initModality(Modality.APPLICATION_MODAL);
|
||||||
|
setOnShown(e -> {
|
||||||
|
TimiFX.relativeCenter4Screen(screen, this);
|
||||||
|
callShownListeners(e);
|
||||||
|
});
|
||||||
|
show();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 相对于主屏幕中间显示并等待 */
|
||||||
|
public void showAwaitRelativeCenter4PrimaryScreen() {
|
||||||
|
showAwaitRelativeCenter4Screen(ScreenFX.primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 相对于屏幕中间显示并等待
|
||||||
|
*
|
||||||
|
* @param screen 屏幕
|
||||||
|
*/
|
||||||
|
public void showAwaitRelativeCenter4Screen(Screen screen) {
|
||||||
|
initModality(Modality.APPLICATION_MODAL);
|
||||||
|
setOnShown(e -> {
|
||||||
|
TimiFX.relativeCenter4Screen(screen, this);
|
||||||
|
callShownListeners(e);
|
||||||
|
});
|
||||||
|
showAndWait();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 清除所有按钮 */
|
||||||
|
public void clearButton() {
|
||||||
|
leftButtons.getChildren().clear();
|
||||||
|
centerButtons.getChildren().clear();
|
||||||
|
rightButtons.getChildren().clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置弹窗按钮,使用按钮默认位置
|
||||||
|
*
|
||||||
|
* @param buttons 弹窗按钮
|
||||||
|
*/
|
||||||
|
public void setButton(AlertButton... buttons) {
|
||||||
|
clearButton();
|
||||||
|
putButtons(buttons);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 追加弹窗按钮,使用按钮默认位置
|
||||||
|
*
|
||||||
|
* @param buttons 弹窗按钮
|
||||||
|
*/
|
||||||
|
public void putButtons(AlertButton... buttons) {
|
||||||
|
if (buttons != null) {
|
||||||
|
for (int i = 0; i < buttons.length; i++) {
|
||||||
|
final int j = i;
|
||||||
|
buttons[i].setOnAction(e -> {
|
||||||
|
action = buttons[j].action;
|
||||||
|
if (onActionEvent == null || onActionEvent.handler(buttons[j].action)) {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
switch (buttons[i].pos) {
|
||||||
|
case LEFT -> leftButtons.getChildren().add(buttons[i]);
|
||||||
|
case CENTER -> centerButtons.getChildren().add(buttons[i]);
|
||||||
|
case RIGHT -> rightButtons.getChildren().add(buttons[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 追加弹窗按钮
|
||||||
|
*
|
||||||
|
* @param to 目标容器
|
||||||
|
* @param buttons 弹窗按钮
|
||||||
|
*/
|
||||||
|
public void putButtons(HBox to, AlertButton... buttons) {
|
||||||
|
for (int i = 0; i < buttons.length; i++) {
|
||||||
|
final int j = i;
|
||||||
|
buttons[i].setOnAction(e -> {
|
||||||
|
action = buttons[j].action;
|
||||||
|
if (onActionEvent == null || onActionEvent.handler(buttons[j].action)) {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
to.getChildren().add(buttons[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置左侧弹窗按钮
|
||||||
|
*
|
||||||
|
* @param btns 按钮
|
||||||
|
*/
|
||||||
|
public void setLeftButtons(AlertButton... btns) {
|
||||||
|
leftButtons.getChildren().clear();
|
||||||
|
putButtons(leftButtons, btns);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置中间弹窗按钮
|
||||||
|
*
|
||||||
|
* @param buttons 弹窗按钮
|
||||||
|
*/
|
||||||
|
public void setCenterButtons(AlertButton... buttons) {
|
||||||
|
centerButtons.getChildren().clear();
|
||||||
|
putButtons(centerButtons, buttons);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置右侧弹窗按钮
|
||||||
|
*
|
||||||
|
* @param buttons 弹出按钮
|
||||||
|
*/
|
||||||
|
public void setRightButtons(AlertButton... buttons) {
|
||||||
|
rightButtons.getChildren().clear();
|
||||||
|
putButtons(rightButtons, buttons);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置弹窗类型
|
||||||
|
*
|
||||||
|
* @param type 弹窗类型
|
||||||
|
*/
|
||||||
|
public void setType(AlertType type) {
|
||||||
|
this.typeProperty.set(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取弹窗类型
|
||||||
|
*
|
||||||
|
* @return 弹窗类型
|
||||||
|
*/
|
||||||
|
public AlertType getType() {
|
||||||
|
return typeProperty.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取弹窗类型监听
|
||||||
|
*
|
||||||
|
* @return 弹窗类型监听
|
||||||
|
*/
|
||||||
|
public ObjectProperty<AlertType> typeProperty() {
|
||||||
|
return typeProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置图标
|
||||||
|
*
|
||||||
|
* @param icon 图标
|
||||||
|
*/
|
||||||
|
public void setIcon(Image icon) {
|
||||||
|
getIcons().setAll(icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加显示回调
|
||||||
|
*
|
||||||
|
* @param callback 回调
|
||||||
|
*/
|
||||||
|
public void addShownListener(CallbackArg<WindowEvent> callback) {
|
||||||
|
shownListeners.add(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置弹窗动作事件(用户点击带有动作的弹窗按钮)
|
||||||
|
*
|
||||||
|
* @param onActionEvent 弹窗动作事件
|
||||||
|
*/
|
||||||
|
public void setOnActionEvent(CallbackArgReturn<AlertButton.Action, Boolean> onActionEvent) {
|
||||||
|
this.onActionEvent = onActionEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最近用户动作(弹窗按钮事件动作)
|
||||||
|
*
|
||||||
|
* @return 最近用户动作
|
||||||
|
*/
|
||||||
|
public AlertButton.Action getAction() {
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取窗体尺寸是否适应场景尺寸
|
||||||
|
*
|
||||||
|
* @return true 为窗体尺寸是否适应场景尺寸
|
||||||
|
*/
|
||||||
|
public boolean isEnableSizeToScene() {
|
||||||
|
return enableSizeToScene;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置窗体尺寸是否适应场景尺寸,显示前设置有效,默认 true
|
||||||
|
*
|
||||||
|
* @param enableSizeToScene true 为窗体尺寸适应场景尺寸
|
||||||
|
*/
|
||||||
|
public void setEnableSizeToScene(boolean enableSizeToScene) {
|
||||||
|
this.enableSizeToScene = enableSizeToScene;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取按钮布局的主面板
|
||||||
|
*
|
||||||
|
* @return 按钮布局主面板
|
||||||
|
*/
|
||||||
|
public BorderPane getBtnPane() {
|
||||||
|
return btnPane;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取按钮布局面板的左侧面板(如果按钮布局主面板被修改,此面板无效)
|
||||||
|
*
|
||||||
|
* @return 按钮布局左侧面板
|
||||||
|
*/
|
||||||
|
public HBox getLeftButtons() {
|
||||||
|
return leftButtons;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取按钮布局面板的中间面板(如果按钮布局主面板被修改,此面板无效)
|
||||||
|
*
|
||||||
|
* @return 按钮布局中间面板
|
||||||
|
*/
|
||||||
|
public HBox getCenterButtons() {
|
||||||
|
return centerButtons;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取按钮布局面板的右侧面板(如果按钮布局主面板被修改,此面板无效)
|
||||||
|
*
|
||||||
|
* @return 按钮布局右侧面板
|
||||||
|
*/
|
||||||
|
public HBox getRightButtons() {
|
||||||
|
return rightButtons;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取根布局(BorderPane 下部分为按钮面板)
|
||||||
|
*
|
||||||
|
* @return 根布局面板
|
||||||
|
*/
|
||||||
|
public BorderPane getRoot() {
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,307 @@
|
|||||||
|
package com.imyeyu.fx.ui.components.alert;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.icon.TimiFXIcon;
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import com.imyeyu.fx.ui.components.FileTreeView;
|
||||||
|
import com.imyeyu.fx.ui.components.IconButton;
|
||||||
|
import com.imyeyu.fx.ui.components.ToggleIcon;
|
||||||
|
import com.imyeyu.fx.ui.components.popup.PopupTipsService;
|
||||||
|
import com.imyeyu.fx.ui.components.popup.tips.PopupTipsLabel;
|
||||||
|
import com.imyeyu.java.TimiJava;
|
||||||
|
import com.imyeyu.java.bean.CallbackArgReturn;
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.beans.property.BooleanProperty;
|
||||||
|
import javafx.collections.ObservableList;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.scene.control.SelectionMode;
|
||||||
|
import javafx.scene.control.TextField;
|
||||||
|
import javafx.scene.control.TreeItem;
|
||||||
|
import javafx.scene.input.MouseButton;
|
||||||
|
import javafx.scene.input.MouseEvent;
|
||||||
|
import javafx.scene.layout.BorderPane;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 抽象文件选择器,基本文件选择窗体
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-05-23 15:34
|
||||||
|
*/
|
||||||
|
public abstract class AbstractAlertFile extends AbstractAlert implements TimiFXUI {
|
||||||
|
|
||||||
|
|
||||||
|
/** 当前绝对路径 */
|
||||||
|
protected final TextField absolutePath;
|
||||||
|
|
||||||
|
/** 切换隐藏 */
|
||||||
|
protected final ToggleIcon toggleHide;
|
||||||
|
|
||||||
|
/** 确认按钮 */
|
||||||
|
protected final AlertButton confirm;
|
||||||
|
|
||||||
|
/** 取消按钮 */
|
||||||
|
protected final AlertButton cancel;
|
||||||
|
|
||||||
|
/** 文件目录树 */
|
||||||
|
protected final FileTreeView tree;
|
||||||
|
|
||||||
|
private CallbackArgReturn<List<File>, Boolean> onConfirmEvent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认构造器
|
||||||
|
*
|
||||||
|
* @param mode 模式
|
||||||
|
*/
|
||||||
|
public AbstractAlertFile(SelectionMode mode) {
|
||||||
|
VBox header = new VBox();
|
||||||
|
|
||||||
|
// 主页
|
||||||
|
IconButton home = new IconButton(TimiFXIcon.fromName("HOME")).withBackground();
|
||||||
|
home.getStyleClass().add(CSS.BORDER_R);
|
||||||
|
PopupTipsService.installText(home, TimiFXUI.MULTILINGUAL.text("home"));
|
||||||
|
|
||||||
|
// 刷新
|
||||||
|
IconButton refresh = new IconButton(TimiFXIcon.fromName("REFRESH")).withBackground();
|
||||||
|
refresh.getStyleClass().add(CSS.BORDER_R);
|
||||||
|
PopupTipsService.installText(refresh, TimiFXUI.MULTILINGUAL.text("refresh"));
|
||||||
|
|
||||||
|
// 创建文件夹
|
||||||
|
IconButton mkdir = new IconButton(TimiFXIcon.fromName("FOLDER_ADD")).withBackground();
|
||||||
|
mkdir.getStyleClass().add(CSS.BORDER_R);
|
||||||
|
PopupTipsService.installText(mkdir, TimiFXUI.MULTILINGUAL.text("file.mkdir"));
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
IconButton destroy = new IconButton(TimiFXIcon.fromName("FAIL", Colorful.RED)).withBackground();
|
||||||
|
destroy.getStyleClass().add(CSS.BORDER_R);
|
||||||
|
PopupTipsService.installText(destroy, TimiFXUI.MULTILINGUAL.text("delete"));
|
||||||
|
|
||||||
|
// 切换隐藏
|
||||||
|
toggleHide = new ToggleIcon(TimiFXIcon.fromName("HIDE"));
|
||||||
|
PopupTipsService.installText(toggleHide, TimiFXUI.MULTILINGUAL.text("file.show_hide"));
|
||||||
|
|
||||||
|
BorderPane ctrl = new BorderPane();
|
||||||
|
ctrl.setLeft(new HBox(home, refresh, mkdir, destroy));
|
||||||
|
ctrl.setRight(toggleHide);
|
||||||
|
header.getChildren().add(ctrl);
|
||||||
|
|
||||||
|
BorderPane absolutePathPane = new BorderPane();
|
||||||
|
absolutePathPane.setBorder(Stroke.TOP);
|
||||||
|
|
||||||
|
// 绝对路径
|
||||||
|
absolutePath = new TextField();
|
||||||
|
absolutePath.getStyleClass().add(CSS.BORDER_N);
|
||||||
|
IconButton absolutePathGo = new IconButton(TimiFXIcon.fromName("ARROW_2_E")).withBackground();
|
||||||
|
absolutePathGo.getStyleClass().add(CSS.BORDER_L);
|
||||||
|
{
|
||||||
|
if (mode == SelectionMode.SINGLE) {
|
||||||
|
PopupTipsLabel tips = PopupTipsService.installBindingText(absolutePath, absolutePath.textProperty());
|
||||||
|
tips.enableProperty().bind(absolutePath.textProperty().isNotEmpty());
|
||||||
|
} else {
|
||||||
|
absolutePathPane.setVisible(false);
|
||||||
|
absolutePathPane.setManaged(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
absolutePathPane.setCenter(absolutePath);
|
||||||
|
absolutePathPane.setRight(absolutePathGo);
|
||||||
|
}
|
||||||
|
header.getChildren().add(absolutePathPane);
|
||||||
|
|
||||||
|
// 目录树
|
||||||
|
tree = new FileTreeView();
|
||||||
|
tree.setBorder(Stroke.TOP);
|
||||||
|
tree.getSelectionModel().setSelectionMode(mode);
|
||||||
|
|
||||||
|
root.setTop(header);
|
||||||
|
root.setCenter(tree);
|
||||||
|
|
||||||
|
btnPane.setBorder(Stroke.TOP);
|
||||||
|
|
||||||
|
{
|
||||||
|
confirm = AlertButton.confirm();
|
||||||
|
confirm.getStyleClass().add(CSS.BORDER_L);
|
||||||
|
|
||||||
|
cancel = AlertButton.cancel();
|
||||||
|
cancel.getStyleClass().add(CSS.BORDER_L);
|
||||||
|
|
||||||
|
setRightButtons(confirm, cancel);
|
||||||
|
btnPane.setPadding(Insets.EMPTY);
|
||||||
|
rightButtons.setSpacing(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
setEnableSizeToScene(false);
|
||||||
|
setWidth(390);
|
||||||
|
setHeight(490);
|
||||||
|
|
||||||
|
// ---------- 事件 ----------
|
||||||
|
|
||||||
|
// 根目录
|
||||||
|
home.setOnAction(e -> {
|
||||||
|
tree.getSelectionModel().clearAndSelect(0);
|
||||||
|
tree.scrollTo(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 刷新
|
||||||
|
refresh.disableProperty().bind(Bindings.createBooleanBinding(() -> {
|
||||||
|
List<TreeItem<File>> items = tree.getSelectionModel().getSelectedItems();
|
||||||
|
// 没有选择、多选、选的不是文件时禁用
|
||||||
|
return items == null || items.size() != 1 || items.get(0).getValue().isFile();
|
||||||
|
}, tree.getSelectionModel().selectedItemProperty()));
|
||||||
|
refresh.setOnAction(e -> tree.refreshItem(tree.getSelectionModel().getSelectedItem()));
|
||||||
|
|
||||||
|
// 创建文件夹
|
||||||
|
mkdir.disableProperty().bind(refresh.disableProperty());
|
||||||
|
mkdir.setOnAction(e -> tree.mkdir(tree.getSelectionModel().getSelectedItem()));
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
List<File> roots = List.of(File.listRoots());
|
||||||
|
destroy.disableProperty().bind(Bindings.createBooleanBinding(() -> {
|
||||||
|
ObservableList<TreeItem<File>> items = tree.getSelectionModel().getSelectedItems();
|
||||||
|
if (items.isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < items.size(); i++) {
|
||||||
|
if (roots.contains(items.get(i).getValue())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}, tree.getSelectionModel().getSelectedItems()));
|
||||||
|
destroy.setOnAction(e -> tree.destroy(tree.getSelectionModel().getSelectedItems()));
|
||||||
|
|
||||||
|
// 显示隐藏
|
||||||
|
toggleHide.selectedProperty().bindBidirectional(tree.showHideProperty());
|
||||||
|
toggleHide.setOnAction(e -> tree.getRoots().forEach(i -> i.setExpanded(false)));
|
||||||
|
|
||||||
|
// 前往路径
|
||||||
|
absolutePathGo.setOnAction(e -> {
|
||||||
|
if (TimiJava.isNotEmpty(absolutePath.getText())) {
|
||||||
|
File file = new File(absolutePath.getText());
|
||||||
|
if (file.exists()) {
|
||||||
|
tree.selectItem(file);
|
||||||
|
} else {
|
||||||
|
AlertTips.error(this, TimiFXUI.MULTILINGUAL.textArgs("file.tips.not_found_target", absolutePath.getText()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 选中
|
||||||
|
tree.getSelectionModel().selectedItemProperty().addListener((obs, o, newItem) -> {
|
||||||
|
if (newItem != null && newItem.getValue() != null) {
|
||||||
|
absolutePath.setText(newItem.getValue().getAbsolutePath());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 双击触发确认
|
||||||
|
tree.addEventFilter(MouseEvent.MOUSE_CLICKED, e -> {
|
||||||
|
if (e.getButton() == MouseButton.PRIMARY && e.getClickCount() == 2) {
|
||||||
|
confirm.fire();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 确认
|
||||||
|
confirm.setOnAction(e -> {
|
||||||
|
if (onConfirmEvent != null) {
|
||||||
|
List<File> list = tree.getSelectionModel().getSelectedItems().stream().map(TreeItem::getValue).toList();
|
||||||
|
if (onConfirmEvent.handler(list)) {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加构建节点过滤器,返回 false 时不创建该节点
|
||||||
|
*
|
||||||
|
* @param itemFilter 节点过滤器
|
||||||
|
*/
|
||||||
|
public void addItemFilter(CallbackArgReturn<File, Boolean> itemFilter) {
|
||||||
|
tree.addItemFilter(itemFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除构建节点过滤器
|
||||||
|
*
|
||||||
|
* @param itemFilter 节点过滤器
|
||||||
|
*/
|
||||||
|
public void removeItemFilter(CallbackArgReturn<File, Boolean> itemFilter) {
|
||||||
|
tree.removeItemFilter(itemFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步选择目标目录
|
||||||
|
*
|
||||||
|
* @param path 目标目录
|
||||||
|
*/
|
||||||
|
public void selectItem(String path) {
|
||||||
|
tree.selectItem(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步选择目标文件
|
||||||
|
*
|
||||||
|
* @param file 目标文件
|
||||||
|
*/
|
||||||
|
public void selectItem(File file) {
|
||||||
|
tree.selectItem(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取是否显示隐藏文件
|
||||||
|
*
|
||||||
|
* @param showHide true 为显示隐藏文件
|
||||||
|
*/
|
||||||
|
public void isShowHide(boolean showHide) {
|
||||||
|
toggleHide.setSelected(showHide);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置是否显示隐藏文件
|
||||||
|
*
|
||||||
|
* @param showHide true 为显示隐藏文件
|
||||||
|
*/
|
||||||
|
public void setShowHide(boolean showHide) {
|
||||||
|
toggleHide.setSelected(showHide);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取切换显示隐藏文件监听
|
||||||
|
*
|
||||||
|
* @return 切换显示隐藏文件监听
|
||||||
|
*/
|
||||||
|
public BooleanProperty showHideProperty() {
|
||||||
|
return toggleHide.selectedProperty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取确认事件
|
||||||
|
*
|
||||||
|
* @return 确认事件
|
||||||
|
*/
|
||||||
|
public CallbackArgReturn<List<File>, Boolean> getOnConfirmEvent() {
|
||||||
|
return onConfirmEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置确认事件,返回 true 自动关闭窗体
|
||||||
|
*
|
||||||
|
* @param onConfirmEvent 确认事件
|
||||||
|
*/
|
||||||
|
public void setOnConfirmEvent(CallbackArgReturn<List<File>, Boolean> onConfirmEvent) {
|
||||||
|
this.onConfirmEvent = onConfirmEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件目录树
|
||||||
|
*
|
||||||
|
* @return 文件目录树
|
||||||
|
*/
|
||||||
|
public FileTreeView getTree() {
|
||||||
|
return tree;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,111 @@
|
|||||||
|
package com.imyeyu.fx.ui.components.alert;
|
||||||
|
|
||||||
|
import com.imyeyu.java.TimiJava;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.TextInputControl;
|
||||||
|
import javafx.scene.layout.BorderPane;
|
||||||
|
import javafx.scene.layout.Region;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 抽象输入弹窗
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-04-07 10:51
|
||||||
|
*/
|
||||||
|
public abstract class AbstractAlertInput<T extends TextInputControl> extends AbstractAlert {
|
||||||
|
|
||||||
|
/** 输入组件 */
|
||||||
|
protected final T input;
|
||||||
|
|
||||||
|
/** 提示文本 */
|
||||||
|
protected final Label tips;
|
||||||
|
|
||||||
|
/** 内容面板 */
|
||||||
|
protected final BorderPane content;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 输入弹窗
|
||||||
|
*
|
||||||
|
* @param t 输入组件
|
||||||
|
* @param title 标题
|
||||||
|
*/
|
||||||
|
public AbstractAlertInput(T t, String title) {
|
||||||
|
this(t, AlertType.INFORMATION, title, "", "", AlertButton.confirm(), AlertButton.cancel());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 输入弹窗
|
||||||
|
*
|
||||||
|
* @param t 输入组件
|
||||||
|
* @param type 弹窗类型
|
||||||
|
* @param title 标题
|
||||||
|
* @param content 内容
|
||||||
|
* @param text 预设输入框文本
|
||||||
|
* @param btns 可控按钮
|
||||||
|
*/
|
||||||
|
public AbstractAlertInput(T t, AlertType type, String title, String content, String text, AlertButton... btns) {
|
||||||
|
tips = new Label(content);
|
||||||
|
tips.setTextFill(GRAY);
|
||||||
|
tips.setWrapText(true);
|
||||||
|
tips.visibleProperty().bind(tips.textProperty().isNotEmpty());
|
||||||
|
tips.managedProperty().bind(tips.visibleProperty());
|
||||||
|
input = t;
|
||||||
|
input.setText(text);
|
||||||
|
if (input.getPrefWidth() == Region.USE_COMPUTED_SIZE) {
|
||||||
|
tips.setPrefWidth(360);
|
||||||
|
input.setPrefWidth(360);
|
||||||
|
}
|
||||||
|
|
||||||
|
root.setCenter(this.content = new BorderPane() {{
|
||||||
|
setMargin(tips, new Insets(4, 6, 4, 6));
|
||||||
|
setPadding(PADDING_CONTENT);
|
||||||
|
setTop(tips);
|
||||||
|
setCenter(input);
|
||||||
|
}});
|
||||||
|
|
||||||
|
setResizable(false);
|
||||||
|
setType(type);
|
||||||
|
|
||||||
|
if (TimiJava.isNotEmpty(title)) {
|
||||||
|
setTitle(title);
|
||||||
|
}
|
||||||
|
putButtons(btns);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取输入的文本
|
||||||
|
*
|
||||||
|
* @return 输入的文本
|
||||||
|
*/
|
||||||
|
public String getText() {
|
||||||
|
return input.getText();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置提示
|
||||||
|
*
|
||||||
|
* @param tips 提示文本
|
||||||
|
*/
|
||||||
|
public void setTips(String tips) {
|
||||||
|
this.tips.setText(tips);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取提示标签组件
|
||||||
|
*
|
||||||
|
* @return 提示标签组件
|
||||||
|
*/
|
||||||
|
public Label getTips() {
|
||||||
|
return tips;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取输入组件
|
||||||
|
*
|
||||||
|
* @return 输入组件
|
||||||
|
*/
|
||||||
|
public T getInput() {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
}
|
||||||
249
src/main/java/com/imyeyu/fx/ui/components/alert/AlertButton.java
Normal file
249
src/main/java/com/imyeyu/fx/ui/components/alert/AlertButton.java
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
package com.imyeyu.fx.ui.components.alert;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import javafx.geometry.HPos;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 弹窗按钮
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-01-07 09:37
|
||||||
|
*/
|
||||||
|
public class AlertButton extends Button {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按钮通用动作,用于标记,具体事件由调用决定
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-01-20 00:55
|
||||||
|
*/
|
||||||
|
public enum Action {
|
||||||
|
|
||||||
|
/** 同意 */
|
||||||
|
APPLY,
|
||||||
|
|
||||||
|
/** 好 */
|
||||||
|
OK,
|
||||||
|
|
||||||
|
/** 取消 */
|
||||||
|
CANCEL,
|
||||||
|
|
||||||
|
/** 关闭 */
|
||||||
|
CLOSE,
|
||||||
|
|
||||||
|
/** 确认 */
|
||||||
|
CONFIRM,
|
||||||
|
|
||||||
|
/** 是 */
|
||||||
|
YES,
|
||||||
|
|
||||||
|
/** 否 */
|
||||||
|
NO,
|
||||||
|
|
||||||
|
/** 完成 */
|
||||||
|
FINISH,
|
||||||
|
|
||||||
|
/** 上一步 */
|
||||||
|
PREVIOUS,
|
||||||
|
|
||||||
|
/** 下一步 */
|
||||||
|
NEXT,
|
||||||
|
|
||||||
|
/** 跳过 */
|
||||||
|
SKIP,
|
||||||
|
|
||||||
|
/** 保存 */
|
||||||
|
SAVE,
|
||||||
|
|
||||||
|
/** 用于自定义事件 */
|
||||||
|
OTHER
|
||||||
|
}
|
||||||
|
|
||||||
|
HPos pos;
|
||||||
|
Action action;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 弹窗按钮构造器
|
||||||
|
*
|
||||||
|
* @param text 文本
|
||||||
|
*/
|
||||||
|
public AlertButton(String text) {
|
||||||
|
this(HPos.CENTER, Action.OK, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 弹窗按钮构造器
|
||||||
|
*
|
||||||
|
* @param pos 位置
|
||||||
|
* @param action 动作
|
||||||
|
* @param text 文本
|
||||||
|
*/
|
||||||
|
public AlertButton(HPos pos, Action action, String text) {
|
||||||
|
super(text);
|
||||||
|
this.pos = pos;
|
||||||
|
this.action = action;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取按钮所属位置
|
||||||
|
*
|
||||||
|
* @return 按钮所属位置
|
||||||
|
*/
|
||||||
|
public HPos getPos() {
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置按钮所属位置
|
||||||
|
*
|
||||||
|
* @param pos 按钮所属位置
|
||||||
|
*/
|
||||||
|
public void setPos(HPos pos) {
|
||||||
|
this.pos = pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取按钮动作
|
||||||
|
*
|
||||||
|
* @return 按钮动作
|
||||||
|
*/
|
||||||
|
public Action getAction() {
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置按钮动作
|
||||||
|
*
|
||||||
|
* @param action 按钮动作
|
||||||
|
*/
|
||||||
|
public void setAction(Action action) {
|
||||||
|
this.action = action;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按钮动作是否一致
|
||||||
|
*
|
||||||
|
* @param o 比较对象
|
||||||
|
* @return true 为按钮动作 AlertButton.Action 相同
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (o == null || getClass() != o.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
AlertButton that = (AlertButton) o;
|
||||||
|
return action == that.action;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快速构造应用按钮
|
||||||
|
*
|
||||||
|
* @return 应用按钮
|
||||||
|
*/
|
||||||
|
public static AlertButton apply() {
|
||||||
|
return new AlertButton(HPos.RIGHT, Action.APPLY, TimiFXUI.MULTILINGUAL.text("alert.apply", "应用"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快速构造明确按钮
|
||||||
|
*
|
||||||
|
* @return 明确按钮
|
||||||
|
*/
|
||||||
|
public static AlertButton ok() {
|
||||||
|
return new AlertButton(HPos.RIGHT, Action.OK, TimiFXUI.MULTILINGUAL.text("alert.ok", "好"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快速构造取消按钮
|
||||||
|
*
|
||||||
|
* @return 取消按钮
|
||||||
|
*/
|
||||||
|
public static AlertButton cancel() {
|
||||||
|
return new AlertButton(HPos.RIGHT, Action.CANCEL, TimiFXUI.MULTILINGUAL.text("alert.cancel", "取消"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快速构造关闭按钮
|
||||||
|
*
|
||||||
|
* @return 关闭按钮
|
||||||
|
*/
|
||||||
|
public static AlertButton close() {
|
||||||
|
return new AlertButton(HPos.RIGHT, Action.CLOSE, TimiFXUI.MULTILINGUAL.text("alert.close", "关闭"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快速构造确认按钮
|
||||||
|
*
|
||||||
|
* @return 确认按钮
|
||||||
|
*/
|
||||||
|
public static AlertButton confirm() {
|
||||||
|
return new AlertButton(HPos.RIGHT, Action.CONFIRM, TimiFXUI.MULTILINGUAL.text("alert.confirm", "确认"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快速构造确定按钮
|
||||||
|
*
|
||||||
|
* @return 确定按钮
|
||||||
|
*/
|
||||||
|
public static AlertButton yes() {
|
||||||
|
return new AlertButton(HPos.RIGHT, Action.YES, TimiFXUI.MULTILINGUAL.text("alert.yes", "是"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快速构造否定按钮
|
||||||
|
*
|
||||||
|
* @return 否定按钮
|
||||||
|
*/
|
||||||
|
public static AlertButton no() {
|
||||||
|
return new AlertButton(HPos.RIGHT, Action.NO, TimiFXUI.MULTILINGUAL.text("alert.no", "否"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快速构造完成按钮
|
||||||
|
*
|
||||||
|
* @return 完成按钮
|
||||||
|
*/
|
||||||
|
public static AlertButton finish() {
|
||||||
|
return new AlertButton(HPos.RIGHT, Action.FINISH, TimiFXUI.MULTILINGUAL.text("alert.finish", "完成"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快速构造上一步按钮
|
||||||
|
*
|
||||||
|
* @return 上一步按钮
|
||||||
|
*/
|
||||||
|
public static AlertButton previous() {
|
||||||
|
return new AlertButton(HPos.RIGHT, Action.PREVIOUS, TimiFXUI.MULTILINGUAL.text("alert.previous", "上一步"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快速构造下一步按钮
|
||||||
|
*
|
||||||
|
* @return 下一步按钮
|
||||||
|
*/
|
||||||
|
public static AlertButton next() {
|
||||||
|
return new AlertButton(HPos.RIGHT, Action.NEXT, TimiFXUI.MULTILINGUAL.text("alert.next", "下一步"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快速构造跳过按钮
|
||||||
|
*
|
||||||
|
* @return 跳过按钮
|
||||||
|
*/
|
||||||
|
public static AlertButton skip() {
|
||||||
|
return new AlertButton(HPos.RIGHT, Action.SKIP, TimiFXUI.MULTILINGUAL.text("alert.skip", "跳过"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快速构造保存按钮
|
||||||
|
*
|
||||||
|
* @return 保存按钮
|
||||||
|
*/
|
||||||
|
public static AlertButton save() {
|
||||||
|
return new AlertButton(HPos.RIGHT, Action.SAVE, TimiFXUI.MULTILINGUAL.text("alert.save", "保存"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,67 @@
|
|||||||
|
package com.imyeyu.fx.ui.components.alert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 询问确认弹窗
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-08-20 23:37
|
||||||
|
*/
|
||||||
|
public abstract class AlertConfirm extends AlertTips {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认构造器
|
||||||
|
*
|
||||||
|
* @param content 询问内容
|
||||||
|
*/
|
||||||
|
public AlertConfirm(String content) {
|
||||||
|
this(AlertType.INFORMATION, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造器
|
||||||
|
*
|
||||||
|
* @param type 类型
|
||||||
|
*/
|
||||||
|
public AlertConfirm(AlertType type) {
|
||||||
|
this(type, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造器
|
||||||
|
*
|
||||||
|
* @param type 类型
|
||||||
|
* @param content 询问内容
|
||||||
|
*/
|
||||||
|
public AlertConfirm(AlertType type, String content) {
|
||||||
|
this(type, content, AlertButton.yes(), AlertButton.no());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认构造
|
||||||
|
*
|
||||||
|
* @param type 类型
|
||||||
|
* @param content 提示内容
|
||||||
|
* @param btns 按钮
|
||||||
|
*/
|
||||||
|
public AlertConfirm(AlertType type, String content, AlertButton... btns) {
|
||||||
|
super(type, btns);
|
||||||
|
|
||||||
|
setTips(content);
|
||||||
|
setOnActionEvent(action -> {
|
||||||
|
if (action == AlertButton.Action.YES || action == AlertButton.Action.CONFIRM || action == AlertButton.Action.OK) {
|
||||||
|
onConfirm();
|
||||||
|
} else {
|
||||||
|
onCancel();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 确认事件 */
|
||||||
|
protected abstract void onConfirm();
|
||||||
|
|
||||||
|
/** 取消事件 */
|
||||||
|
protected void onCancel() {
|
||||||
|
// 子类可选实现
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
package com.imyeyu.fx.ui.components.alert;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.icon.TimiFXIcon;
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.scene.control.SelectionMode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 混合文件选择,可以选择文件也可以选择目录
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2023-01-24 19:07
|
||||||
|
*/
|
||||||
|
public class AlertFileBlendSelector extends AbstractAlertFile {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造器
|
||||||
|
*
|
||||||
|
* @param mode 选择模式
|
||||||
|
*/
|
||||||
|
public AlertFileBlendSelector(SelectionMode mode) {
|
||||||
|
super(mode);
|
||||||
|
|
||||||
|
// 确认
|
||||||
|
confirm.disableProperty().bind(Bindings.isEmpty(tree.getSelectionModel().getSelectedItems()));
|
||||||
|
|
||||||
|
getIcons().setAll(TimiFXIcon.iconFromName("FOLDER"));
|
||||||
|
setTitle(TimiFXUI.MULTILINGUAL.text("file.tips.select_directory"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
package com.imyeyu.fx.ui.components.alert;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.icon.TimiFXIcon;
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.scene.control.SelectionMode;
|
||||||
|
import javafx.scene.control.TreeItem;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 目录选择
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-05-20 10:55
|
||||||
|
*/
|
||||||
|
public class AlertFilePathSelector extends AbstractAlertFile {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造器
|
||||||
|
*
|
||||||
|
* @param mode 选择模式
|
||||||
|
*/
|
||||||
|
public AlertFilePathSelector(SelectionMode mode) {
|
||||||
|
super(mode);
|
||||||
|
|
||||||
|
// 确认
|
||||||
|
confirm.disableProperty().bind(Bindings.createBooleanBinding(() -> {
|
||||||
|
TreeItem<File> item = tree.getSelectionModel().getSelectedItem();
|
||||||
|
return item == null || item.getValue().isFile();
|
||||||
|
}, tree.getSelectionModel().selectedItemProperty()));
|
||||||
|
|
||||||
|
getIcons().setAll(TimiFXIcon.iconFromName("FOLDER"));
|
||||||
|
setTitle(TimiFXUI.MULTILINGUAL.text("file.tips.select_directory"));
|
||||||
|
|
||||||
|
addItemFilter(File::isDirectory);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,87 @@
|
|||||||
|
package com.imyeyu.fx.ui.components.alert;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.icon.TimiFXIcon;
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import com.imyeyu.io.IO;
|
||||||
|
import com.imyeyu.java.TimiJava;
|
||||||
|
import com.imyeyu.utils.Text;
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.collections.ListChangeListener;
|
||||||
|
import javafx.collections.ObservableList;
|
||||||
|
import javafx.scene.control.SelectionMode;
|
||||||
|
import javafx.scene.control.TreeItem;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件选择
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-05-23 15:49
|
||||||
|
*/
|
||||||
|
public class AlertFileSelector extends AbstractAlertFile {
|
||||||
|
|
||||||
|
/** 格式过滤列表 */
|
||||||
|
protected final ObservableList<String> formatFilters;
|
||||||
|
|
||||||
|
private String[] formatFiltersCache;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认构造
|
||||||
|
*
|
||||||
|
* @param mode 选择模式
|
||||||
|
*/
|
||||||
|
public AlertFileSelector(SelectionMode mode) {
|
||||||
|
super(mode);
|
||||||
|
formatFilters = FXCollections.observableArrayList();
|
||||||
|
|
||||||
|
// 确认
|
||||||
|
confirm.disableProperty().bind(Bindings.createBooleanBinding(() -> {
|
||||||
|
TreeItem<File> item = tree.getSelectionModel().getSelectedItem();
|
||||||
|
return item == null || item.getValue().isDirectory();
|
||||||
|
}, tree.getSelectionModel().selectedItemProperty()));
|
||||||
|
|
||||||
|
getIcons().setAll(TimiFXIcon.iconFromName("FILE"));
|
||||||
|
setTitle(TimiFXUI.MULTILINGUAL.text("file.select"));
|
||||||
|
|
||||||
|
addItemFilter(file -> {
|
||||||
|
if (file.isDirectory() || TimiJava.isEmpty(formatFiltersCache)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return Text.eqIgnoreCaseOr(IO.fileExtension(file), formatFiltersCache);
|
||||||
|
});
|
||||||
|
formatFilters.addListener((ListChangeListener<String>) c -> {
|
||||||
|
while (c.next()) {
|
||||||
|
formatFiltersCache = formatFilters.toArray(new String[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加文件格式过滤,默认显示所有格式的文件,添加过滤后将只显示过滤格式列表的文件
|
||||||
|
*
|
||||||
|
* @param formats 需要显示的文件格式
|
||||||
|
*/
|
||||||
|
public void addFormatFilters(String... formats) {
|
||||||
|
formatFilters.addAll(formats);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除文件格式过滤
|
||||||
|
*
|
||||||
|
* @param formats 不需显示的文件格式
|
||||||
|
*/
|
||||||
|
public void removeFormatFilters(String... formats) {
|
||||||
|
formatFilters.removeAll(formats);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取格式过滤列表
|
||||||
|
*
|
||||||
|
* @return 格式过滤列表
|
||||||
|
*/
|
||||||
|
public ObservableList<String> getFormatFilters() {
|
||||||
|
return formatFilters;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,90 @@
|
|||||||
|
package com.imyeyu.fx.ui.components.alert;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import javafx.beans.property.StringProperty;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.layout.BorderPane;
|
||||||
|
import javafx.scene.layout.StackPane;
|
||||||
|
import javafx.scene.text.TextAlignment;
|
||||||
|
import javafx.stage.StageStyle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 阻塞式弹出加载中弹窗,此弹窗用户必须等待,不能手动关闭,显示期间不可操作其他窗体
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-01-07 16:39
|
||||||
|
*/
|
||||||
|
public class AlertLoading extends AbstractAlert {
|
||||||
|
|
||||||
|
/** 提示标签 */
|
||||||
|
protected Label tips;
|
||||||
|
|
||||||
|
/** 默认构造 */
|
||||||
|
public AlertLoading() {
|
||||||
|
this(TimiFXUI.MULTILINGUAL.text("loading"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造器
|
||||||
|
*
|
||||||
|
* @param tips 提示
|
||||||
|
*/
|
||||||
|
public AlertLoading(String tips) {
|
||||||
|
this.tips = new Label(tips);
|
||||||
|
this.tips.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
|
||||||
|
this.tips.setWrapText(true);
|
||||||
|
this.tips.setPrefWidth(280);
|
||||||
|
this.tips.setAlignment(Pos.CENTER);
|
||||||
|
this.tips.setTextAlignment(TextAlignment.CENTER);
|
||||||
|
|
||||||
|
BorderPane.setMargin(this.tips, PADDING_CONTENT);
|
||||||
|
root.setEffect(Shadow.POPUP);
|
||||||
|
root.setBorder(Stroke.DEFAULT);
|
||||||
|
root.setCenter(this.tips);
|
||||||
|
root.setBackground(BG.DEFAULT);
|
||||||
|
root.setBottom(null);
|
||||||
|
|
||||||
|
StackPane shadow = new StackPane();
|
||||||
|
shadow.setPadding(Shadow.PADDING);
|
||||||
|
shadow.setBackground(BG.TRANSPARENT);
|
||||||
|
shadow.getChildren().add(root);
|
||||||
|
|
||||||
|
getScene().setFill(null);
|
||||||
|
getScene().setRoot(shadow);
|
||||||
|
setTitle(TimiFXUI.MULTILINGUAL.text("loading"));
|
||||||
|
initStyle(StageStyle.TRANSPARENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onEscape() {
|
||||||
|
// 禁用 ESC 关闭
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置提示文本
|
||||||
|
*
|
||||||
|
* @param tips 提示文本
|
||||||
|
*/
|
||||||
|
public void setTips(String tips) {
|
||||||
|
this.tips.setText(tips);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取提示文本属性
|
||||||
|
*
|
||||||
|
* @return 提示文本属性
|
||||||
|
*/
|
||||||
|
public StringProperty tipsProperty() {
|
||||||
|
return tips.textProperty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前提示文本
|
||||||
|
*
|
||||||
|
* @return 提示文本
|
||||||
|
*/
|
||||||
|
public String getTips() {
|
||||||
|
return tips.getText();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
package com.imyeyu.fx.ui.components.alert;
|
||||||
|
|
||||||
|
import javafx.scene.control.PasswordField;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 密码输入弹窗
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-04-06 16:47
|
||||||
|
*/
|
||||||
|
public class AlertPassword extends AbstractAlertInput<PasswordField> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 密码输入弹窗
|
||||||
|
*
|
||||||
|
* @param content 内容
|
||||||
|
*/
|
||||||
|
public AlertPassword(String content) {
|
||||||
|
this(AlertType.INFORMATION, AlertType.INFORMATION.getTitle(), content, "", AlertButton.confirm(), AlertButton.cancel());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 密码输入弹窗
|
||||||
|
*
|
||||||
|
* @param content 内容
|
||||||
|
* @param btns 可控按钮
|
||||||
|
*/
|
||||||
|
public AlertPassword(String content, AlertButton... btns) {
|
||||||
|
this(AlertType.INFORMATION, AlertType.INFORMATION.getTitle(), content, "", btns);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 密码输入弹窗
|
||||||
|
*
|
||||||
|
* @param type 弹窗类型
|
||||||
|
* @param title 标题
|
||||||
|
* @param content 内容
|
||||||
|
* @param text 预设输入框文本
|
||||||
|
* @param btns 可控按钮
|
||||||
|
*/
|
||||||
|
public AlertPassword(AlertType type, String title, String content, String text, AlertButton... btns) {
|
||||||
|
super(new PasswordField(), type, title, content, text, btns);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,215 @@
|
|||||||
|
package com.imyeyu.fx.ui.components.alert;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.TimiFX;
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import com.imyeyu.fx.utils.SmoothScroll;
|
||||||
|
import com.imyeyu.java.bean.CallbackArg;
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.beans.binding.BooleanBinding;
|
||||||
|
import javafx.geometry.HPos;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.scene.control.TextArea;
|
||||||
|
import javafx.scene.layout.Border;
|
||||||
|
import javafx.stage.Window;
|
||||||
|
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文本域输入弹窗
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-01-20 00:36
|
||||||
|
*/
|
||||||
|
public class AlertTextArea extends AbstractAlertInput<TextArea> {
|
||||||
|
|
||||||
|
/** 反馈事件 */
|
||||||
|
private static CallbackArg<String> onFeedback4Error;
|
||||||
|
|
||||||
|
/** 默认构造 */
|
||||||
|
public AlertTextArea() {
|
||||||
|
this("");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文本域输入弹窗
|
||||||
|
*
|
||||||
|
* @param text 输入内容
|
||||||
|
*/
|
||||||
|
public AlertTextArea(String text) {
|
||||||
|
this(AlertType.INFORMATION, AlertType.INFORMATION.getTitle(), "", text, AlertButton.confirm(), AlertButton.cancel());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文本域输入弹窗
|
||||||
|
*
|
||||||
|
* @param content 提示
|
||||||
|
* @param text 输入内容
|
||||||
|
*/
|
||||||
|
public AlertTextArea(String content, String text) {
|
||||||
|
this(AlertType.INFORMATION, AlertType.INFORMATION.getTitle(), content, text, AlertButton.confirm(), AlertButton.cancel());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文本域输入弹窗
|
||||||
|
*
|
||||||
|
* @param text 输入内容
|
||||||
|
* @param btns 可控按钮
|
||||||
|
*/
|
||||||
|
public AlertTextArea(String text, AlertButton... btns) {
|
||||||
|
this(AlertType.INFORMATION, AlertType.INFORMATION.getTitle(), "", text, btns);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文本域输入弹窗
|
||||||
|
*
|
||||||
|
* @param content 提示
|
||||||
|
* @param text 输入内容
|
||||||
|
* @param btns 可控按钮
|
||||||
|
*/
|
||||||
|
public AlertTextArea(String content, String text, AlertButton... btns) {
|
||||||
|
this(AlertType.INFORMATION, AlertType.INFORMATION.getTitle(), content, text, btns);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文本域输入弹窗
|
||||||
|
*
|
||||||
|
* @param type 弹窗类型
|
||||||
|
* @param title 标题
|
||||||
|
* @param content 内容
|
||||||
|
* @param text 预设输入框文本
|
||||||
|
* @param btns 可控按钮
|
||||||
|
*/
|
||||||
|
public AlertTextArea(AlertType type, String title, String content, String text, AlertButton... btns) {
|
||||||
|
super(new TextArea(), type, title, content, text, btns);
|
||||||
|
|
||||||
|
SmoothScroll.textarea(getInput());
|
||||||
|
setResizable(true);
|
||||||
|
|
||||||
|
// 没有按钮监听
|
||||||
|
BooleanBinding emptyButton = Bindings.createBooleanBinding(() -> {
|
||||||
|
boolean emptyLeft = getLeftButtons().getChildren().isEmpty();
|
||||||
|
boolean emptyCenter = getCenterButtons().getChildren().isEmpty();
|
||||||
|
boolean emptyRight = getRightButtons().getChildren().isEmpty();
|
||||||
|
return emptyLeft && emptyCenter && emptyRight;
|
||||||
|
}, getLeftButtons().getChildren(), getCenterButtons().getChildren(), getRightButtons().getChildren());
|
||||||
|
|
||||||
|
emptyButton.addListener((obs, o, isEmpty) -> {
|
||||||
|
btnPane.setVisible(!isEmpty);
|
||||||
|
btnPane.setManaged(!isEmpty);
|
||||||
|
super.content.setPadding(isEmpty ? Insets.EMPTY : PADDING_CONTENT);
|
||||||
|
super.btnPane.setPadding(isEmpty ? Insets.EMPTY : PADDING_BUTTON);
|
||||||
|
});
|
||||||
|
TimiFX.toggleStyleClass4Binding(input, emptyButton, CSS.BORDER_T, CSS.BORDER_ALL);
|
||||||
|
root.borderProperty().bind(Bindings.when(tips.visibleProperty()).then(Stroke.TOP).otherwise(Border.EMPTY));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一般提示
|
||||||
|
*
|
||||||
|
* @param owner 依赖窗体
|
||||||
|
* @param content 内容
|
||||||
|
* @return 弹窗对象
|
||||||
|
*/
|
||||||
|
public static AlertTextArea info(Window owner, String content) {
|
||||||
|
AlertTextArea alert = new AlertTextArea(content);
|
||||||
|
alert.getInput().setEditable(false);
|
||||||
|
alert.clearButton();
|
||||||
|
TimiFX.showCenter(owner, alert);
|
||||||
|
return alert;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一般错误
|
||||||
|
*
|
||||||
|
* @param owner 依赖窗体
|
||||||
|
* @param content 内容
|
||||||
|
* @return 弹窗对象
|
||||||
|
*/
|
||||||
|
public static AlertTextArea error(Window owner, String content) {
|
||||||
|
return error(owner, "", content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一般错误
|
||||||
|
*
|
||||||
|
* @param owner 依赖窗体
|
||||||
|
* @param tips 提示
|
||||||
|
* @param content 内容
|
||||||
|
* @return 弹窗对象
|
||||||
|
*/
|
||||||
|
public static AlertTextArea error(Window owner, String tips, String content) {
|
||||||
|
AlertTextArea alert = new AlertTextArea(AlertType.ERROR, AlertType.ERROR.getTitle(), tips, content);
|
||||||
|
alert.getInput().setEditable(false);
|
||||||
|
alert.getInput().setPrefSize(750, 340);
|
||||||
|
alert.content.setPadding(Insets.EMPTY);
|
||||||
|
alert.btnPane.setPadding(Insets.EMPTY);
|
||||||
|
TimiFX.showCenter(owner, alert);
|
||||||
|
return alert;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异常错误
|
||||||
|
*
|
||||||
|
* @param owner 依赖窗体
|
||||||
|
* @param e 异常
|
||||||
|
* @return 弹窗对象
|
||||||
|
*/
|
||||||
|
public static AlertTextArea error(Window owner, Throwable e) {
|
||||||
|
return error(owner, e.getMessage(), e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异常错误
|
||||||
|
*
|
||||||
|
* @param owner 依赖窗体
|
||||||
|
* @param tips 提示
|
||||||
|
* @param e 异常
|
||||||
|
* @return 弹窗对象
|
||||||
|
*/
|
||||||
|
public static AlertTextArea error(Window owner, String tips, Throwable e) {
|
||||||
|
StringWriter sw = new StringWriter();
|
||||||
|
e.printStackTrace(new PrintWriter(sw));
|
||||||
|
String text = sw.getBuffer().toString();
|
||||||
|
|
||||||
|
AlertTextArea alert = new AlertTextArea(AlertType.ERROR, AlertType.ERROR.getTitle(), tips, text);
|
||||||
|
alert.getTips().setPrefWidth(750);
|
||||||
|
alert.getInput().setEditable(false);
|
||||||
|
alert.getInput().setPrefSize(750, 340);
|
||||||
|
if (onFeedback4Error != null) {
|
||||||
|
// 支持反馈时显示反馈按钮和关闭
|
||||||
|
alert.putButtons(new AlertButton(HPos.LEFT, AlertButton.Action.OTHER, TimiFXUI.MULTILINGUAL.text("alert.feedback")));
|
||||||
|
alert.putButtons(AlertButton.close());
|
||||||
|
alert.setOnActionEvent(action -> {
|
||||||
|
if (action == AlertButton.Action.OTHER) {
|
||||||
|
onFeedback4Error.handler(tips + "\n" + text);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
alert.content.setPadding(Insets.EMPTY);
|
||||||
|
alert.btnPane.setPadding(Insets.EMPTY);
|
||||||
|
}
|
||||||
|
alert.autoSize().showRelativeCenter(owner);
|
||||||
|
return alert;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取反馈错误事件
|
||||||
|
*
|
||||||
|
* @return 反馈错误事件
|
||||||
|
*/
|
||||||
|
public static CallbackArg<String> getOnFeedback4Error() {
|
||||||
|
return onFeedback4Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置反馈错误事件,全局事件,设置一次即可
|
||||||
|
*
|
||||||
|
* @param onFeedback4Error 反馈错误事件
|
||||||
|
*/
|
||||||
|
public static void setOnFeedback4Error(CallbackArg<String> onFeedback4Error) {
|
||||||
|
AlertTextArea.onFeedback4Error = onFeedback4Error;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,103 @@
|
|||||||
|
package com.imyeyu.fx.ui.components.alert;
|
||||||
|
|
||||||
|
import com.imyeyu.java.bean.CallbackArg;
|
||||||
|
import javafx.scene.control.TextField;
|
||||||
|
import javafx.stage.Window;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 输入弹窗
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-01-07 15:43
|
||||||
|
*/
|
||||||
|
public class AlertTextField extends AbstractAlertInput<TextField> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 输入弹窗
|
||||||
|
*
|
||||||
|
* @param type 弹窗类型
|
||||||
|
*/
|
||||||
|
public AlertTextField(AlertType type) {
|
||||||
|
this(type, "", AlertButton.confirm(), AlertButton.cancel());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 输入弹窗
|
||||||
|
*
|
||||||
|
* @param content 提示
|
||||||
|
*/
|
||||||
|
public AlertTextField(String content) {
|
||||||
|
this(AlertType.INFORMATION, AlertType.INFORMATION.getTitle(), content, "", AlertButton.confirm(), AlertButton.cancel());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 输入弹窗
|
||||||
|
*
|
||||||
|
* @param content 提示
|
||||||
|
* @param btns 可控按钮
|
||||||
|
*/
|
||||||
|
public AlertTextField(String content, AlertButton... btns) {
|
||||||
|
this(AlertType.INFORMATION, AlertType.INFORMATION.getTitle(), content, "", btns);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 输入弹窗
|
||||||
|
*
|
||||||
|
* @param type 弹窗类型
|
||||||
|
* @param content 内容
|
||||||
|
* @param btns 可控按钮
|
||||||
|
*/
|
||||||
|
public AlertTextField(AlertType type, String content, AlertButton... btns) {
|
||||||
|
super(new TextField(), type, type.getTitle(), content, "", btns);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 输入弹窗
|
||||||
|
*
|
||||||
|
* @param type 弹窗类型
|
||||||
|
* @param title 标题
|
||||||
|
* @param content 内容
|
||||||
|
* @param text 预设输入框文本
|
||||||
|
* @param btns 可控按钮
|
||||||
|
*/
|
||||||
|
public AlertTextField(AlertType type, String title, String content, String text, AlertButton... btns) {
|
||||||
|
super(new TextField(), type, title, content, text, btns);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 输入确认弹窗
|
||||||
|
*
|
||||||
|
* @param owner 依赖窗体
|
||||||
|
* @param content 内容
|
||||||
|
* @param confirm 确认事件
|
||||||
|
* @return 弹窗对象
|
||||||
|
*/
|
||||||
|
public static AlertTextField confirm(Window owner, String content, CallbackArg<String> confirm) {
|
||||||
|
return confirm(owner, content, confirm, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 输入确认弹窗
|
||||||
|
*
|
||||||
|
* @param owner 依赖窗体
|
||||||
|
* @param content 内容
|
||||||
|
* @param confirm 确认事件
|
||||||
|
* @param otherwise 其他事件
|
||||||
|
* @return 弹窗对象
|
||||||
|
*/
|
||||||
|
public static AlertTextField confirm(Window owner, String content, CallbackArg<String> confirm, CallbackArg<String> otherwise) {
|
||||||
|
AlertTextField alert = new AlertTextField(AlertType.CONFIRMATION, content, AlertButton.yes(), AlertButton.no());
|
||||||
|
alert.setOnActionEvent(action -> {
|
||||||
|
if (action == AlertButton.Action.YES) {
|
||||||
|
confirm.handler(alert.getInput().getText());
|
||||||
|
} else {
|
||||||
|
if (otherwise != null) {
|
||||||
|
otherwise.handler(alert.getInput().getText());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
alert.autoSize().showRelativeCenter(owner);
|
||||||
|
return alert;
|
||||||
|
}
|
||||||
|
}
|
||||||
166
src/main/java/com/imyeyu/fx/ui/components/alert/AlertTips.java
Normal file
166
src/main/java/com/imyeyu/fx/ui/components/alert/AlertTips.java
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
package com.imyeyu.fx.ui.components.alert;
|
||||||
|
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.stage.Window;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 弹窗提示
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-01-07 11:29
|
||||||
|
*/
|
||||||
|
public class AlertTips extends AbstractAlert {
|
||||||
|
|
||||||
|
/** 提示标签 */
|
||||||
|
protected final Label tips;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 弹窗提示
|
||||||
|
*
|
||||||
|
* @param content 内容
|
||||||
|
*/
|
||||||
|
public AlertTips(String content) {
|
||||||
|
this(AlertType.INFORMATION, null, content, AlertButton.close());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 弹窗提示
|
||||||
|
*
|
||||||
|
* @param content 内容
|
||||||
|
* @param btns 可控按钮
|
||||||
|
*/
|
||||||
|
public AlertTips(String content, AlertButton... btns) {
|
||||||
|
this(AlertType.INFORMATION, null, content, btns);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 弹窗提示
|
||||||
|
*
|
||||||
|
* @param type 类型
|
||||||
|
* @param btns 可控按钮
|
||||||
|
*/
|
||||||
|
public AlertTips(AlertType type, AlertButton... btns) {
|
||||||
|
this(type, null, "", btns);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 弹窗提示
|
||||||
|
*
|
||||||
|
* @param type 类型
|
||||||
|
* @param content 内容
|
||||||
|
*/
|
||||||
|
public AlertTips(AlertType type, String content) {
|
||||||
|
this(type, null, content, AlertButton.close());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 弹窗提示
|
||||||
|
*
|
||||||
|
* @param type 类型
|
||||||
|
* @param content 内容
|
||||||
|
* @param btns 可控按钮
|
||||||
|
*/
|
||||||
|
public AlertTips(AlertType type, String content, AlertButton... btns) {
|
||||||
|
this(type, null, content, btns);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 弹窗提示
|
||||||
|
*
|
||||||
|
* @param type 类型
|
||||||
|
* @param title 标题
|
||||||
|
* @param content 内容
|
||||||
|
* @param btns 可控按钮
|
||||||
|
*/
|
||||||
|
public AlertTips(AlertType type, String title, String content, AlertButton... btns) {
|
||||||
|
tips = new Label(content);
|
||||||
|
tips.setPadding(PADDING_CONTENT);
|
||||||
|
tips.setWrapText(true);
|
||||||
|
tips.setPrefWidth(360);
|
||||||
|
|
||||||
|
root.setCenter(tips);
|
||||||
|
setResizable(false);
|
||||||
|
setType(type);
|
||||||
|
|
||||||
|
if (title != null && !title.trim().equals("")) {
|
||||||
|
setTitle(title);
|
||||||
|
}
|
||||||
|
putButtons(btns);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置提示
|
||||||
|
*
|
||||||
|
* @param tips 提示内容
|
||||||
|
*/
|
||||||
|
public void setTips(String tips) {
|
||||||
|
this.tips.setText(tips);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取提示标签
|
||||||
|
*
|
||||||
|
* @return 提示标签
|
||||||
|
*/
|
||||||
|
public Label getTips() {
|
||||||
|
return tips;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 快速构造 ----------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快速弹出提示
|
||||||
|
*
|
||||||
|
* @param owner 显示相对窗体
|
||||||
|
* @param content 提示内容
|
||||||
|
* @return 弹窗对象
|
||||||
|
*/
|
||||||
|
public static AlertTips info(Window owner, String content) {
|
||||||
|
AlertTips alert = new AlertTips(AlertType.INFORMATION, content);
|
||||||
|
alert.autoSize();
|
||||||
|
alert.showRelativeCenter(owner);
|
||||||
|
return alert;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快速弹出警告
|
||||||
|
*
|
||||||
|
* @param owner 显示相对窗体
|
||||||
|
* @param content 警告内容
|
||||||
|
* @return 弹窗对象
|
||||||
|
*/
|
||||||
|
public static AlertTips warn(Window owner, String content) {
|
||||||
|
AlertTips alert = new AlertTips(AlertType.WARNING, content);
|
||||||
|
alert.autoSize();
|
||||||
|
alert.showRelativeCenter(owner);
|
||||||
|
return alert;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快速弹出严重警告
|
||||||
|
*
|
||||||
|
* @param owner 显示相对窗体
|
||||||
|
* @param content 警告内容
|
||||||
|
* @return 弹窗对象
|
||||||
|
*/
|
||||||
|
public static AlertTips warnDanger(Window owner, String content) {
|
||||||
|
AlertTips alert = new AlertTips(AlertType.WARNING_DANGER, content);
|
||||||
|
alert.autoSize();
|
||||||
|
alert.showRelativeCenter(owner);
|
||||||
|
return alert;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快速弹出错误
|
||||||
|
*
|
||||||
|
* @param owner 显示相对窗体
|
||||||
|
* @param content 错误内容
|
||||||
|
* @return 弹窗对象
|
||||||
|
*/
|
||||||
|
public static AlertTips error(Window owner, String content) {
|
||||||
|
AlertTips alert = new AlertTips(AlertType.ERROR, content);
|
||||||
|
alert.autoSize();
|
||||||
|
alert.showRelativeCenter(owner);
|
||||||
|
return alert;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
package com.imyeyu.fx.ui.components.alert;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 弹窗类型
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-01-07 10:51
|
||||||
|
*/
|
||||||
|
public enum AlertType {
|
||||||
|
|
||||||
|
/** 信息 */
|
||||||
|
INFORMATION(new Image("timifx/dialog-information16x.png"), TimiFXUI.MULTILINGUAL.text("alert.title.information", "信息")),
|
||||||
|
|
||||||
|
/** 警告 */
|
||||||
|
WARNING(new Image("timifx/dialog-warning16x.png"), TimiFXUI.MULTILINGUAL.text("warning", "警告")),
|
||||||
|
|
||||||
|
/** 危险警告 */
|
||||||
|
WARNING_DANGER(new Image("timifx/dialog-warning-danger16x.png"), TimiFXUI.MULTILINGUAL.text("warning", "危险警告")),
|
||||||
|
|
||||||
|
/** 询问 */
|
||||||
|
CONFIRMATION(new Image("timifx/dialog-confirmation16x.png"), TimiFXUI.MULTILINGUAL.text("confirmation", "询问")),
|
||||||
|
|
||||||
|
/** 错误 */
|
||||||
|
ERROR(new Image("timifx/dialog-error16x.png"), TimiFXUI.MULTILINGUAL.text("error", "错误"));
|
||||||
|
|
||||||
|
final Image icon;
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
AlertType(Image icon, String title) {
|
||||||
|
this.icon = icon;
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取弹窗类型的图标
|
||||||
|
*
|
||||||
|
* @return 弹窗类型图标
|
||||||
|
*/
|
||||||
|
public Image getIcon() {
|
||||||
|
return icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取弹窗类型的标题
|
||||||
|
*
|
||||||
|
* @return 弹窗类型标题
|
||||||
|
*/
|
||||||
|
public String getTitle() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
/** 弹窗组件 */
|
||||||
|
package com.imyeyu.fx.ui.components.alert;
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
/** 扩展组件库 */
|
||||||
|
package com.imyeyu.fx.ui.components;
|
||||||
@ -0,0 +1,150 @@
|
|||||||
|
package com.imyeyu.fx.ui.components.popup;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import com.imyeyu.fx.ui.components.popup.tips.AbstractPopupTips;
|
||||||
|
import com.imyeyu.java.bean.CallbackArg;
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
|
import javafx.event.EventHandler;
|
||||||
|
import javafx.geometry.Bounds;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.input.MouseEvent;
|
||||||
|
import javafx.scene.layout.Pane;
|
||||||
|
import javafx.stage.Popup;
|
||||||
|
|
||||||
|
import java.awt.MouseInfo;
|
||||||
|
import java.awt.Point;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 抽象弹出提示服务,你需要实现 {@link #createRoot()},为 Popup 提供标准根节点。
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2021-12-04 14:26
|
||||||
|
*/
|
||||||
|
public abstract class AbstractPopupTipsService<T extends Pane> extends Popup implements TimiFXUI {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组件安装提示后会把提示对象({@link AbstractPopupTips})添加到 {@link Node#getProperties()} 中。
|
||||||
|
* 可以通过此 KEY 获取该对象
|
||||||
|
*/
|
||||||
|
protected static final String TIPS_KEY = "TIMI_FX_POPUP_TIPS";
|
||||||
|
|
||||||
|
/** 根布局 */
|
||||||
|
protected T root;
|
||||||
|
|
||||||
|
/** 显示到跟布局中 */
|
||||||
|
protected CallbackArg<Node> showOnRoot;
|
||||||
|
|
||||||
|
/** 显示提示的节点 */
|
||||||
|
protected final ObjectProperty<Node> showingTipsNode;
|
||||||
|
|
||||||
|
/** 默认构造 */
|
||||||
|
protected AbstractPopupTipsService() {
|
||||||
|
showingTipsNode = new SimpleObjectProperty<>();
|
||||||
|
showingTipsNode.addListener((obs, o, newNode) -> {
|
||||||
|
if (newNode != null) {
|
||||||
|
if (newNode.getProperties().get(TIPS_KEY) instanceof AbstractPopupTips<?> tips) {
|
||||||
|
if (showOnRoot == null) {
|
||||||
|
root.getChildren().setAll(tips.getNode());
|
||||||
|
} else {
|
||||||
|
showOnRoot.handler(tips.getNode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
root = createRoot();
|
||||||
|
|
||||||
|
getContent().add(root);
|
||||||
|
setAutoHide(true);
|
||||||
|
getScene().setFill(null);
|
||||||
|
|
||||||
|
// ---------- 事件 ----------
|
||||||
|
|
||||||
|
setOnHidden(e -> showingTipsNode.set(null));
|
||||||
|
// 显示提示监听
|
||||||
|
showingTipsNode.addListener((obs, oldNode, newNode) -> {
|
||||||
|
autoHideProperty().unbind();
|
||||||
|
if (newNode != null) {
|
||||||
|
Object newObj = newNode.getProperties().get(TIPS_KEY);
|
||||||
|
if (newObj instanceof AbstractPopupTips<?> newTips) {
|
||||||
|
if (newTips.isEnable()) {
|
||||||
|
// 已启用
|
||||||
|
autoHideProperty().bind(newTips.keepShowProperty());
|
||||||
|
// 显示
|
||||||
|
Point p = MouseInfo.getPointerInfo().getLocation();
|
||||||
|
if (!isShowing()) {
|
||||||
|
super.show(newNode.getScene().getWindow(), p.x + 16, p.y + 12);
|
||||||
|
}
|
||||||
|
setOpacity(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (oldNode != null) {
|
||||||
|
Object oldObj = oldNode.getProperties().get(TIPS_KEY);
|
||||||
|
if (oldObj instanceof AbstractPopupTips<?> oldTips) {
|
||||||
|
setOpacity(0);
|
||||||
|
oldTips.setKeepShow(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造根容器
|
||||||
|
*
|
||||||
|
* @return 根容器
|
||||||
|
*/
|
||||||
|
protected abstract T createRoot();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 强制显示并保持
|
||||||
|
*
|
||||||
|
* @param node 安装了提示的组件
|
||||||
|
*/
|
||||||
|
public void showAndKeep(Node node) {
|
||||||
|
Object obj = node.getProperties().get(TIPS_KEY);
|
||||||
|
if (obj instanceof AbstractPopupTips<?> tips) {
|
||||||
|
tips.setKeepShow(true);
|
||||||
|
showingTipsNode.set(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为组件安装弹窗提示
|
||||||
|
*
|
||||||
|
* @param node 组件
|
||||||
|
* @param popupTips 弹窗数据
|
||||||
|
*/
|
||||||
|
public void install(Node node, AbstractPopupTips<?> popupTips) {
|
||||||
|
node.getProperties().put(TIPS_KEY, popupTips);
|
||||||
|
// 指向事件
|
||||||
|
node.hoverProperty().addListener((obs, o, isHover) -> {
|
||||||
|
if (isHover) {
|
||||||
|
showingTipsNode.set(node);
|
||||||
|
} else {
|
||||||
|
if (!popupTips.isKeepShow()) {
|
||||||
|
showingTipsNode.set(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 移动事件
|
||||||
|
EventHandler<MouseEvent> mouseEvent = e -> {
|
||||||
|
if (!popupTips.isKeepShow()) {
|
||||||
|
Bounds nodeBounds = node.getLayoutBounds();
|
||||||
|
if (!popupTips.isEnable() || e.getX() < 0 || nodeBounds.getWidth() < e.getX() || e.getY() < 0 || nodeBounds.getHeight() < e.getY()) {
|
||||||
|
// 提示没有开启或光标移出组件
|
||||||
|
showingTipsNode.set(null);
|
||||||
|
} else {
|
||||||
|
showingTipsNode.set(node);
|
||||||
|
if (isShowing()) {
|
||||||
|
setX(e.getScreenX() + 12);
|
||||||
|
setY(e.getScreenY() + 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
node.addEventFilter(MouseEvent.MOUSE_MOVED, mouseEvent);
|
||||||
|
node.addEventFilter(MouseEvent.MOUSE_DRAGGED, mouseEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,152 @@
|
|||||||
|
package com.imyeyu.fx.ui.components.popup;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.ui.components.popup.tips.AbstractPopupTips;
|
||||||
|
import com.imyeyu.fx.ui.components.popup.tips.PopupTipsImage;
|
||||||
|
import com.imyeyu.fx.ui.components.popup.tips.PopupTipsLabel;
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.beans.binding.StringBinding;
|
||||||
|
import javafx.beans.property.BooleanProperty;
|
||||||
|
import javafx.beans.property.StringProperty;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.effect.DropShadow;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.scene.layout.Background;
|
||||||
|
import javafx.scene.layout.BorderPane;
|
||||||
|
import javafx.scene.layout.StackPane;
|
||||||
|
import javafx.scene.paint.Color;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 弹窗提示
|
||||||
|
* <p>示例:
|
||||||
|
* <pre>
|
||||||
|
* PopupTipsService.installText(node, "文本提示");
|
||||||
|
* PopupTipsService.installImage(node, new Image("/tips.png")); // 图片提示
|
||||||
|
* PopupTipsService.install(node, new AbstractPopupTips<>(new Button("自定义组件提示")));
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2021-04-22 19:48
|
||||||
|
*/
|
||||||
|
public final class PopupTipsService extends AbstractPopupTipsService<StackPane> {
|
||||||
|
|
||||||
|
private static PopupTipsService service; // 单例对象
|
||||||
|
|
||||||
|
/** 主布局 */
|
||||||
|
private BorderPane main;
|
||||||
|
|
||||||
|
private PopupTipsService() {
|
||||||
|
showOnRoot = node -> main.setCenter(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected StackPane createRoot() {
|
||||||
|
DropShadow shadow = new DropShadow();
|
||||||
|
shadow.setRadius(6);
|
||||||
|
shadow.setOffsetX(0);
|
||||||
|
shadow.setOffsetY(0);
|
||||||
|
shadow.setSpread(.05);
|
||||||
|
shadow.setColor(Color.valueOf("#3333"));
|
||||||
|
|
||||||
|
main = new BorderPane();
|
||||||
|
main.setEffect(shadow);
|
||||||
|
|
||||||
|
StackPane root = new StackPane();
|
||||||
|
root.setBackground(Background.EMPTY);
|
||||||
|
root.getChildren().add(main);
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单例对象
|
||||||
|
*
|
||||||
|
* @return 单例对象
|
||||||
|
*/
|
||||||
|
public static synchronized PopupTipsService getInstance() {
|
||||||
|
if (service == null) {
|
||||||
|
service = new PopupTipsService();
|
||||||
|
}
|
||||||
|
return service;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为组件安装文本弹窗提示
|
||||||
|
*
|
||||||
|
* @param node 组件
|
||||||
|
* @param text 图片
|
||||||
|
* @return 标签弹出提示
|
||||||
|
*/
|
||||||
|
public static PopupTipsLabel installText(Node node, String text) {
|
||||||
|
PopupTipsLabel tips = new PopupTipsLabel(text);
|
||||||
|
installTips(node, tips);
|
||||||
|
return tips;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为组件安装绑定文本弹窗提示
|
||||||
|
*
|
||||||
|
* @param node 组件
|
||||||
|
* @param property 文本绑定属性
|
||||||
|
* @return 标签弹出提示
|
||||||
|
*/
|
||||||
|
public static PopupTipsLabel installBindingText(Node node, StringProperty property) {
|
||||||
|
PopupTipsLabel tips = new PopupTipsLabel();
|
||||||
|
tips.getNode().textProperty().bind(property);
|
||||||
|
tips.enableProperty().bind(property.isNotEmpty());
|
||||||
|
installTips(node, tips);
|
||||||
|
return tips;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为组件安装绑定文本弹窗提示
|
||||||
|
*
|
||||||
|
* @param node 组件
|
||||||
|
* @param binding 文本绑定属性
|
||||||
|
* @return 标签弹出提示
|
||||||
|
*/
|
||||||
|
public static PopupTipsLabel installBindingText(Node node, StringBinding binding) {
|
||||||
|
PopupTipsLabel tips = new PopupTipsLabel();
|
||||||
|
tips.getNode().textProperty().bind(binding);
|
||||||
|
tips.enableProperty().bind(binding.isNotEmpty());
|
||||||
|
installTips(node, tips);
|
||||||
|
return tips;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为组件安装绑定文本弹窗提示
|
||||||
|
*
|
||||||
|
* @param node 组件
|
||||||
|
* @param when 条件
|
||||||
|
* @param then 条件 true 时显示文本
|
||||||
|
* @param otherwise 条件 false 时显示文本
|
||||||
|
* @return 标签弹出提示
|
||||||
|
*/
|
||||||
|
public static PopupTipsLabel installBindingText(Node node, BooleanProperty when, String then, String otherwise) {
|
||||||
|
PopupTipsLabel tips = new PopupTipsLabel();
|
||||||
|
tips.getNode().textProperty().bind(Bindings.when(when).then(then).otherwise(otherwise));
|
||||||
|
installTips(node, tips);
|
||||||
|
return tips;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为组件安装图片弹窗提示
|
||||||
|
*
|
||||||
|
* @param node 组件
|
||||||
|
* @param image 图片
|
||||||
|
* @return 图片弹出提示
|
||||||
|
*/
|
||||||
|
public static PopupTipsImage installImage(Node node, Image image) {
|
||||||
|
PopupTipsImage tips = new PopupTipsImage(image);
|
||||||
|
installTips(node, tips);
|
||||||
|
return tips;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为组件安装通用提示
|
||||||
|
*
|
||||||
|
* @param node 组件
|
||||||
|
* @param tips 提示
|
||||||
|
*/
|
||||||
|
public static void installTips(Node node, AbstractPopupTips<?> tips) {
|
||||||
|
getInstance().install(node, tips);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
/** 弹出提示服务,指向组件弹出提示功能,此提示没有延时,且跟随鼠标 */
|
||||||
|
package com.imyeyu.fx.ui.components.popup;
|
||||||
@ -0,0 +1,123 @@
|
|||||||
|
package com.imyeyu.fx.ui.components.popup.tips;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import javafx.beans.property.BooleanProperty;
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 抽象弹出提示对象
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2023-02-13 12:37
|
||||||
|
*/
|
||||||
|
public class AbstractPopupTips<T extends Node> implements TimiFXUI {
|
||||||
|
|
||||||
|
/** 是否启用 */
|
||||||
|
protected final BooleanProperty enable;
|
||||||
|
|
||||||
|
/** 是否保持显示 */
|
||||||
|
protected final BooleanProperty keepShow;
|
||||||
|
|
||||||
|
/** 组件 */
|
||||||
|
protected final ObjectProperty<T> node;
|
||||||
|
|
||||||
|
/** 默认构造 */
|
||||||
|
public AbstractPopupTips() {
|
||||||
|
this(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标准构造
|
||||||
|
*
|
||||||
|
* @param node 提示内容组件
|
||||||
|
*/
|
||||||
|
public AbstractPopupTips(T node) {
|
||||||
|
enable = new SimpleBooleanProperty(true);
|
||||||
|
keepShow = new SimpleBooleanProperty(false);
|
||||||
|
this.node = new SimpleObjectProperty<>(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取是否启用提示
|
||||||
|
*
|
||||||
|
* @return true 为启用
|
||||||
|
*/
|
||||||
|
public boolean isEnable() {
|
||||||
|
return enable.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取是否启用提示属性
|
||||||
|
*
|
||||||
|
* @return 是否启用提示属性
|
||||||
|
*/
|
||||||
|
public BooleanProperty enableProperty() {
|
||||||
|
return enable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置是否启用提示
|
||||||
|
*
|
||||||
|
* @param enable true 为启用
|
||||||
|
*/
|
||||||
|
public void setEnable(boolean enable) {
|
||||||
|
this.enable.set(enable);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取是否保持显示,鼠标移出触发组件时也保持显示
|
||||||
|
*
|
||||||
|
* @return true 为保持显示
|
||||||
|
*/
|
||||||
|
public boolean isKeepShow() {
|
||||||
|
return keepShow.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取是否保持显示属性
|
||||||
|
*
|
||||||
|
* @return 是否保持显示属性
|
||||||
|
*/
|
||||||
|
public BooleanProperty keepShowProperty() {
|
||||||
|
return keepShow;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置是否保持显示,鼠标移出触发组件时也保持显示
|
||||||
|
*
|
||||||
|
* @param keepShow true 为保持显示
|
||||||
|
*/
|
||||||
|
public void setKeepShow(boolean keepShow) {
|
||||||
|
this.keepShow.set(keepShow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取弹出显示的组件
|
||||||
|
*
|
||||||
|
* @return 弹出显示组件
|
||||||
|
*/
|
||||||
|
public T getNode() {
|
||||||
|
return node.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取弹出显示组件属性
|
||||||
|
*
|
||||||
|
* @return 弹出显示组件属性
|
||||||
|
*/
|
||||||
|
public ObjectProperty<T> nodeProperty() {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置弹出显示组件
|
||||||
|
*
|
||||||
|
* @param node 弹出显示组件
|
||||||
|
*/
|
||||||
|
public void setNode(T node) {
|
||||||
|
this.node.set(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,105 @@
|
|||||||
|
package com.imyeyu.fx.ui.components.popup.tips;
|
||||||
|
|
||||||
|
import com.imyeyu.io.IO;
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.beans.binding.BooleanBinding;
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
|
import javafx.scene.layout.Background;
|
||||||
|
import javafx.scene.layout.Border;
|
||||||
|
import javafx.scene.layout.StackPane;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图片弹出提示
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2023-02-13 13:09
|
||||||
|
*/
|
||||||
|
public class PopupTipsImage extends AbstractPopupTips<StackPane> {
|
||||||
|
|
||||||
|
private final ImageView imageView;
|
||||||
|
|
||||||
|
/** 默认构造 */
|
||||||
|
public PopupTipsImage() {
|
||||||
|
this((Image) null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认构造
|
||||||
|
*
|
||||||
|
* @param url 图片 URL
|
||||||
|
*/
|
||||||
|
public PopupTipsImage(String url) {
|
||||||
|
this(new Image(url));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认构造
|
||||||
|
*
|
||||||
|
* @param image 图片对象
|
||||||
|
*/
|
||||||
|
public PopupTipsImage(Image image) {
|
||||||
|
super(new StackPane());
|
||||||
|
getNode().getChildren().setAll(imageView = new ImageView(image));
|
||||||
|
|
||||||
|
BooleanBinding emptyImage = imageView.imageProperty().isNull();
|
||||||
|
getNode().borderProperty().bind(Bindings.when(emptyImage).then(Border.EMPTY).otherwise(Stroke.DEFAULT));
|
||||||
|
getNode().backgroundProperty().bind(Bindings.when(emptyImage).then(Background.EMPTY).otherwise(BG.DEFAULT));
|
||||||
|
enable.bind(emptyImage.not());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置显示图片
|
||||||
|
*
|
||||||
|
* @param url 图片地址
|
||||||
|
*/
|
||||||
|
public void setImage(String url) {
|
||||||
|
imageView.setImage(new Image(url));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置显示图片
|
||||||
|
*
|
||||||
|
* @param file 图片文件
|
||||||
|
* @throws FileNotFoundException 找不到文件
|
||||||
|
*/
|
||||||
|
public void setImage(File file) throws FileNotFoundException {
|
||||||
|
imageView.setImage(new Image(IO.getInputStream(file)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置显示图片
|
||||||
|
*
|
||||||
|
* @param image 图片
|
||||||
|
*/
|
||||||
|
public void setImage(Image image) {
|
||||||
|
imageView.setImage(image);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 清除图片 */
|
||||||
|
public void clear() {
|
||||||
|
imageView.setImage(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取显示的图片
|
||||||
|
*
|
||||||
|
* @return 图片
|
||||||
|
*/
|
||||||
|
public Image getImage() {
|
||||||
|
return imageView.getImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取图片属性
|
||||||
|
*
|
||||||
|
* @return 图片属性
|
||||||
|
*/
|
||||||
|
public ObjectProperty<Image> imageProperty() {
|
||||||
|
return imageView.imageProperty();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
package com.imyeyu.fx.ui.components.popup.tips;
|
||||||
|
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文本标签弹出提示
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2023-02-13 12:39
|
||||||
|
*/
|
||||||
|
public class PopupTipsLabel extends AbstractPopupTips<Label> {
|
||||||
|
|
||||||
|
private static final Insets PADDING_TEXT = new Insets(3, 6, 3, 6);
|
||||||
|
|
||||||
|
/** 默认构造 */
|
||||||
|
public PopupTipsLabel() {
|
||||||
|
this("");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认构造
|
||||||
|
*
|
||||||
|
* @param text 显示文本
|
||||||
|
*/
|
||||||
|
public PopupTipsLabel(String text) {
|
||||||
|
super(new Label(text));
|
||||||
|
|
||||||
|
Label label = getNode();
|
||||||
|
label.setBorder(Stroke.DEFAULT);
|
||||||
|
label.setPadding(PADDING_TEXT);
|
||||||
|
label.setWrapText(true);
|
||||||
|
label.setMaxWidth(520);
|
||||||
|
label.setBackground(BG.DEFAULT);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
/** 弹出提示组件 */
|
||||||
|
package com.imyeyu.fx.ui.components.popup.tips;
|
||||||
@ -0,0 +1,104 @@
|
|||||||
|
package com.imyeyu.fx.ui.components.table;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import javafx.beans.property.Property;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.TableCell;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 抽象表格绑定单元格
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2023-03-14 14:42
|
||||||
|
*
|
||||||
|
* @param <S> 表格数据类型
|
||||||
|
* @param <T> 数据属性类型
|
||||||
|
* @param <P> 绑定类型
|
||||||
|
* @param <N> 组件类型
|
||||||
|
*/
|
||||||
|
public abstract class BindingTableCell<S, T, P extends Property<T>, N extends Node> extends TableCell<S, T> implements TimiFXUI {
|
||||||
|
|
||||||
|
/** 组件 */
|
||||||
|
protected final N node;
|
||||||
|
|
||||||
|
/** 当前绑定 */
|
||||||
|
protected P nowBind;
|
||||||
|
|
||||||
|
/** 默认构造器 */
|
||||||
|
public BindingTableCell() {
|
||||||
|
node = component();
|
||||||
|
onInit(node);
|
||||||
|
node.focusedProperty().addListener((obs, o, isFocused) -> {
|
||||||
|
if (isFocused) {
|
||||||
|
getTableView().getSelectionModel().clearAndSelect(getIndex());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setPadding(Insets.EMPTY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建组件
|
||||||
|
*
|
||||||
|
* @return 组件
|
||||||
|
*/
|
||||||
|
protected abstract N component();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组件属性绑定类
|
||||||
|
*
|
||||||
|
* @param node 组件
|
||||||
|
* @return 属性绑定类
|
||||||
|
*/
|
||||||
|
protected abstract P componentValue(N node);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 双向绑定属性,组件对数据的双向绑定属性
|
||||||
|
*
|
||||||
|
* @param s 数据对象
|
||||||
|
* @return 监听属性
|
||||||
|
*/
|
||||||
|
protected abstract P property(S s);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void updateItem(T item, boolean empty) {
|
||||||
|
super.updateItem(item, empty);
|
||||||
|
if (empty || item == null || getTableRow() == null || getTableRow().getItem() == null) {
|
||||||
|
setGraphic(null);
|
||||||
|
} else {
|
||||||
|
// 动态绑定
|
||||||
|
P thisBind = property(getTableRow().getItem());
|
||||||
|
if (nowBind == null) {
|
||||||
|
nowBind = thisBind;
|
||||||
|
componentValue(node).bindBidirectional(nowBind);
|
||||||
|
onUpdateBinding(getTableRow().getItem());
|
||||||
|
} else {
|
||||||
|
if (nowBind != thisBind) {
|
||||||
|
componentValue(node).unbindBidirectional(nowBind);
|
||||||
|
nowBind = thisBind;
|
||||||
|
componentValue(node).bindBidirectional(nowBind);
|
||||||
|
onUpdateBinding(getTableRow().getItem());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setGraphic(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造完成触发
|
||||||
|
*
|
||||||
|
* @param node 组件
|
||||||
|
*/
|
||||||
|
protected void onInit(N node) {
|
||||||
|
// 子类实现
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发生更新绑定时触发
|
||||||
|
*
|
||||||
|
* @param s 数据对象
|
||||||
|
*/
|
||||||
|
protected void onUpdateBinding(S s) {
|
||||||
|
// 子类实现
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
package com.imyeyu.fx.ui.components.table;
|
||||||
|
|
||||||
|
import javafx.beans.property.BooleanProperty;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.control.CheckBox;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 复选框单元格
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2023-03-14 14:56
|
||||||
|
*/
|
||||||
|
public abstract class CheckBoxTableCell<S> extends BindingTableCell<S, Boolean, BooleanProperty, CheckBox> {
|
||||||
|
|
||||||
|
/** 默认构造器 */
|
||||||
|
public CheckBoxTableCell() {
|
||||||
|
setAlignment(Pos.CENTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected final CheckBox component() {
|
||||||
|
return new CheckBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected final BooleanProperty componentValue(CheckBox node) {
|
||||||
|
return node.selectedProperty();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
package com.imyeyu.fx.ui.components.table;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import javafx.beans.property.StringProperty;
|
||||||
|
import javafx.scene.control.TextField;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表格可编辑单元格,相对原始的可编辑效果,此单元格没有输入框的样式,就像 Excel 那样
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* TableColumn<String, String> col = new TableColumn<>("col");
|
||||||
|
* col.setCellFactory(cell -> new TextFieldTableCell<>() {
|
||||||
|
*
|
||||||
|
* @Override
|
||||||
|
* protected StringProperty property(Item item) {
|
||||||
|
* // 给出此可编辑单元格的具体映射属性
|
||||||
|
* return item.valueProperty();
|
||||||
|
* }
|
||||||
|
* });
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-05-09 22:21
|
||||||
|
*/
|
||||||
|
public abstract class TextFieldTableCell<S> extends BindingTableCell<S, String, StringProperty, TextField> implements TimiFXUI {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected final TextField component() {
|
||||||
|
TextField textField = new TextField();
|
||||||
|
textField.getStyleClass().add(CSS.BORDER_N);
|
||||||
|
return textField;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected final StringProperty componentValue(TextField textField) {
|
||||||
|
return textField.textProperty();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
/** 表格属性绑定单元格 */
|
||||||
|
package com.imyeyu.fx.ui.components.table;
|
||||||
53
src/main/resources/lang/timi-fx-ui/de_DE.lang
Normal file
53
src/main/resources/lang/timi-fx-ui/de_DE.lang
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
alert.apply=@Anwenden
|
||||||
|
alert.cancel=@Abbrechen
|
||||||
|
alert.close=@Schließen
|
||||||
|
alert.confirm=@Bestätigen
|
||||||
|
alert.feedback=Feedback
|
||||||
|
alert.finish=@Fertigstellen
|
||||||
|
alert.next=Nächster Schritt
|
||||||
|
alert.no=@No
|
||||||
|
alert.ok=@OK
|
||||||
|
alert.previous=Vorheriger Schritt
|
||||||
|
alert.save=@Speichern
|
||||||
|
alert.skip=@Skip
|
||||||
|
alert.title.information=@Info
|
||||||
|
alert.yes=@Ja
|
||||||
|
apply=Anwendung
|
||||||
|
cancel=Abbrechen
|
||||||
|
close=schließen
|
||||||
|
confirm=bestätigen
|
||||||
|
confirmation=Anfrage
|
||||||
|
copy=Kopie
|
||||||
|
cut=Schere
|
||||||
|
delete=löschen
|
||||||
|
error=Fehler
|
||||||
|
file.destroy=Sind Sie sicher, dass Sie die ausgewählten Elemente löschen?
|
||||||
|
file.mkdir=Einen neuen Ordner erstellen
|
||||||
|
file.rename=@Umbenennen
|
||||||
|
file.select=Datei auswählen
|
||||||
|
file.show_hide=Versteckte Dateien anzeigen
|
||||||
|
file.tips.destroy_fail=Löschen fehlgeschlagen {0}
|
||||||
|
file.tips.not_found_target=Ziel existiert nicht {0}
|
||||||
|
file.tips.select_directory=Bitte wählen Sie einen Ordner
|
||||||
|
finish=komplett
|
||||||
|
home=Startseite
|
||||||
|
info=Informationen
|
||||||
|
loading=Laden
|
||||||
|
name=Name
|
||||||
|
no=nein
|
||||||
|
now=heute
|
||||||
|
now_tick=dieser Moment
|
||||||
|
ok=gut
|
||||||
|
paste=Paste
|
||||||
|
redo=Wiederholen
|
||||||
|
refresh=Aktualisieren
|
||||||
|
rename=umbenennen
|
||||||
|
replace=ersetzen
|
||||||
|
replace_all=Alle ersetzen
|
||||||
|
save=konservieren
|
||||||
|
skip=überspringen
|
||||||
|
undo=widerrufen
|
||||||
|
version.fail={0}-Überprüfen auf neue Version fehlgeschlagen, klicken Sie auf Wiederholen
|
||||||
|
warning=Warnung
|
||||||
|
wrap=Zeilenumbruch
|
||||||
|
yes=ja
|
||||||
53
src/main/resources/lang/timi-fx-ui/en_US.lang
Normal file
53
src/main/resources/lang/timi-fx-ui/en_US.lang
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
alert.apply=@Apply
|
||||||
|
alert.cancel=@Cancel
|
||||||
|
alert.close=@Close
|
||||||
|
alert.confirm=@Confirm
|
||||||
|
alert.feedback=Feedback
|
||||||
|
alert.finish=@Finish
|
||||||
|
alert.next=Next step
|
||||||
|
alert.no=@No
|
||||||
|
alert.ok=@OK
|
||||||
|
alert.previous=Previous step
|
||||||
|
alert.save=@Save
|
||||||
|
alert.skip=@Skip
|
||||||
|
alert.title.information=@Info
|
||||||
|
alert.yes=@Yes
|
||||||
|
apply=Apply
|
||||||
|
cancel=Cancel
|
||||||
|
close=Close
|
||||||
|
confirm=Confirm
|
||||||
|
confirmation=Inquiry
|
||||||
|
copy=Copy
|
||||||
|
cut=Cut
|
||||||
|
delete=Delete
|
||||||
|
error=Error
|
||||||
|
file.destroy=Are you sure to delete the selected items?
|
||||||
|
file.mkdir=Create a new folder
|
||||||
|
file.rename=@Rename
|
||||||
|
file.select=Select file
|
||||||
|
file.show_hide=Show hidden files
|
||||||
|
file.tips.destroy_fail=Delete failed {0}
|
||||||
|
file.tips.not_found_target=Target does not exist {0}
|
||||||
|
file.tips.select_directory=Please select a folder
|
||||||
|
finish=Finish
|
||||||
|
home=Home
|
||||||
|
info=Info
|
||||||
|
loading=Loading
|
||||||
|
name=Name
|
||||||
|
no=No
|
||||||
|
now=Now
|
||||||
|
now_tick=This moment
|
||||||
|
ok=OK
|
||||||
|
paste=Paste
|
||||||
|
redo=Redo
|
||||||
|
refresh=Refresh
|
||||||
|
rename=Rename
|
||||||
|
replace=Replace
|
||||||
|
replace_all=Replace All
|
||||||
|
save=Preserve
|
||||||
|
skip=Skip
|
||||||
|
undo=Undo
|
||||||
|
version.fail={0} - Check for new version failed, click retry
|
||||||
|
warning=Warning
|
||||||
|
wrap=Wrap
|
||||||
|
yes=Yes
|
||||||
53
src/main/resources/lang/timi-fx-ui/ja_JP.lang
Normal file
53
src/main/resources/lang/timi-fx-ui/ja_JP.lang
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
alert.apply=@apply
|
||||||
|
alert.cancel=@cancel
|
||||||
|
alert.close=@close
|
||||||
|
alert.confirm=@confirm
|
||||||
|
alert.feedback=フィードバック
|
||||||
|
alert.finish=@finish
|
||||||
|
alert.next=次のステップ
|
||||||
|
alert.no=@no
|
||||||
|
alert.ok=@ok
|
||||||
|
alert.previous=前へ
|
||||||
|
alert.save=@save
|
||||||
|
alert.skip=@skip
|
||||||
|
alert.title.information=@info
|
||||||
|
alert.yes=@yes
|
||||||
|
apply=適用#テキヨウ#
|
||||||
|
cancel=キャンセル
|
||||||
|
close=閉じる
|
||||||
|
confirm=確認
|
||||||
|
confirmation=に質問
|
||||||
|
copy=レプリケーション
|
||||||
|
cut=せん断
|
||||||
|
delete=削除#サクジョ#
|
||||||
|
error=エラー
|
||||||
|
file.destroy=オプションの削除を確認しますか?
|
||||||
|
file.mkdir=新規フォルダ
|
||||||
|
file.rename=@rename
|
||||||
|
file.select=ファイルを選択
|
||||||
|
file.show_hide=隠しファイルを表示
|
||||||
|
file.tips.destroy_fail=削除に失敗しました{0}
|
||||||
|
file.tips.not_found_target=ターゲットは存在しません{0}
|
||||||
|
file.tips.select_directory=フォルダを選択してください
|
||||||
|
finish=完了
|
||||||
|
home=ホーム・ページ
|
||||||
|
info=情報#ジョウホウ#
|
||||||
|
loading=ロード中..。
|
||||||
|
name=の名前をあげる
|
||||||
|
no=いいえ
|
||||||
|
now=現在
|
||||||
|
now_tick=今は
|
||||||
|
ok=よし
|
||||||
|
paste=貼り付け
|
||||||
|
redo=やり直す
|
||||||
|
refresh=リフレッシュ
|
||||||
|
rename=名前を変更
|
||||||
|
replace=置換
|
||||||
|
replace_all=すべて置換
|
||||||
|
save=保存#ホゾン#
|
||||||
|
skip=スキップ
|
||||||
|
undo=元に戻す
|
||||||
|
version.fail=-新しいバージョンのチェックに失敗しました。再試行をクリックしてください
|
||||||
|
warning=に警告
|
||||||
|
wrap=折り返し
|
||||||
|
yes=はい
|
||||||
53
src/main/resources/lang/timi-fx-ui/ko_KR.lang
Normal file
53
src/main/resources/lang/timi-fx-ui/ko_KR.lang
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
alert.apply=@apply
|
||||||
|
alert.cancel=@cancel
|
||||||
|
alert.close=@close
|
||||||
|
alert.confirm=@confirm
|
||||||
|
alert.feedback=피드백
|
||||||
|
alert.finish=@finish
|
||||||
|
alert.next=다음 단계
|
||||||
|
alert.no=@no
|
||||||
|
alert.ok=@ok
|
||||||
|
alert.previous=이전 단계
|
||||||
|
alert.save=@save
|
||||||
|
alert.skip=@skip
|
||||||
|
alert.title.information=@info
|
||||||
|
alert.yes=@yes
|
||||||
|
apply=적용
|
||||||
|
cancel=취소
|
||||||
|
close=닫기
|
||||||
|
confirm=확인
|
||||||
|
confirmation=문의
|
||||||
|
copy=복제
|
||||||
|
cut=잘라내기
|
||||||
|
delete=삭제
|
||||||
|
error=오류
|
||||||
|
file.destroy=선택 항목을 삭제하시겠습니까?
|
||||||
|
file.mkdir=새 폴더
|
||||||
|
file.rename=@rename
|
||||||
|
file.select=파일 선택
|
||||||
|
file.show_hide=숨겨진 파일 표시
|
||||||
|
file.tips.destroy_fail=제거 실패 {0}
|
||||||
|
file.tips.not_found_target=대상이 없습니다. {0}
|
||||||
|
file.tips.select_directory=폴더를 선택하십시오.
|
||||||
|
finish=완료
|
||||||
|
home=홈 페이지
|
||||||
|
info=정보
|
||||||
|
loading=로드 중...
|
||||||
|
name=이름
|
||||||
|
no=아니오
|
||||||
|
now=지금
|
||||||
|
now_tick=지금
|
||||||
|
ok=좋아요.
|
||||||
|
paste=붙여넣기
|
||||||
|
redo=다시 실행
|
||||||
|
refresh=새로 고침
|
||||||
|
rename=이름 바꾸기
|
||||||
|
replace=대체
|
||||||
|
replace_all=모두 바꾸기
|
||||||
|
save=저장
|
||||||
|
skip=건너뛰기
|
||||||
|
undo=취소
|
||||||
|
version.fail={0} - 새 버전을 검사하는 데 실패했습니다. 다시 시도하려면 클릭하십시오.
|
||||||
|
warning=경고
|
||||||
|
wrap=줄 바꿈
|
||||||
|
yes=예
|
||||||
53
src/main/resources/lang/timi-fx-ui/ru_RU.lang
Normal file
53
src/main/resources/lang/timi-fx-ui/ru_RU.lang
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
alert.apply=@ apply
|
||||||
|
alert.cancel=@ Cancel
|
||||||
|
alert.close=@ close
|
||||||
|
alert.confirm=@ Confirm
|
||||||
|
alert.feedback=Обратная связь
|
||||||
|
alert.finish=@ finish
|
||||||
|
alert.next=Следующий шаг
|
||||||
|
alert.no=@ no
|
||||||
|
alert.ok=@ ОК
|
||||||
|
alert.previous=Предыдущий шаг
|
||||||
|
alert.save=@ save
|
||||||
|
alert.skip=@ skip
|
||||||
|
alert.title.information=@ info
|
||||||
|
alert.yes=@ Yes
|
||||||
|
apply=Применение
|
||||||
|
cancel=Отменить
|
||||||
|
close=Закрыть
|
||||||
|
confirm=Подтверждение
|
||||||
|
confirmation=Запрос
|
||||||
|
copy=Копирование
|
||||||
|
cut=Вырезание
|
||||||
|
delete=Удалить
|
||||||
|
error=Ошибка
|
||||||
|
file.destroy=Подтвердить удаление выбранных опций?
|
||||||
|
file.mkdir=Создать папку
|
||||||
|
file.rename=@ rename
|
||||||
|
file.select=Выбрать файл
|
||||||
|
file.show_hide=Показать скрытые файлы
|
||||||
|
file.tips.destroy_fail=Ошибка удаления {0}
|
||||||
|
file.tips.not_found_target=Цель не существует {0}
|
||||||
|
file.tips.select_directory=Выберите папку
|
||||||
|
finish=Завершено
|
||||||
|
home=Домашняя страница
|
||||||
|
info=Информация
|
||||||
|
loading=Загрузка...
|
||||||
|
name=Имя
|
||||||
|
no=Нет
|
||||||
|
now=А теперь...
|
||||||
|
now_tick=В данный момент
|
||||||
|
ok=Ладно.
|
||||||
|
paste=Вставить
|
||||||
|
redo=Повторить
|
||||||
|
refresh=Обновить
|
||||||
|
rename=Переименовать
|
||||||
|
replace=Замена
|
||||||
|
replace_all=Заменить все
|
||||||
|
save=Сохранить
|
||||||
|
skip=Пропустить
|
||||||
|
undo=Отмена
|
||||||
|
version.fail={0} - Ошибка проверки новой версии, нажмите для повторного тестирования
|
||||||
|
warning=предупреждение
|
||||||
|
wrap=Смена строк
|
||||||
|
yes=Да.
|
||||||
53
src/main/resources/lang/timi-fx-ui/zh_CN.lang
Normal file
53
src/main/resources/lang/timi-fx-ui/zh_CN.lang
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
alert.apply=@apply
|
||||||
|
alert.cancel=@cancel
|
||||||
|
alert.close=@close
|
||||||
|
alert.confirm=@confirm
|
||||||
|
alert.feedback=反馈
|
||||||
|
alert.finish=@finish
|
||||||
|
alert.next=下一步
|
||||||
|
alert.no=@no
|
||||||
|
alert.ok=@ok
|
||||||
|
alert.previous=上一步
|
||||||
|
alert.save=@save
|
||||||
|
alert.skip=@skip
|
||||||
|
alert.title.information=@info
|
||||||
|
alert.yes=@yes
|
||||||
|
apply=应用
|
||||||
|
cancel=取消
|
||||||
|
close=关闭
|
||||||
|
confirm=确认
|
||||||
|
confirmation=询问
|
||||||
|
copy=复制
|
||||||
|
cut=剪切
|
||||||
|
delete=删除
|
||||||
|
error=错误
|
||||||
|
file.destroy=是否确认删除已选项?
|
||||||
|
file.mkdir=新建文件夹
|
||||||
|
file.rename=@rename
|
||||||
|
file.select=选择文件
|
||||||
|
file.show_hide=显示隐藏文件
|
||||||
|
file.tips.destroy_fail=删除失败 {0}
|
||||||
|
file.tips.not_found_target=目标不存在 {0}
|
||||||
|
file.tips.select_directory=请选择文件夹
|
||||||
|
finish=完成
|
||||||
|
home=主页
|
||||||
|
info=信息
|
||||||
|
loading=加载中..
|
||||||
|
name=名称
|
||||||
|
no=否
|
||||||
|
now=现在
|
||||||
|
now_tick=此刻
|
||||||
|
ok=好
|
||||||
|
paste=粘贴
|
||||||
|
redo=重做
|
||||||
|
refresh=刷新
|
||||||
|
rename=重命名
|
||||||
|
replace=替换
|
||||||
|
replace_all=替换全部
|
||||||
|
save=保存
|
||||||
|
skip=跳过
|
||||||
|
undo=撤销
|
||||||
|
version.fail={0} - 检查新版本失败,点击重试
|
||||||
|
warning=警告
|
||||||
|
wrap=换行
|
||||||
|
yes=是
|
||||||
53
src/main/resources/lang/timi-fx-ui/zh_TW.lang
Normal file
53
src/main/resources/lang/timi-fx-ui/zh_TW.lang
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
alert.apply=@apply
|
||||||
|
alert.cancel=@cancel
|
||||||
|
alert.close=@close
|
||||||
|
alert.confirm=@confirm
|
||||||
|
alert.feedback=反饋
|
||||||
|
alert.finish=@finish
|
||||||
|
alert.next=下一步
|
||||||
|
alert.no=@no
|
||||||
|
alert.ok=@ok
|
||||||
|
alert.previous=上一步
|
||||||
|
alert.save=@save
|
||||||
|
alert.skip=@skip
|
||||||
|
alert.title.information=@info
|
||||||
|
alert.yes=@yes
|
||||||
|
apply=應用
|
||||||
|
cancel=取消
|
||||||
|
close=關閉
|
||||||
|
confirm=確認
|
||||||
|
confirmation=詢問
|
||||||
|
copy=複製
|
||||||
|
cut=剪切
|
||||||
|
delete=删除
|
||||||
|
error=錯誤
|
||||||
|
file.destroy=是否確認删除已選項?
|
||||||
|
file.mkdir=新建資料夾
|
||||||
|
file.rename=@rename
|
||||||
|
file.select=選擇檔案
|
||||||
|
file.show_hide=顯示隱藏文件
|
||||||
|
file.tips.destroy_fail=删除失敗{0}
|
||||||
|
file.tips.not_found_target=目標不存在{0}
|
||||||
|
file.tips.select_directory=請選擇資料夾
|
||||||
|
finish=完成
|
||||||
|
home=主頁
|
||||||
|
info=訊息
|
||||||
|
loading=加載中..
|
||||||
|
name=名稱
|
||||||
|
no=否
|
||||||
|
now=現在
|
||||||
|
now_tick=此刻
|
||||||
|
ok=好
|
||||||
|
paste=粘貼
|
||||||
|
redo=重做
|
||||||
|
refresh=刷新
|
||||||
|
rename=重命名
|
||||||
|
replace=替換
|
||||||
|
replace_all=替換全部
|
||||||
|
save=保存
|
||||||
|
skip=跳過
|
||||||
|
undo=撤銷
|
||||||
|
version.fail={0} -檢查新版本失敗,點擊重試
|
||||||
|
warning=警告
|
||||||
|
wrap=換行
|
||||||
|
yes=是
|
||||||
BIN
src/main/resources/timifx/MinecraftAE.ttf
Normal file
BIN
src/main/resources/timifx/MinecraftAE.ttf
Normal file
Binary file not shown.
BIN
src/main/resources/timifx/dialog-confirmation16x.png
Normal file
BIN
src/main/resources/timifx/dialog-confirmation16x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 485 B |
BIN
src/main/resources/timifx/dialog-error16x.png
Normal file
BIN
src/main/resources/timifx/dialog-error16x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 433 B |
BIN
src/main/resources/timifx/dialog-information16x.png
Normal file
BIN
src/main/resources/timifx/dialog-information16x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 479 B |
BIN
src/main/resources/timifx/dialog-warning-danger16x.png
Normal file
BIN
src/main/resources/timifx/dialog-warning-danger16x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 210 B |
BIN
src/main/resources/timifx/dialog-warning16x.png
Normal file
BIN
src/main/resources/timifx/dialog-warning16x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 335 B |
22
src/main/resources/timifx/font.css
Normal file
22
src/main/resources/timifx/font.css
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
@font-face {
|
||||||
|
src: url('./MinecraftAE.ttf');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Minecraft AE 字体在 16 32 64 128 像素时表现为最清晰 */
|
||||||
|
.text,
|
||||||
|
.label,
|
||||||
|
.button,
|
||||||
|
.chart *,
|
||||||
|
.slider *,
|
||||||
|
.tab-pane,
|
||||||
|
.combo-box,
|
||||||
|
.check-box,
|
||||||
|
.tree-view,
|
||||||
|
.text-area,
|
||||||
|
.text-field,
|
||||||
|
.list-view > *,
|
||||||
|
.table-view > *,
|
||||||
|
.minecraft-ae {
|
||||||
|
-fx-font-size: 16;
|
||||||
|
-fx-font-family: 'Minecraft AE';
|
||||||
|
}
|
||||||
BIN
src/main/resources/timifx/icon.png
Normal file
BIN
src/main/resources/timifx/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 191 B |
816
src/main/resources/timifx/style.css
Normal file
816
src/main/resources/timifx/style.css
Normal file
@ -0,0 +1,816 @@
|
|||||||
|
/******************************************************************************
|
||||||
|
* *
|
||||||
|
* Pixel Size to Font Size *
|
||||||
|
* *
|
||||||
|
* 1px: 0.083333em *
|
||||||
|
* 2px: 0.166667em *
|
||||||
|
* 3px: 0.25em *
|
||||||
|
* 4px: 0.333333em *
|
||||||
|
* 5px: 0.416667em *
|
||||||
|
* 6px: 0.5em *
|
||||||
|
* 7px: 0.583333em *
|
||||||
|
* 8px: 0.666667em *
|
||||||
|
* 9px: 0.75em *
|
||||||
|
* 10px: 0.833333em *
|
||||||
|
* 11px: 0.916667em *
|
||||||
|
* 12px: 1.0em *
|
||||||
|
* *
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
/*
|
||||||
|
- TimiFX 的样式表 -
|
||||||
|
|
||||||
|
1. 主体颜色为灰色,辅助色为靛青 #177CB0
|
||||||
|
2. 基本组件在前,复杂组件在后,公共类最后
|
||||||
|
|
||||||
|
*/
|
||||||
|
* {
|
||||||
|
-fx-tab-size: 4;
|
||||||
|
-fx-font-smoothing-type: gray;
|
||||||
|
}
|
||||||
|
.root {
|
||||||
|
-timi-fx-color: #177CB0;
|
||||||
|
-timi-fx-icon-color: #333;
|
||||||
|
-timi-fx-border-color: #B5B5B5;
|
||||||
|
-timi-fx-popup-shadow: dropshadow(three-pass-box, rgba(0, 0, 0, .3), 5, 0, 0, 1);
|
||||||
|
-timi-fx-opacity-hover: .7;
|
||||||
|
-timi-fx-selected-color: #7CC4FF;
|
||||||
|
-timi-fx-opacity-disabled: .4;
|
||||||
|
|
||||||
|
-fx-accent: -timi-fx-color;
|
||||||
|
-fx-focus-color: transparent;
|
||||||
|
-fx-focused-base: transparent;
|
||||||
|
-fx-text-box-border: -timi-fx-border-color;
|
||||||
|
-fx-faint-focus-color: transparent;
|
||||||
|
-fx-shadow-highlight-color: transparent;
|
||||||
|
-fx-cell-focus-inner-border: transparent;
|
||||||
|
-fx-control-inner-background: #FFF;
|
||||||
|
}
|
||||||
|
/* --------------------------- 聚焦边距 --------------------------- */
|
||||||
|
.text-area:focused,
|
||||||
|
.text-field:focused {
|
||||||
|
-fx-background-insets: 1, 2;
|
||||||
|
}
|
||||||
|
.button:focused {
|
||||||
|
-fx-background-insets: 0;
|
||||||
|
}
|
||||||
|
.list-view:focused {
|
||||||
|
-fx-background-insets: 0, 0, 2;
|
||||||
|
}
|
||||||
|
.table-view:focused {
|
||||||
|
-fx-background-insets: 0, 0, 1;
|
||||||
|
}
|
||||||
|
/* --------------------------- 只读 --------------------------- */
|
||||||
|
.text-area:readonly,
|
||||||
|
.text-field:readonly,
|
||||||
|
.time-picker:readonly {
|
||||||
|
-fx-text-fill: #666;
|
||||||
|
}
|
||||||
|
/* --------------------------- 控件 --------------------------- */
|
||||||
|
/* 可选标签 */
|
||||||
|
.selectable-label * {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
}
|
||||||
|
.selectable-label .scroll-pane {
|
||||||
|
-fx-hbar-policy: never;
|
||||||
|
-fx-vbar-policy: never;
|
||||||
|
}
|
||||||
|
/* 按钮 */
|
||||||
|
.button {
|
||||||
|
-fx-padding: .25em .833333em; /* 3 10 */
|
||||||
|
-fx-border-width: 1;
|
||||||
|
-fx-border-color: -timi-fx-border-color;
|
||||||
|
-fx-background-color: -fx-body-color;
|
||||||
|
-fx-background-insets: 0;
|
||||||
|
-fx-background-radius: 0;
|
||||||
|
}
|
||||||
|
.toggle-button {
|
||||||
|
-fx-padding: .25em .833333em; /* 3 10 */
|
||||||
|
-fx-border-width: 1;
|
||||||
|
-fx-border-color: -timi-fx-border-color;
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
-fx-background-insets: 0;
|
||||||
|
-fx-background-radius: .083333em;
|
||||||
|
}
|
||||||
|
.toggle-button:selected {
|
||||||
|
-fx-background-color: -timi-fx-selected-color;
|
||||||
|
}
|
||||||
|
/* 图标按钮 */
|
||||||
|
.icon-button {
|
||||||
|
-fx-border-width: 1;
|
||||||
|
-fx-border-color: transparent;
|
||||||
|
}
|
||||||
|
/* 切换图标按钮 */
|
||||||
|
.toggle-icon:disabled {
|
||||||
|
-fx-opacity: -timi-fx-opacity-disabled;
|
||||||
|
}
|
||||||
|
.toggle-icon:disabled .icon {
|
||||||
|
-fx-opacity: -timi-fx-opacity-disabled;
|
||||||
|
}
|
||||||
|
/* 文本框 */
|
||||||
|
.text-field {
|
||||||
|
-fx-padding: .25em .333333em; /* 3 4 */
|
||||||
|
-fx-border-width: 1;
|
||||||
|
-fx-border-color: -timi-fx-border-color;
|
||||||
|
-fx-background-color: -fx-control-inner-background;
|
||||||
|
-fx-background-insets: 0;
|
||||||
|
-fx-background-radius: 0;
|
||||||
|
}
|
||||||
|
/* 文本域 */
|
||||||
|
.text-area {
|
||||||
|
-fx-border-width: 1;
|
||||||
|
-fx-border-color: -timi-fx-border-color;
|
||||||
|
-fx-background-color: -fx-control-inner-background;
|
||||||
|
-fx-background-radius: 0;
|
||||||
|
}
|
||||||
|
.text-area:focused {
|
||||||
|
-fx-background-color: -fx-focus-color, -fx-control-inner-background;
|
||||||
|
}
|
||||||
|
.text-area .content {
|
||||||
|
-fx-padding: .25em .388888em; /* 3 4.5 */
|
||||||
|
-fx-background-color: -fx-control-inner-background;
|
||||||
|
}
|
||||||
|
.text-area .scroll-pane {
|
||||||
|
-fx-padding: 0;
|
||||||
|
}
|
||||||
|
/* 文本域编辑器 */
|
||||||
|
.text-area-editor .find-field {
|
||||||
|
-fx-background-color: #FFF;
|
||||||
|
-fx-background-insets: 0;
|
||||||
|
}
|
||||||
|
.text-area-editor .replace-field {
|
||||||
|
-fx-padding: .25em .333333em .25em 1.25em; /* 3 4 3 15 */
|
||||||
|
}
|
||||||
|
/* 滑动选择 */
|
||||||
|
.slider > .track {
|
||||||
|
-fx-padding: .25em;
|
||||||
|
-fx-background-radius: 0;
|
||||||
|
}
|
||||||
|
.slider > .thumb {
|
||||||
|
-fx-padding: .5em .3em;
|
||||||
|
-fx-background-color: -fx-shadow-highlight-color, -fx-outer-border, -fx-inner-border, -fx-body-color;
|
||||||
|
-fx-background-insets: 0 0 -1 0, 0, 1, 2;
|
||||||
|
-fx-background-radius: 0;
|
||||||
|
}
|
||||||
|
.slider:focused > .thumb {
|
||||||
|
-fx-background-color: -fx-shadow-highlight-color, -fx-outer-border, -fx-inner-border, -fx-body-color;
|
||||||
|
-fx-background-insets: 0 0 -1 0, 0, 1, 2;
|
||||||
|
}
|
||||||
|
/* 树形结构 */
|
||||||
|
.tree-view {
|
||||||
|
-fx-padding: 0;
|
||||||
|
}
|
||||||
|
.tree-view .tree-disclosure-node {
|
||||||
|
-fx-padding: .166667em .583333em 0 .333333em;
|
||||||
|
}
|
||||||
|
.tree-view .tree-cell .arrow,
|
||||||
|
.tree-view .tree-cell:expanded .arrow {
|
||||||
|
-fx-shape: "M303.5,391.5h1v1h1v1h1v1h1v1h1v1h-1v1h-1v1h-1v1h-1v1h-1Z";
|
||||||
|
-fx-min-width: 5;
|
||||||
|
-fx-min-height: 9;
|
||||||
|
-fx-pref-width: 5;
|
||||||
|
-fx-pref-height: 9;
|
||||||
|
}
|
||||||
|
/* 列表 */
|
||||||
|
.list-view {
|
||||||
|
-fx-padding: 0;
|
||||||
|
-fx-border-width: 1;
|
||||||
|
-fx-border-color: -timi-fx-border-color;
|
||||||
|
-fx-background-color: -fx-control-inner-background;
|
||||||
|
-fx-background-insets: 0;
|
||||||
|
}
|
||||||
|
.list-view:focused {
|
||||||
|
-fx-background-color: -fx-control-inner-background;
|
||||||
|
-fx-background-insets: 0;
|
||||||
|
}
|
||||||
|
/* 表格 */
|
||||||
|
.table-view {
|
||||||
|
-fx-padding: 0;
|
||||||
|
-fx-border-width: 1;
|
||||||
|
-fx-border-color: -timi-fx-border-color;
|
||||||
|
-fx-background-color: -fx-control-inner-background;
|
||||||
|
-fx-background-insets: 1;
|
||||||
|
}
|
||||||
|
.table-view .arrow,
|
||||||
|
.tree-table-view .arrow {
|
||||||
|
-fx-shape: "M0,0h9v1H8v1H7v1H6v1H5v1H4V4H3V3H2V2H1V1H0V0z";
|
||||||
|
-fx-min-width: 9;
|
||||||
|
-fx-min-height: 5;
|
||||||
|
-fx-pref-width: 9;
|
||||||
|
-fx-pref-height: 5;
|
||||||
|
}
|
||||||
|
.table-view .text-field {
|
||||||
|
-fx-background-insets: 0;
|
||||||
|
}
|
||||||
|
.table-view .text-field:focused {
|
||||||
|
-fx-background-insets: 1;
|
||||||
|
}
|
||||||
|
/* 下拉选择,菜单按钮,颜色选择,日期选择 */
|
||||||
|
.combo-box,
|
||||||
|
.menu-button,
|
||||||
|
.date-picker,
|
||||||
|
.color-picker {
|
||||||
|
-fx-border-width: 1;
|
||||||
|
-fx-border-color: -fx-text-box-border;
|
||||||
|
-fx-background-color: -fx-body-color;
|
||||||
|
-fx-background-insets: 0;
|
||||||
|
-fx-background-radius: 0;
|
||||||
|
}
|
||||||
|
.combo-box .text-input,
|
||||||
|
.date-picker .text-input {
|
||||||
|
-fx-padding: 4;
|
||||||
|
-fx-background-color: -fx-text-box-border, #FFF;
|
||||||
|
-fx-background-insets: 0, 0 1 0 0;
|
||||||
|
-fx-background-radius: 0;
|
||||||
|
}
|
||||||
|
.combo-box > .list-cell {
|
||||||
|
-fx-padding: .25em;
|
||||||
|
}
|
||||||
|
.menu-button.icon > .label {
|
||||||
|
-fx-padding: .166667em .083333em .25em .333333em; /* 2 1 3 4 */
|
||||||
|
}
|
||||||
|
.menu-button.empty > .label {
|
||||||
|
-fx-padding: 0;
|
||||||
|
}
|
||||||
|
/* 下拉箭头 */
|
||||||
|
.combo-box-base .text-field {
|
||||||
|
-fx-border-width: 0;
|
||||||
|
}
|
||||||
|
.combo-box .arrow-button,
|
||||||
|
.menu-button .arrow-button {
|
||||||
|
-fx-padding: 0 .5em;
|
||||||
|
-fx-background-radius: 0;
|
||||||
|
-fx-background-insets: 0;
|
||||||
|
}
|
||||||
|
.combo-box .arrow-button .arrow,
|
||||||
|
.menu-button .arrow-button .arrow,
|
||||||
|
.color-picker .arrow-button .arrow {
|
||||||
|
-fx-shape: "M301.5,393.5v1h1v1h1v1h1v1h1v1h1v-1h1v-1h1v-1h1v-1h1v-1Z";
|
||||||
|
-fx-min-width: 9;
|
||||||
|
-fx-min-height: 5;
|
||||||
|
-fx-pref-width: 9;
|
||||||
|
-fx-pref-height: 5;
|
||||||
|
}
|
||||||
|
.combo-box .combo-box-popup .list-view {
|
||||||
|
-fx-effect: -timi-fx-popup-shadow;
|
||||||
|
-fx-padding: 0;
|
||||||
|
-fx-translate-y: -1;
|
||||||
|
-fx-border-width: 1 1 0 1;
|
||||||
|
-fx-background-color: -timi-fx-border-color, #FFF;
|
||||||
|
-fx-background-insets: 0, 0 0 1 0;
|
||||||
|
}
|
||||||
|
.combo-box .combo-box-popup .list-view .list-cell {
|
||||||
|
-fx-border-color: transparent;
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
}
|
||||||
|
.combo-box .combo-box-popup .list-view .list-cell:filled:hover {
|
||||||
|
-fx-text-fill: #FFF;
|
||||||
|
-fx-background-color: -timi-fx-color;
|
||||||
|
}
|
||||||
|
.combo-box .combo-box-popup .list-view .list-cell:filled:selected,
|
||||||
|
.combo-box .combo-box-popup .list-view .list-cell:filled:selected:hover {
|
||||||
|
-fx-text-fill: #FFF;
|
||||||
|
-fx-background-color: -timi-fx-color;
|
||||||
|
}
|
||||||
|
.combo-box .combo-box-popup .list-view .scroll-bar:vertical {
|
||||||
|
-fx-border-width: 0 0 1 1;
|
||||||
|
-fx-border-color: -timi-fx-border-color;
|
||||||
|
-fx-background-color: -fx-background;
|
||||||
|
}
|
||||||
|
/* 单选,多选 */
|
||||||
|
.check-box,
|
||||||
|
.radio-button,
|
||||||
|
.check-box:hover,
|
||||||
|
.radio-button:hover {
|
||||||
|
-fx-padding: .166667em .333333em .166667em 0; /* 3 4 3 0 */
|
||||||
|
}
|
||||||
|
.check-box > .box,
|
||||||
|
.radio-button > .radio {
|
||||||
|
-fx-padding: 0;
|
||||||
|
-fx-pref-width: 18;
|
||||||
|
-fx-pref-height: 18;
|
||||||
|
-fx-background-color: -fx-focus-color, -fx-outer-border, -fx-inner-border, -fx-body-color;
|
||||||
|
-fx-background-insets: -1.4, 0, 1, 2;
|
||||||
|
-fx-background-radius: 0, 0, 0, 1;
|
||||||
|
}
|
||||||
|
.check-box .mark,
|
||||||
|
.radio-button .dot {
|
||||||
|
-fx-shape: "M3,7v2h1v1h1v1h1v1h1v-1h1v-1h1V9h1V8h1V7h1V6h1V4h-2v1h-1v1H9v1H8v1H7v1H6V8H5V7H3z";
|
||||||
|
-fx-padding: 0;
|
||||||
|
-fx-max-width: 10;
|
||||||
|
-fx-max-height: 8;
|
||||||
|
-fx-pref-width: 10;
|
||||||
|
-fx-pref-height: 8;
|
||||||
|
}
|
||||||
|
/* 弹出菜单 */
|
||||||
|
.context-menu {
|
||||||
|
-fx-color: -fx-base;
|
||||||
|
-fx-effect: -timi-fx-popup-shadow;
|
||||||
|
-fx-padding: 0;
|
||||||
|
-fx-border-width: 1;
|
||||||
|
-fx-border-color: -timi-fx-border-color;
|
||||||
|
-fx-background-color: #FFF;
|
||||||
|
-fx-background-insets: 0;
|
||||||
|
}
|
||||||
|
.context-menu .menu-item:focused {
|
||||||
|
-fx-background-color: -timi-fx-selected-color;
|
||||||
|
}
|
||||||
|
.context-menu .menu-item:focused .label {
|
||||||
|
-fx-text-fill: #333;
|
||||||
|
}
|
||||||
|
.context-menu .separator {
|
||||||
|
-fx-padding: 0;
|
||||||
|
}
|
||||||
|
/* 托盘菜单 */
|
||||||
|
.tray-menu {
|
||||||
|
-fx-border-width: 0;
|
||||||
|
}
|
||||||
|
/* 菜单 */
|
||||||
|
.menu > .right-container > .arrow {
|
||||||
|
-fx-shape: "M303.5,391.5h1v1h1v1h1v1h1v1h1v1h-1v1h-1v1h-1v1h-1v1h-1Z";
|
||||||
|
-fx-min-width: 5;
|
||||||
|
-fx-min-height: 9;
|
||||||
|
-fx-pref-width: 5;
|
||||||
|
-fx-pref-height: 9;
|
||||||
|
}
|
||||||
|
.radio-menu-item:checked > .left-container > .radio {
|
||||||
|
-fx-shape: "M3,7v2h1v1h1v1h1v1h1v-1h1v-1h1V9h1V8h1V7h1V6h1V4h-2v1h-1v1H9v1H8v1H7v1H6V8H5V7H3z";
|
||||||
|
}
|
||||||
|
.check-menu-item:checked > .left-container > .check {
|
||||||
|
-fx-shape: "M3,7v2h1v1h1v1h1v1h1v-1h1v-1h1V9h1V8h1V7h1V6h1V4h-2v1h-1v1H9v1H8v1H7v1H6V8H5V7H3z";
|
||||||
|
}
|
||||||
|
/* 日期选择 */
|
||||||
|
.date-picker:focused {
|
||||||
|
-fx-background-color: -fx-control-inner-background;
|
||||||
|
}
|
||||||
|
.date-picker > .arrow-button {
|
||||||
|
-fx-padding: 0 8;
|
||||||
|
-fx-background-insets: 0;
|
||||||
|
}
|
||||||
|
.date-picker > .arrow-button > .arrow {
|
||||||
|
-fx-shape: "M300,391v2h11v-2Z M303,394v2h2v-2Z M306,394v2h2v-2Z M309,394v2h2v-2Z M309,397v2h2v-2Z M306,397v2h2v-2Z M305,397v2h-2v-2Z M300,397h2v2h-2Z M300,400v2h2v-2Z M303,400v2h2v-2Z M306,400v2h2v-2Z";
|
||||||
|
-fx-min-width: 11;
|
||||||
|
-fx-min-height: 11;
|
||||||
|
-fx-pref-width: 11;
|
||||||
|
-fx-pref-height: 11;
|
||||||
|
-fx-background-color: -timi-fx-icon-color;
|
||||||
|
}
|
||||||
|
.date-picker-popup {
|
||||||
|
-fx-effect: -timi-fx-popup-shadow;
|
||||||
|
-fx-translate-y: -1;
|
||||||
|
-fx-background-color: -fx-box-border;
|
||||||
|
-fx-background-insets: 0;
|
||||||
|
}
|
||||||
|
.date-picker-popup > .month-year-pane {
|
||||||
|
-fx-padding: .25em;
|
||||||
|
}
|
||||||
|
.date-picker-popup > * > .spinner {
|
||||||
|
-fx-spacing: .166667em;
|
||||||
|
}
|
||||||
|
.date-picker-popup > * > .spinner > .left-button,
|
||||||
|
.date-picker-popup > * > .spinner > .right-button {
|
||||||
|
-fx-padding: 0 .333333em .333333em .333333em;
|
||||||
|
-fx-border-width: 0;
|
||||||
|
}
|
||||||
|
.date-picker-popup > * > .spinner > .button > .left-arrow,
|
||||||
|
.date-picker-popup > * > .spinner > .button > .right-arrow {
|
||||||
|
-fx-min-width: 8;
|
||||||
|
-fx-min-height: 13;
|
||||||
|
-fx-pref-width: 8;
|
||||||
|
-fx-pref-height: 13;
|
||||||
|
-fx-background-color: -timi-fx-icon-color;
|
||||||
|
}
|
||||||
|
.date-picker-popup > * > .spinner > .button > .left-arrow {
|
||||||
|
-fx-shape: "M298,394h1v-1h1v-1h1v-1h1v-1h1v-1h1v-1h2v1h-1v1h-1v1h-1v1h-1v1h-1v1h-1v1h1v1h1v1h1v1h1v1h1v1h1v1h-2v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1Z";
|
||||||
|
}
|
||||||
|
.date-picker-popup > * > .spinner > .button > .right-arrow {
|
||||||
|
-fx-shape: "M306,394h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-2v1h1v1h1v1h1v1h1v1h1v1h1v1h-1v1h-1v1h-1v1h-1v1h-1v1h-1v1h2v-1h1v-1h1v-1h1v-1h1v-1h1v-1h1Z";
|
||||||
|
}
|
||||||
|
/* 颜色选择器 */
|
||||||
|
.color-palette {
|
||||||
|
-fx-translate-y: -1;
|
||||||
|
-fx-background-radius: 0;
|
||||||
|
}
|
||||||
|
.custom-color-dialog {
|
||||||
|
-fx-border-width: 1 0 0 0;
|
||||||
|
-fx-border-color: -timi-fx-border-color;
|
||||||
|
}
|
||||||
|
.custom-color-dialog > .color-rect-pane > .color-bar > #color-bar-indicator {
|
||||||
|
-fx-border-radius: 0;
|
||||||
|
}
|
||||||
|
.custom-color-dialog .controls-pane .toggle-button {
|
||||||
|
-fx-padding: .25em .5em;
|
||||||
|
-fx-background-radius: 0;
|
||||||
|
}
|
||||||
|
.custom-color-dialog .controls-pane .current-new-color-grid #current-new-color-border {
|
||||||
|
-fx-border-color: derive(-fx-base, -20%);
|
||||||
|
-fx-border-width: 1px;
|
||||||
|
}
|
||||||
|
.custom-color-dialog .controls-pane .customcolor-controls-background {
|
||||||
|
-fx-background-color: -fx-text-box-border, -fx-control-inner-background;
|
||||||
|
-fx-background-insets: 12px 0 0 0, 13px 1px 1px 1px;
|
||||||
|
-fx-background-radius: 0;
|
||||||
|
}
|
||||||
|
/* 滚动条 */
|
||||||
|
.scroll-bar {
|
||||||
|
-fx-padding: 0;
|
||||||
|
}
|
||||||
|
.scroll-bar .decrement-arrow,
|
||||||
|
.scroll-bar .increment-arrow,
|
||||||
|
.scroll-bar .decrement-button,
|
||||||
|
.scroll-bar .increment-button {
|
||||||
|
visibility: hidden;
|
||||||
|
-fx-min-width: 0;
|
||||||
|
-fx-min-height: 0;
|
||||||
|
-fx-pref-width: 0;
|
||||||
|
-fx-pref-height: 0;
|
||||||
|
}
|
||||||
|
.scroll-bar:vertical,
|
||||||
|
.scroll-bar:horizontal {
|
||||||
|
-fx-padding: 0;
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
}
|
||||||
|
.scroll-bar:vertical {
|
||||||
|
-fx-pref-width: 7;
|
||||||
|
}
|
||||||
|
.scroll-bar:horizontal {
|
||||||
|
-fx-pref-height: 7;
|
||||||
|
}
|
||||||
|
.scroll-bar:vertical .thumb,
|
||||||
|
.scroll-bar:horizontal .thumb {
|
||||||
|
-fx-background-color: -timi-fx-color;
|
||||||
|
-fx-background-insets: 0;
|
||||||
|
-fx-background-radius: 0;
|
||||||
|
}
|
||||||
|
/* 列表滚动条 */
|
||||||
|
.tree-view .scroll-bar:vertical,
|
||||||
|
.list-view .scroll-bar:vertical,
|
||||||
|
.table-view .scroll-bar:vertical {
|
||||||
|
-fx-border-width: 0 0 0 1;
|
||||||
|
-fx-border-color: -timi-fx-border-color;
|
||||||
|
-fx-background-color: -fx-background;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-view .scroll-bar:horizontal,
|
||||||
|
.list-view .scroll-bar:horizontal,
|
||||||
|
.table-view .scroll-bar:horizontal {
|
||||||
|
-fx-border-width: 1 0 0 0;
|
||||||
|
-fx-border-color: -timi-fx-border-color;
|
||||||
|
-fx-background-color: -fx-background;
|
||||||
|
}
|
||||||
|
/* 进度条 */
|
||||||
|
.progress-bar {
|
||||||
|
-fx-padding: 1;
|
||||||
|
-fx-border-width: 1;
|
||||||
|
-fx-border-color: -timi-fx-border-color;
|
||||||
|
-fx-indeterminate-bar-flip: true;
|
||||||
|
-fx-indeterminate-bar-length: 120;
|
||||||
|
-fx-indeterminate-bar-escape: true;
|
||||||
|
-fx-indeterminate-bar-animation-time: 4;
|
||||||
|
}
|
||||||
|
.progress-bar .track {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
-fx-background-radius: 0;
|
||||||
|
}
|
||||||
|
.progress-bar .bar {
|
||||||
|
-fx-background-color: -timi-fx-color;
|
||||||
|
-fx-background-insets: 0;
|
||||||
|
-fx-background-radius: 0;
|
||||||
|
}
|
||||||
|
.progress-bar:indeterminate .bar {
|
||||||
|
-fx-background-color: linear-gradient(to left, transparent, -timi-fx-color);
|
||||||
|
}
|
||||||
|
/* 标签进度 */
|
||||||
|
.label-progress-bar .bar {
|
||||||
|
-fx-background-color: #7ACAF4;
|
||||||
|
}
|
||||||
|
.label-progress-bar:indeterminate .bar {
|
||||||
|
-fx-background-color: linear-gradient(to left, transparent, #7ACAF4);
|
||||||
|
}
|
||||||
|
/* 滑动选择 - 进度 */
|
||||||
|
.progress-slider .track {
|
||||||
|
-fx-padding: 0;
|
||||||
|
}
|
||||||
|
.progress-slider .progress-bar .bar {
|
||||||
|
-fx-pref-height: 6;
|
||||||
|
-fx-background-insets: 0;
|
||||||
|
}
|
||||||
|
/* 分页 */
|
||||||
|
.x-pagination .button,
|
||||||
|
.x-pagination .toggle-button {
|
||||||
|
-fx-padding: 0;
|
||||||
|
-fx-pref-width: 28;
|
||||||
|
-fx-pref-height: 26;
|
||||||
|
}
|
||||||
|
/* 标题组件 */
|
||||||
|
.titled-pane {
|
||||||
|
-fx-animate: false;
|
||||||
|
-fx-border-width: 1;
|
||||||
|
-fx-border-color: -timi-fx-border-color;
|
||||||
|
-fx-background-color: -timi-fx-border-color, #F1F1F1;
|
||||||
|
-fx-background-insets: 0, 0 0 1 0;
|
||||||
|
}
|
||||||
|
.titled-pane > .title {
|
||||||
|
-fx-padding: .333333em .833333em; /* 4 10 */
|
||||||
|
-fx-background-color: -fx-inner-border, -fx-body-color;
|
||||||
|
-fx-background-insets: 0, 1;
|
||||||
|
-fx-background-radius: 0, 0;
|
||||||
|
}
|
||||||
|
.titled-pane > .title > .arrow-button {
|
||||||
|
-fx-padding: 0;
|
||||||
|
-fx-translate-x: -4;
|
||||||
|
-fx-translate-y: -1;
|
||||||
|
}
|
||||||
|
.titled-pane > .title > .arrow-button > .arrow {
|
||||||
|
-fx-shape: "M300,393v1h1v1h1v1h1v1h1v1h1v-1h1v-1h1v-1h1v-1h1v-1Z";
|
||||||
|
-fx-padding: 0;
|
||||||
|
-fx-min-width: 9;
|
||||||
|
-fx-min-height: 5;
|
||||||
|
-fx-pref-width: 9;
|
||||||
|
-fx-pref-height: 5;
|
||||||
|
}
|
||||||
|
.titled-pane > .content {
|
||||||
|
-fx-padding: 0;
|
||||||
|
-fx-border-width: 2 0 0 0;
|
||||||
|
-fx-border-color: -timi-fx-border-color;
|
||||||
|
-fx-background-color: #FFF;
|
||||||
|
-fx-background-insets: 0;
|
||||||
|
}
|
||||||
|
.titled-group .titled-pane {
|
||||||
|
-fx-border-width: 0 0 1 0;
|
||||||
|
-fx-background-insets: 0, 0;
|
||||||
|
}
|
||||||
|
/* 图表 */
|
||||||
|
.chart {
|
||||||
|
-fx-padding: 0 0 6 0;
|
||||||
|
}
|
||||||
|
.chart-content {
|
||||||
|
-fx-padding: 0 24 4 6;
|
||||||
|
}
|
||||||
|
.chart-legend {
|
||||||
|
-fx-padding: 4 8;
|
||||||
|
-fx-background-radius: 0, 0;
|
||||||
|
}
|
||||||
|
.area-legend-symbol {
|
||||||
|
-fx-padding: 6;
|
||||||
|
-fx-background-radius: 0;
|
||||||
|
-fx-background-insets: 0, 6;
|
||||||
|
}
|
||||||
|
/* 导航 */
|
||||||
|
.navigation {
|
||||||
|
-fx-border-width: 1;
|
||||||
|
-fx-border-color: -timi-fx-border-color;
|
||||||
|
}
|
||||||
|
.navigation .group-pane {
|
||||||
|
-fx-animated: false;
|
||||||
|
-fx-border-width: 1 0 0 0;
|
||||||
|
}
|
||||||
|
.navigation .group-pane.bottom-line {
|
||||||
|
-fx-border-width: 1 0 1 0;
|
||||||
|
}
|
||||||
|
.navigation .group-pane .content {
|
||||||
|
-fx-padding: 0;
|
||||||
|
}
|
||||||
|
.navigation .navigation-button {
|
||||||
|
-fx-padding: .25em .833333em; /* 3 10 */
|
||||||
|
-fx-border-width: 0;
|
||||||
|
-fx-border-color: -timi-fx-border-color;
|
||||||
|
}
|
||||||
|
.navigation .navigation-button.after-group { /* 紧挨着上一个导航组时,添加上边框 */
|
||||||
|
-fx-border-width: 1 0 0 0;
|
||||||
|
}
|
||||||
|
.navigation .navigation-button:hover {
|
||||||
|
-fx-background-color: #CCC;
|
||||||
|
}
|
||||||
|
.navigation .navigation-button:selected {
|
||||||
|
-fx-text-fill: #FFF;
|
||||||
|
-fx-background-color: -timi-fx-color;
|
||||||
|
}
|
||||||
|
.navigation .navigation-button:disabled {
|
||||||
|
-fx-opacity: -timi-fx-opacity-disabled;
|
||||||
|
}
|
||||||
|
/* 可编辑表格 */
|
||||||
|
.editable-table .table-row-cell:odd {
|
||||||
|
-fx-background-color: -fx-table-cell-border-color, -fx-control-inner-background;
|
||||||
|
}
|
||||||
|
.editable-table .table-row-cell:filled:selected,
|
||||||
|
.editable-table .table-row-cell:filled:selected:focused {
|
||||||
|
-fx-padding: 0;
|
||||||
|
-fx-text-fill: -fx-selection-bar-text;
|
||||||
|
-fx-background: -timi-fx-selected-color;
|
||||||
|
-fx-border-width: 0;
|
||||||
|
-fx-background-color: -timi-fx-selected-color;
|
||||||
|
-fx-background-insets: 0;
|
||||||
|
-fx-table-cell-border-color: -timi-fx-selected-color;
|
||||||
|
}
|
||||||
|
.editable-table .table-cell {
|
||||||
|
-fx-padding: 0;
|
||||||
|
-fx-cell-size: 0;
|
||||||
|
-fx-border-width: .083333em .083333em .083333em 0; /* 1 1 1 0 */
|
||||||
|
}
|
||||||
|
.editable-table .table-row-cell .text-field {
|
||||||
|
-fx-background-color: -timi-fx-color, #FFF;
|
||||||
|
-fx-background-insets: 0, 0;
|
||||||
|
}
|
||||||
|
.editable-table .table-row-cell .text-field:focused {
|
||||||
|
-fx-background-insets: 0, 1;
|
||||||
|
}
|
||||||
|
/* 选项卡 */
|
||||||
|
.tab-pane {
|
||||||
|
-fx-border-color: -timi-fx-border-color;
|
||||||
|
}
|
||||||
|
.tab-pane > .tab-header-area > .headers-region > .tab {
|
||||||
|
-fx-padding: 0 0 0 .5em;
|
||||||
|
-fx-background-insets: 0 0 1 0, 0 1 1 0, 1 2 1 1;
|
||||||
|
-fx-background-radius: 0, 0, 0;
|
||||||
|
}
|
||||||
|
.tab-pane > .tab-header-area > .headers-region > .tab:selected {
|
||||||
|
-fx-background-insets: 0 1 0 0, 0 1 0 0;
|
||||||
|
}
|
||||||
|
.tab-pane > .tab-header-area > .headers-region > .tab > .tab-container > .tab-label {
|
||||||
|
-fx-padding: 0 .5em 0 0;
|
||||||
|
}
|
||||||
|
.tab-pane:top > .tab-header-area {
|
||||||
|
-fx-padding: 0;
|
||||||
|
}
|
||||||
|
/* 分割线 */
|
||||||
|
.separator > .line {
|
||||||
|
-fx-border-style: null;
|
||||||
|
-fx-border-width: 0;
|
||||||
|
}
|
||||||
|
.separator:horizontal > .line {
|
||||||
|
-fx-padding: 0;
|
||||||
|
-fx-min-height: 1;
|
||||||
|
-fx-max-height: 1;
|
||||||
|
-fx-pref-height: 1;
|
||||||
|
-fx-background-color: -timi-fx-border-color;
|
||||||
|
}
|
||||||
|
/* --------------------------- 布局 --------------------------- */
|
||||||
|
/* 滚动 */
|
||||||
|
.scroll-pane {
|
||||||
|
-fx-padding: 0;
|
||||||
|
-fx-background-insets: 0;
|
||||||
|
}
|
||||||
|
.scroll-pane .corner {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
}
|
||||||
|
/* 分割 */
|
||||||
|
.split-pane {
|
||||||
|
-fx-padding: 0;
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
-fx-background-insets: 0;
|
||||||
|
}
|
||||||
|
/* --------------------------- 可选类 --------------------------- */
|
||||||
|
/* 其他 */
|
||||||
|
.bg-tp,
|
||||||
|
.bg-tp .viewport {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
-fx-background-insets: 0;
|
||||||
|
}
|
||||||
|
.bg-white {
|
||||||
|
-fx-background-color: #FFF;
|
||||||
|
}
|
||||||
|
.bg-black {
|
||||||
|
-fx-background-color: #000;
|
||||||
|
}
|
||||||
|
.bg-default {
|
||||||
|
-fx-background-color: #F4F4F4;
|
||||||
|
}
|
||||||
|
.bg-button-static {
|
||||||
|
-fx-border-color: -timi-fx-border-color;
|
||||||
|
-fx-background-color: -fx-body-color;
|
||||||
|
}
|
||||||
|
.bg-button-static:disabled {
|
||||||
|
-fx-opacity: -timi-fx-opacity-disabled;
|
||||||
|
}
|
||||||
|
.bg-button {
|
||||||
|
-fx-border-color: -timi-fx-border-color;
|
||||||
|
-fx-background-color: -fx-body-color;
|
||||||
|
}
|
||||||
|
.bg-button:hover {
|
||||||
|
-fx-color: -fx-hover-base;
|
||||||
|
}
|
||||||
|
.bg-button:armed {
|
||||||
|
-fx-color: -fx-pressed-base;
|
||||||
|
}
|
||||||
|
.bg-button:disabled {
|
||||||
|
-fx-opacity: -timi-fx-opacity-disabled;
|
||||||
|
}
|
||||||
|
.hover-opacity:hover {
|
||||||
|
-fx-opacity: -timi-fx-opacity-hover;
|
||||||
|
}
|
||||||
|
/* 滚动面板的滚动条边框 */
|
||||||
|
.sp-border .scroll-bar:vertical {
|
||||||
|
-fx-border-width: 0 0 0 1;
|
||||||
|
-fx-border-color: -timi-fx-border-color;
|
||||||
|
}
|
||||||
|
.sp-border .scroll-bar:horizontal {
|
||||||
|
-fx-border-width: 1 0 0 0;
|
||||||
|
-fx-border-color: -timi-fx-border-color;
|
||||||
|
}
|
||||||
|
.sp-border .corner {
|
||||||
|
-fx-border-width: 1 0 0 1;
|
||||||
|
-fx-border-color: -timi-fx-border-color;
|
||||||
|
}
|
||||||
|
/* 内边距 */
|
||||||
|
.padding-n {
|
||||||
|
-fx-padding: 0;
|
||||||
|
}
|
||||||
|
/* 字体颜色 */
|
||||||
|
.white {
|
||||||
|
-fx-text-fill: #FFF;
|
||||||
|
}
|
||||||
|
.red {
|
||||||
|
-fx-text-fill: #F30;
|
||||||
|
}
|
||||||
|
.brown {
|
||||||
|
-fx-text-fill: #A67D7B;
|
||||||
|
}
|
||||||
|
.black {
|
||||||
|
-fx-text-fill: #000;
|
||||||
|
}
|
||||||
|
.orange {
|
||||||
|
-fx-text-fill: #F60;
|
||||||
|
}
|
||||||
|
.yellow {
|
||||||
|
-fx-text-fill: #FF0;
|
||||||
|
}
|
||||||
|
.green {
|
||||||
|
-fx-text-fill: #393;
|
||||||
|
}
|
||||||
|
.dark-green {
|
||||||
|
-fx-text-fill: #373;
|
||||||
|
}
|
||||||
|
.gray {
|
||||||
|
-fx-text-fill: #666;
|
||||||
|
}
|
||||||
|
.blue {
|
||||||
|
-fx-text-fill: #008DCB;
|
||||||
|
}
|
||||||
|
.light-blue {
|
||||||
|
-fx-text-fill: #DDEAF0;
|
||||||
|
}
|
||||||
|
.gray-white {
|
||||||
|
-fx-text-fill: #F4F4F4;
|
||||||
|
}
|
||||||
|
.light-gray {
|
||||||
|
-fx-text-fill: #B5B5B5;
|
||||||
|
}
|
||||||
|
.dark-gray {
|
||||||
|
-fx-text-fill: #333;
|
||||||
|
}
|
||||||
|
.pink {
|
||||||
|
-fx-text-fill: #FF7A9B;
|
||||||
|
}
|
||||||
|
.transparent {
|
||||||
|
-fx-text-fill: #FFFFFF00;
|
||||||
|
}
|
||||||
|
/* 边框控制 */
|
||||||
|
.border-all {
|
||||||
|
-fx-border-width: 1;
|
||||||
|
}
|
||||||
|
.border-n {
|
||||||
|
-fx-border-width: 0;
|
||||||
|
}
|
||||||
|
.border-t {
|
||||||
|
-fx-border-width: 1 0 0 0;
|
||||||
|
}
|
||||||
|
.border-r {
|
||||||
|
-fx-border-width: 0 1 0 0;
|
||||||
|
}
|
||||||
|
.border-b {
|
||||||
|
-fx-border-width: 0 0 1 0;
|
||||||
|
}
|
||||||
|
.border-l {
|
||||||
|
-fx-border-width: 0 0 0 1;
|
||||||
|
}
|
||||||
|
.border-tr {
|
||||||
|
-fx-border-width: 1 1 0 0;
|
||||||
|
}
|
||||||
|
.border-rb {
|
||||||
|
-fx-border-width: 0 1 1 0;
|
||||||
|
}
|
||||||
|
.border-bl {
|
||||||
|
-fx-border-width: 0 0 1 1;
|
||||||
|
}
|
||||||
|
.border-lt {
|
||||||
|
-fx-border-width: 1 0 0 1;
|
||||||
|
}
|
||||||
|
.border-trb {
|
||||||
|
-fx-border-width: 1 1 1 0;
|
||||||
|
}
|
||||||
|
.border-rbl {
|
||||||
|
-fx-border-width: 0 1 1 1;
|
||||||
|
}
|
||||||
|
.border-blt {
|
||||||
|
-fx-border-width: 1 0 1 1;
|
||||||
|
}
|
||||||
|
.border-ltr {
|
||||||
|
-fx-border-width: 1 1 0 1;
|
||||||
|
}
|
||||||
|
.border-tb {
|
||||||
|
-fx-border-width: 1 0 1 0;
|
||||||
|
}
|
||||||
|
.border-lr {
|
||||||
|
-fx-border-width: 0 1 0 1;
|
||||||
|
}
|
||||||
66
src/test/java/com/imyeyu/fx/ui/examples/TimiFXExamples.java
Normal file
66
src/test/java/com/imyeyu/fx/ui/examples/TimiFXExamples.java
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package com.imyeyu.fx.ui.examples;
|
||||||
|
|
||||||
|
import com.imyeyu.inject.InjectApp;
|
||||||
|
import javafx.application.Application;
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import com.imyeyu.config.ConfigLoader;
|
||||||
|
import com.imyeyu.fx.config.BindingsConfig;
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import com.imyeyu.fx.ui.examples.bean.Config;
|
||||||
|
import com.imyeyu.fx.ui.examples.ctrl.Main;
|
||||||
|
import com.imyeyu.inject.annotation.TimiInjectApplication;
|
||||||
|
import com.imyeyu.java.bean.Language;
|
||||||
|
import com.imyeyu.lang.multi.ResourcesMultilingual;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TimiFX 示例程序
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-05-03 14:52
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@TimiInjectApplication
|
||||||
|
public class TimiFXExamples {
|
||||||
|
|
||||||
|
/** 版本号 */
|
||||||
|
public static final String VERSION = "1.1.0";
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private static Config config;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private static ConfigLoader<Config> configLoader;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private static InjectApp injectApp;
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
try {
|
||||||
|
injectApp = new InjectApp(TimiFXExamples.class);
|
||||||
|
{
|
||||||
|
configLoader = new ConfigLoader<>("TimiFXExamples.yaml", Config.class);
|
||||||
|
for (Map.Entry<Class<?>, BindingsConfig.PropertyConverter<?, ?>> item : BindingsConfig.DEFAULT_CONVERTER_MAP.entrySet()) {
|
||||||
|
configLoader.addConverter(item.getKey(), item.getValue());
|
||||||
|
}
|
||||||
|
configLoader.addConverter(ObjectProperty.class, BindingsConfig.OBJECT);
|
||||||
|
config = configLoader.load();
|
||||||
|
|
||||||
|
ResourcesMultilingual multilingual = TimiFXUI.MULTILINGUAL;
|
||||||
|
multilingual.addAll("lang/timi-fx-ui/%s.lang");
|
||||||
|
multilingual.addAll("lang/%s.lang");
|
||||||
|
multilingual.setActivated(Language.zh_CN);
|
||||||
|
|
||||||
|
// 禁止系统 DPI 缩放
|
||||||
|
System.setProperty("prism.allowhidpi", "false");
|
||||||
|
System.setProperty("glass.win.minHiDPI", "1");
|
||||||
|
}
|
||||||
|
Application.launch(Main.class);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("fatal error", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
src/test/java/com/imyeyu/fx/ui/examples/bean/Config.java
Normal file
34
src/test/java/com/imyeyu/fx/ui/examples/bean/Config.java
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package com.imyeyu.fx.ui.examples.bean;
|
||||||
|
|
||||||
|
import javafx.beans.property.DoubleProperty;
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
import com.imyeyu.java.bean.Language;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author 夜雨
|
||||||
|
* @version 2024-05-07 20:34
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class Config {
|
||||||
|
|
||||||
|
private ObjectProperty<Language> language;
|
||||||
|
|
||||||
|
private DoubleProperty width;
|
||||||
|
|
||||||
|
private DoubleProperty height;
|
||||||
|
|
||||||
|
private Interpolator interpolator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @version 2024-05-07 20:35
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public static class Interpolator {
|
||||||
|
|
||||||
|
private DoubleProperty duration;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
package com.imyeyu.fx.ui.examples.component;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import com.imyeyu.fx.ui.components.XHyperlink;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.layout.GridPane;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 抽象示例面板
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-08-29 15:44
|
||||||
|
*/
|
||||||
|
public abstract class AbstractDemoPane extends AbstractPane {
|
||||||
|
|
||||||
|
/** 标题 */
|
||||||
|
protected Label title;
|
||||||
|
|
||||||
|
/** 提示 */
|
||||||
|
protected Label tips;
|
||||||
|
|
||||||
|
/** 文档 */
|
||||||
|
protected XHyperlink document;
|
||||||
|
|
||||||
|
/** 源码 */
|
||||||
|
protected XHyperlink source;
|
||||||
|
|
||||||
|
public AbstractDemoPane() {
|
||||||
|
// 标题
|
||||||
|
title = new Label();
|
||||||
|
title.setBorder(Stroke.BOTTOM);
|
||||||
|
title.setPadding(new Insets(4, 6, 4, 6));
|
||||||
|
title.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
title.setBackground(BG.WHITE);
|
||||||
|
|
||||||
|
// 文档
|
||||||
|
Label labelDocument = TimiFXUI.label(TimiFXUI.MULTILINGUAL.text("document"));
|
||||||
|
document = new XHyperlink();
|
||||||
|
|
||||||
|
// 源码
|
||||||
|
Label labelSource = TimiFXUI.label(TimiFXUI.MULTILINGUAL.text("source"));
|
||||||
|
source = new XHyperlink();
|
||||||
|
|
||||||
|
// 提示
|
||||||
|
tips = TimiFXUI.label();
|
||||||
|
tips.setPadding(new Insets(4, 6, 4, 6));
|
||||||
|
tips.setWrapText(true);
|
||||||
|
tips.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
tips.visibleProperty().bind(tips.textProperty().isNotEmpty());
|
||||||
|
tips.managedProperty().bind(tips.visibleProperty());
|
||||||
|
tips.textProperty().addListener((obs, o, newTips) -> {
|
||||||
|
if (!newTips.startsWith("\t")) {
|
||||||
|
tips.setText("\t" + newTips);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setTop(new VBox() {{
|
||||||
|
getChildren().addAll(title, new GridPane() {{
|
||||||
|
setHgap(6);
|
||||||
|
setVgap(3);
|
||||||
|
setBorder(Stroke.BOTTOM);
|
||||||
|
setPadding(new Insets(3, 6, 3, 6));
|
||||||
|
|
||||||
|
addRow(0, labelDocument, document);
|
||||||
|
addRow(1, labelSource, source);
|
||||||
|
}}, tips);
|
||||||
|
}});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
package com.imyeyu.fx.ui.examples.component;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import com.imyeyu.fx.ui.examples.component.sidebar.SidebarItem;
|
||||||
|
import com.imyeyu.fx.ui.examples.service.PageService;
|
||||||
|
import com.imyeyu.inject.annotation.Inject;
|
||||||
|
import com.imyeyu.inject.annotation.StaticInject;
|
||||||
|
import javafx.scene.layout.BorderPane;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 抽象面板
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-08-26 14:55
|
||||||
|
*/
|
||||||
|
@StaticInject
|
||||||
|
public abstract class AbstractPane extends BorderPane implements TimiFXUI {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private static PageService pageService;
|
||||||
|
|
||||||
|
/** 显示时触发,UI 线程 */
|
||||||
|
protected void onShow() {
|
||||||
|
// 子类实现
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 隐藏时触发,UI 线程 */
|
||||||
|
protected void onHide() {
|
||||||
|
// 子类实现
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跳转页面
|
||||||
|
*
|
||||||
|
* @param page 页面
|
||||||
|
*/
|
||||||
|
protected final void toPage(SidebarItem page) {
|
||||||
|
pageService.to(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 显示面板事件,由调用者触发,通常是侧边导航 */
|
||||||
|
public final void show() {
|
||||||
|
onShow();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 隐藏面板事件,由侧边导航触发 */
|
||||||
|
public final void hide() {
|
||||||
|
onHide();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
package com.imyeyu.fx.ui.examples.component;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import com.imyeyu.fx.ui.examples.component.sidebar.Sidebar;
|
||||||
|
import com.imyeyu.inject.annotation.Component;
|
||||||
|
import com.imyeyu.inject.annotation.Inject;
|
||||||
|
import com.imyeyu.inject.annotation.InvokeForInjected;
|
||||||
|
import javafx.scene.layout.BorderPane;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根布局
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-08-26 14:57
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class RootLayout extends BorderPane implements TimiFXUI {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private Sidebar sidebar;
|
||||||
|
|
||||||
|
public RootLayout() {
|
||||||
|
setBorder(Stroke.TOP);
|
||||||
|
}
|
||||||
|
|
||||||
|
@InvokeForInjected
|
||||||
|
public void init() {
|
||||||
|
setLeft(sidebar);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,68 @@
|
|||||||
|
package com.imyeyu.fx.ui.examples.component;
|
||||||
|
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import com.imyeyu.fx.task.RunAsync;
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import com.imyeyu.fx.ui.components.VersionLabel;
|
||||||
|
import com.imyeyu.java.TimiJava;
|
||||||
|
import com.imyeyu.java.bean.timi.TimiCode;
|
||||||
|
import com.imyeyu.java.bean.timi.TimiException;
|
||||||
|
import org.apache.hc.client5.http.fluent.Request;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timi 通用版本标签
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-05-31 23:24
|
||||||
|
*/
|
||||||
|
public class TimiVersionLabel extends VersionLabel<JsonObject> {
|
||||||
|
|
||||||
|
private final String nowVersion;
|
||||||
|
private final String appName;
|
||||||
|
|
||||||
|
public TimiVersionLabel(String nowVersion, String appName) {
|
||||||
|
super(TimiFXUI.MULTILINGUAL.textArgs("version.checking", nowVersion));
|
||||||
|
|
||||||
|
this.nowVersion = nowVersion;
|
||||||
|
this.appName = appName;
|
||||||
|
|
||||||
|
RunAsync.later(() -> checkVersion(nowVersion), 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected JsonObject run() {
|
||||||
|
try {
|
||||||
|
Thread.sleep(2000);
|
||||||
|
String api = "https://api.imyeyu.com/versions/" + appName;
|
||||||
|
return JsonParser.parseString(Request.get(api).execute().returnContent().asString()).getAsJsonObject();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new TimiException(TimiCode.ERROR, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String onReturn(JsonObject resp) {
|
||||||
|
int code = resp.get("code").getAsInt();
|
||||||
|
if (code == 20000) {
|
||||||
|
JsonObject data = resp.get("data").getAsJsonObject();
|
||||||
|
setUpdateURL(data.get("url").getAsString());
|
||||||
|
if (data.has("content") && TimiJava.isNotEmpty(data.get("content").getAsString())) {
|
||||||
|
content.setText(data.get("content").getAsString());
|
||||||
|
}
|
||||||
|
return data.get("version").getAsString();
|
||||||
|
} else {
|
||||||
|
throw new TimiException(TimiCode.fromCode(code), resp.get("msg").getAsString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String updateText(String newVersion) {
|
||||||
|
return TimiFXUI.MULTILINGUAL.textArgs("version.new", nowVersion, newVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String failText(Throwable e) {
|
||||||
|
return TimiFXUI.MULTILINGUAL.textArgs("version.fail", nowVersion);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,112 @@
|
|||||||
|
package com.imyeyu.fx.ui.examples.component.animation;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.bean.Interpolates;
|
||||||
|
import com.imyeyu.fx.ui.MinecraftFont;
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import com.imyeyu.fx.ui.components.SelectableLabel;
|
||||||
|
import com.imyeyu.fx.utils.BgFill;
|
||||||
|
import com.sun.scenario.animation.SplineInterpolator;
|
||||||
|
import javafx.animation.TranslateTransition;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.scene.Cursor;
|
||||||
|
import javafx.scene.canvas.Canvas;
|
||||||
|
import javafx.scene.canvas.GraphicsContext;
|
||||||
|
import javafx.scene.layout.BorderPane;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.scene.layout.Region;
|
||||||
|
import javafx.util.Duration;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 动画插值器示例组件
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-09-01 14:44
|
||||||
|
*/
|
||||||
|
public class InterpolatorPane extends BorderPane implements TimiFXUI {
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private final TranslateTransition transition;
|
||||||
|
|
||||||
|
private final Region block;
|
||||||
|
|
||||||
|
public InterpolatorPane(Interpolates interpolator) {
|
||||||
|
SplineInterpolator si = interpolator.getValue();
|
||||||
|
|
||||||
|
// 二次曲线
|
||||||
|
Canvas canvas = new Canvas();
|
||||||
|
canvas.setWidth(64);
|
||||||
|
canvas.setHeight(64);
|
||||||
|
|
||||||
|
// 插值
|
||||||
|
SelectableLabel label = new SelectableLabel(TimiFXUI.MULTILINGUAL.textArgs("fx.example.interpolator.demo", interpolator, si.getX1(), si.getY1(), si.getX2(), si.getY2()));
|
||||||
|
|
||||||
|
// 方块
|
||||||
|
block = new Region();
|
||||||
|
block.setBackground(BgFill.test());
|
||||||
|
|
||||||
|
// 轨道
|
||||||
|
HBox track = new HBox(block);
|
||||||
|
track.setBorder(Stroke.TOP);
|
||||||
|
track.setCursor(Cursor.HAND);
|
||||||
|
track.setBackground(BG.WHITE);
|
||||||
|
|
||||||
|
block.maxWidthProperty().bind(block.heightProperty());
|
||||||
|
block.prefWidthProperty().bind(block.heightProperty());
|
||||||
|
|
||||||
|
setMargin(label, new Insets(2, 4, 2, 4));
|
||||||
|
setBorder(Stroke.DEFAULT);
|
||||||
|
setPrefWidth(520);
|
||||||
|
setBackground(BG.TITLE);
|
||||||
|
setLeft(canvas);
|
||||||
|
setCenter(new BorderPane() {{
|
||||||
|
setBorder(Stroke.LEFT);
|
||||||
|
setTop(label);
|
||||||
|
setCenter(track);
|
||||||
|
}});
|
||||||
|
|
||||||
|
transition = new TranslateTransition();
|
||||||
|
transition.setNode(block);
|
||||||
|
transition.setFromX(0);
|
||||||
|
transition.toXProperty().bind(track.widthProperty().subtract(block.widthProperty()));
|
||||||
|
transition.setDuration(Duration.seconds(2));
|
||||||
|
transition.setInterpolator(interpolator.getValue());
|
||||||
|
|
||||||
|
// ---------- 事件 ----------
|
||||||
|
|
||||||
|
// 绘制图示
|
||||||
|
{
|
||||||
|
double[][] list = interpolator.buildBezierPoint(canvas.getWidth(), canvas.getWidth() * .5);
|
||||||
|
GraphicsContext g = canvas.getGraphicsContext2D();
|
||||||
|
g.setFill(Colorful.WHITE);
|
||||||
|
g.fillRect(0, 0, canvas.getWidth(), canvas.getHeight());
|
||||||
|
g.setLineWidth(2);
|
||||||
|
|
||||||
|
// 文本
|
||||||
|
g.setStroke(Colorful.LIGHT_GRAY);
|
||||||
|
g.setFont(MinecraftFont.X16());
|
||||||
|
g.strokeText("P", 4, 14);
|
||||||
|
g.strokeText("T", canvas.getWidth() - 12, canvas.getHeight() - 6);
|
||||||
|
|
||||||
|
// 坐标轴
|
||||||
|
g.strokeLine(0, canvas.getHeight() * .5, canvas.getWidth(), canvas.getHeight() * .5);
|
||||||
|
g.strokeLine(canvas.getWidth() * .5, 0, canvas.getWidth() * .5, canvas.getHeight());
|
||||||
|
|
||||||
|
// 曲线
|
||||||
|
g.setStroke(Colorful.DARK_GRAY);
|
||||||
|
g.moveTo(0, canvas.getHeight());
|
||||||
|
for (int i = 0; i < list.length; i++) {
|
||||||
|
g.lineTo(list[i][0], canvas.getHeight() - list[i][1]);
|
||||||
|
}
|
||||||
|
g.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击重播
|
||||||
|
track.setOnMouseClicked(e -> transition.play());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重置位置 */
|
||||||
|
public void reset() {
|
||||||
|
block.setTranslateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,116 @@
|
|||||||
|
package com.imyeyu.fx.ui.examples.component.sidebar;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import com.imyeyu.fx.ui.components.Navigation;
|
||||||
|
import com.imyeyu.fx.ui.examples.service.PageService;
|
||||||
|
import com.imyeyu.inject.annotation.Component;
|
||||||
|
import com.imyeyu.inject.annotation.Inject;
|
||||||
|
import javafx.scene.control.ToggleButton;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导航
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-08-26 14:56
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class Sidebar extends Navigation {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private PageService pageService;
|
||||||
|
|
||||||
|
public Sidebar() {
|
||||||
|
Item welcome = new Item(SidebarItem.WELCOME);
|
||||||
|
Item style = new Item(SidebarItem.STYLE);
|
||||||
|
Item extendTools = new Item(SidebarItem.EXTEND_TOOLS);
|
||||||
|
Item bindingConfig = new Item(SidebarItem.BINDING_CONFIG);
|
||||||
|
Item alert = new Item(SidebarItem.ALERT);
|
||||||
|
Item popupTips = new Item(SidebarItem.POPUP_TIPS);
|
||||||
|
Item runAsync = new Item(SidebarItem.RUN_ASYNC);
|
||||||
|
Item animationRenderer = new Item(SidebarItem.ANIMATION_RENDERER);
|
||||||
|
|
||||||
|
add(welcome, style, extendTools, bindingConfig, alert, popupTips, runAsync, animationRenderer);
|
||||||
|
|
||||||
|
// 动画
|
||||||
|
Item interpolator = new Item(SidebarItem.INTERPOLATOR);
|
||||||
|
Item smoothScroll = new Item(SidebarItem.SMOOTH_SCROLL);
|
||||||
|
addGroup(TimiFXUI.MULTILINGUAL.text("animation"), interpolator, smoothScroll);
|
||||||
|
|
||||||
|
// 组件
|
||||||
|
SidebarItem[] components = {
|
||||||
|
SidebarItem.CHECK_BOX_PICKER,
|
||||||
|
SidebarItem.DATE_TIME_PICKER,
|
||||||
|
SidebarItem.EDITABLE_TABLE_CELL,
|
||||||
|
SidebarItem.FILE_TREE_VIEW,
|
||||||
|
SidebarItem.ICON_BUTTON,
|
||||||
|
SidebarItem.ICON_PICKER,
|
||||||
|
SidebarItem.LABEL_PROGRESS_BAR,
|
||||||
|
SidebarItem.NAVIGATION,
|
||||||
|
SidebarItem.NUMBER_FIELD,
|
||||||
|
SidebarItem.PROGRESS_SLIDER,
|
||||||
|
SidebarItem.SELECTABLE_LABEL,
|
||||||
|
SidebarItem.TEXT_AREA_EDITOR,
|
||||||
|
SidebarItem.TITLE_LABEL,
|
||||||
|
SidebarItem.TOGGLE_ICON,
|
||||||
|
SidebarItem.X_PAGINATION,
|
||||||
|
SidebarItem.X_TAB_PANE,
|
||||||
|
SidebarItem.X_TREE_VIEW
|
||||||
|
};
|
||||||
|
Item[] componentItems = new Item[components.length];
|
||||||
|
for (int i = 0; i < componentItems.length; i++) {
|
||||||
|
componentItems[i] = new Item(components[i]);
|
||||||
|
}
|
||||||
|
addGroup(TimiFXUI.MULTILINGUAL.text("component"), componentItems);
|
||||||
|
|
||||||
|
// 其他
|
||||||
|
Item draggableNode = new Item(SidebarItem.DRAGGABLE_NODE);
|
||||||
|
Item draggableWindow = new Item(SidebarItem.DRAGGABLE_WINDOW);
|
||||||
|
Item screen = new Item(SidebarItem.SCREEN);
|
||||||
|
Item tray = new Item(SidebarItem.TRAY);
|
||||||
|
addGroup(TimiFXUI.MULTILINGUAL.text("other"), draggableNode, draggableWindow, screen, tray);
|
||||||
|
|
||||||
|
getStyleClass().add(CSS.BORDER_R);
|
||||||
|
setMinWidth(140);
|
||||||
|
|
||||||
|
// ---------- 事件 ----------
|
||||||
|
|
||||||
|
// 跳转
|
||||||
|
selectedItem.addListener((obs, o, item) -> {
|
||||||
|
if (item instanceof Item i) {
|
||||||
|
pageService.to(i.item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置选中
|
||||||
|
*
|
||||||
|
* @param item 选中项
|
||||||
|
*/
|
||||||
|
public void setSelected(SidebarItem item) {
|
||||||
|
for (int i = 0; i < items.size(); i++) {
|
||||||
|
if (items.get(i) instanceof Item it && it.item == item) {
|
||||||
|
setSelectedItem(items.get(i));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new NullPointerException("not found item page for " + item);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列表项
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-02-22 19:45
|
||||||
|
*/
|
||||||
|
private static class Item extends ToggleButton {
|
||||||
|
|
||||||
|
/** 列表项对象 */
|
||||||
|
final SidebarItem item;
|
||||||
|
|
||||||
|
public Item(SidebarItem item) {
|
||||||
|
this.item = item;
|
||||||
|
setText(item.text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,160 @@
|
|||||||
|
package com.imyeyu.fx.ui.examples.component.sidebar;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import com.imyeyu.fx.ui.examples.TimiFXExamples;
|
||||||
|
import com.imyeyu.fx.ui.examples.component.AbstractPane;
|
||||||
|
import com.imyeyu.fx.ui.examples.view.pages.AlertDemo;
|
||||||
|
import com.imyeyu.fx.ui.examples.view.pages.AnimationRendererDemo;
|
||||||
|
import com.imyeyu.fx.ui.examples.view.pages.BindingsConfigDemo;
|
||||||
|
import com.imyeyu.fx.ui.examples.view.pages.ExtendDemo;
|
||||||
|
import com.imyeyu.fx.ui.examples.view.pages.PopupTipsDemo;
|
||||||
|
import com.imyeyu.fx.ui.examples.view.pages.RunAsyncDemo;
|
||||||
|
import com.imyeyu.fx.ui.examples.view.pages.Style;
|
||||||
|
import com.imyeyu.fx.ui.examples.view.pages.Welcome;
|
||||||
|
import com.imyeyu.fx.ui.examples.view.pages.animation.InterpolatorDemo;
|
||||||
|
import com.imyeyu.fx.ui.examples.view.pages.animation.SmoothScrollDemo;
|
||||||
|
import com.imyeyu.fx.ui.examples.view.pages.component.CheckBoxPickerDemo;
|
||||||
|
import com.imyeyu.fx.ui.examples.view.pages.component.DateTimePickerDemo;
|
||||||
|
import com.imyeyu.fx.ui.examples.view.pages.component.EditableTableCellDemo;
|
||||||
|
import com.imyeyu.fx.ui.examples.view.pages.component.FileTreeViewDemo;
|
||||||
|
import com.imyeyu.fx.ui.examples.view.pages.component.IconButtonDemo;
|
||||||
|
import com.imyeyu.fx.ui.examples.view.pages.component.IconPickerDemo;
|
||||||
|
import com.imyeyu.fx.ui.examples.view.pages.component.LabelProgressBarDemo;
|
||||||
|
import com.imyeyu.fx.ui.examples.view.pages.component.NavigationDemo;
|
||||||
|
import com.imyeyu.fx.ui.examples.view.pages.component.NumberFieldDemo;
|
||||||
|
import com.imyeyu.fx.ui.examples.view.pages.component.ProgressSliderDemo;
|
||||||
|
import com.imyeyu.fx.ui.examples.view.pages.component.SelectableLabelDemo;
|
||||||
|
import com.imyeyu.fx.ui.examples.view.pages.component.TextAreaEditorDemo;
|
||||||
|
import com.imyeyu.fx.ui.examples.view.pages.component.TitleLabelDemo;
|
||||||
|
import com.imyeyu.fx.ui.examples.view.pages.component.ToggleIconDemo;
|
||||||
|
import com.imyeyu.fx.ui.examples.view.pages.component.XPaginationDemo;
|
||||||
|
import com.imyeyu.fx.ui.examples.view.pages.component.XTabPaneDemo;
|
||||||
|
import com.imyeyu.fx.ui.examples.view.pages.component.XTreeViewDemo;
|
||||||
|
import com.imyeyu.fx.ui.examples.view.pages.other.DraggableNodeDemo;
|
||||||
|
import com.imyeyu.fx.ui.examples.view.pages.other.DraggableWindowDemo;
|
||||||
|
import com.imyeyu.fx.ui.examples.view.pages.other.ScreenFXDemo;
|
||||||
|
import com.imyeyu.fx.ui.examples.view.pages.other.TrayFXDemo;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导航项
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-08-26 14:56
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@AllArgsConstructor
|
||||||
|
public enum SidebarItem {
|
||||||
|
|
||||||
|
/** 欢迎页 */
|
||||||
|
WELCOME(Welcome.class, TimiFXUI.MULTILINGUAL.text("fx.example.welcome")),
|
||||||
|
|
||||||
|
/** 样式 */
|
||||||
|
STYLE(Style.class, TimiFXUI.MULTILINGUAL.text("style")),
|
||||||
|
|
||||||
|
/** 扩展工具 */
|
||||||
|
EXTEND_TOOLS(ExtendDemo.class, TimiFXUI.MULTILINGUAL.text("fx.example.extend_tools")),
|
||||||
|
|
||||||
|
/** 配置绑定 */
|
||||||
|
BINDING_CONFIG(BindingsConfigDemo.class, TimiFXUI.MULTILINGUAL.text("fx.example.binding_config")),
|
||||||
|
|
||||||
|
/** 弹窗 */
|
||||||
|
ALERT(AlertDemo.class, TimiFXUI.MULTILINGUAL.text("alert")),
|
||||||
|
|
||||||
|
/** 弹出提示 */
|
||||||
|
POPUP_TIPS(PopupTipsDemo.class, TimiFXUI.MULTILINGUAL.text("fx.example.popup_tips")),
|
||||||
|
|
||||||
|
/** 异步任务 */
|
||||||
|
RUN_ASYNC(RunAsyncDemo.class, TimiFXUI.MULTILINGUAL.text("fx.example.run_async")),
|
||||||
|
|
||||||
|
/** 异步任务 */
|
||||||
|
ANIMATION_RENDERER(AnimationRendererDemo.class, TimiFXUI.MULTILINGUAL.text("fx.example.animation_renderer")),
|
||||||
|
|
||||||
|
// ---------- 动画 ----------
|
||||||
|
|
||||||
|
/** 动画插值器 */
|
||||||
|
INTERPOLATOR(InterpolatorDemo.class, TimiFXUI.MULTILINGUAL.text("fx.example.interpolator")),
|
||||||
|
|
||||||
|
/** 平滑滚动 */
|
||||||
|
SMOOTH_SCROLL(SmoothScrollDemo.class, TimiFXUI.MULTILINGUAL.text("fx.example.smooth_scroll")),
|
||||||
|
|
||||||
|
// ---------- 组件 ----------
|
||||||
|
|
||||||
|
/** 复选框选择器 */
|
||||||
|
CHECK_BOX_PICKER(CheckBoxPickerDemo.class, TimiFXUI.MULTILINGUAL.text("fx.example.check_box_picker")),
|
||||||
|
|
||||||
|
/** 详细时间选择器 */
|
||||||
|
DATE_TIME_PICKER(DateTimePickerDemo.class, TimiFXUI.MULTILINGUAL.text("fx.example.date_time_picker")),
|
||||||
|
|
||||||
|
/** 表格可编辑单元格 */
|
||||||
|
EDITABLE_TABLE_CELL(EditableTableCellDemo.class, TimiFXUI.MULTILINGUAL.text("fx.example.editable_table_cell")),
|
||||||
|
|
||||||
|
/** 文件树视图 */
|
||||||
|
FILE_TREE_VIEW(FileTreeViewDemo.class, TimiFXUI.MULTILINGUAL.text("fx.example.file_tree_view")),
|
||||||
|
|
||||||
|
/** 图标按钮 */
|
||||||
|
ICON_BUTTON(IconButtonDemo.class, TimiFXUI.MULTILINGUAL.text("fx.example.icon_button")),
|
||||||
|
|
||||||
|
/** 图标选择器 */
|
||||||
|
ICON_PICKER(IconPickerDemo.class, TimiFXUI.MULTILINGUAL.text("fx.example.icon_picker")),
|
||||||
|
|
||||||
|
/** 标签进度 */
|
||||||
|
LABEL_PROGRESS_BAR(LabelProgressBarDemo.class, TimiFXUI.MULTILINGUAL.text("fx.example.label_progress_bar")),
|
||||||
|
|
||||||
|
/** 导航 */
|
||||||
|
NAVIGATION(NavigationDemo.class, TimiFXUI.MULTILINGUAL.text("fx.example.navigation")),
|
||||||
|
|
||||||
|
/** 数字输入 */
|
||||||
|
NUMBER_FIELD(NumberFieldDemo.class, TimiFXUI.MULTILINGUAL.text("fx.example.number_field")),
|
||||||
|
|
||||||
|
/** 进度调整 */
|
||||||
|
PROGRESS_SLIDER(ProgressSliderDemo.class, TimiFXUI.MULTILINGUAL.text("fx.example.progress_slider")),
|
||||||
|
|
||||||
|
/** 可选标签 */
|
||||||
|
SELECTABLE_LABEL(SelectableLabelDemo.class, TimiFXUI.MULTILINGUAL.text("fx.example.selectable_label")),
|
||||||
|
|
||||||
|
/** 文本域编辑器 */
|
||||||
|
TEXT_AREA_EDITOR(TextAreaEditorDemo.class, TimiFXUI.MULTILINGUAL.text("fx.example.text_area_editor")),
|
||||||
|
|
||||||
|
/** 标题标签 */
|
||||||
|
TITLE_LABEL(TitleLabelDemo.class, TimiFXUI.MULTILINGUAL.text("fx.example.title_label")),
|
||||||
|
|
||||||
|
/** 切换图标 */
|
||||||
|
TOGGLE_ICON(ToggleIconDemo.class, TimiFXUI.MULTILINGUAL.text("fx.example.toggle_icon")),
|
||||||
|
|
||||||
|
/** 分页 */
|
||||||
|
X_PAGINATION(XPaginationDemo.class, TimiFXUI.MULTILINGUAL.text("pagination")),
|
||||||
|
|
||||||
|
/** 标签面板 */
|
||||||
|
X_TAB_PANE(XTabPaneDemo.class, TimiFXUI.MULTILINGUAL.text("fx.example.x_tab_pane")),
|
||||||
|
|
||||||
|
/** 树形视图 */
|
||||||
|
X_TREE_VIEW(XTreeViewDemo.class, TimiFXUI.MULTILINGUAL.text("fx.example.x_tree_view")),
|
||||||
|
|
||||||
|
// ---------- 其他 ----------
|
||||||
|
|
||||||
|
/** 可拖动组件 */
|
||||||
|
DRAGGABLE_NODE(DraggableNodeDemo.class, TimiFXUI.MULTILINGUAL.text("fx.example.draggable_node")),
|
||||||
|
|
||||||
|
/** 可拖动窗体 */
|
||||||
|
DRAGGABLE_WINDOW(DraggableWindowDemo.class, TimiFXUI.MULTILINGUAL.text("fx.example.draggable_window")),
|
||||||
|
|
||||||
|
/** 多屏操作 */
|
||||||
|
SCREEN(ScreenFXDemo.class, TimiFXUI.MULTILINGUAL.text("fx.example.screen")),
|
||||||
|
|
||||||
|
/** 托盘操作 */
|
||||||
|
TRAY(TrayFXDemo.class, TimiFXUI.MULTILINGUAL.text("fx.example.tray"));
|
||||||
|
|
||||||
|
/** 页面控制反转类 */
|
||||||
|
final Class<? extends AbstractPane> page;
|
||||||
|
|
||||||
|
/** 文本 */
|
||||||
|
final String text;
|
||||||
|
|
||||||
|
/** @return 从 TimiInject 控制反转对象获取该页面 */
|
||||||
|
public Node getIOCPage() {
|
||||||
|
return TimiFXExamples.getInjectApp().injector().di(page);
|
||||||
|
}
|
||||||
|
}
|
||||||
67
src/test/java/com/imyeyu/fx/ui/examples/ctrl/Main.java
Normal file
67
src/test/java/com/imyeyu/fx/ui/examples/ctrl/Main.java
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
package com.imyeyu.fx.ui.examples.ctrl;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.ui.components.TrayFX;
|
||||||
|
import com.imyeyu.fx.ui.examples.TimiFXExamples;
|
||||||
|
import com.imyeyu.fx.ui.examples.component.sidebar.Sidebar;
|
||||||
|
import com.imyeyu.fx.ui.examples.component.sidebar.SidebarItem;
|
||||||
|
import com.imyeyu.fx.ui.examples.service.PageService;
|
||||||
|
import com.imyeyu.fx.ui.examples.view.ViewMain;
|
||||||
|
import com.imyeyu.inject.TimiInject;
|
||||||
|
import com.imyeyu.inject.annotation.IOCReturn;
|
||||||
|
import com.imyeyu.inject.annotation.Inject;
|
||||||
|
import com.imyeyu.inject.annotation.SuperInject;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
|
||||||
|
import java.awt.SplashScreen;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主控
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-05-03 15:43
|
||||||
|
*/
|
||||||
|
@SuperInject
|
||||||
|
public class Main extends ViewMain {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private TrayFX trayFX;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private Sidebar sidebar;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private PageService pageService;
|
||||||
|
|
||||||
|
private Stage stage;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start(Stage stage) {
|
||||||
|
this.stage = stage;
|
||||||
|
|
||||||
|
TimiInject.run(TimiFXExamples.getInjectApp()).ioc(this);
|
||||||
|
super.start(stage);
|
||||||
|
|
||||||
|
sidebar.setSelected(SidebarItem.WELCOME);
|
||||||
|
|
||||||
|
// 主窗体尺寸
|
||||||
|
config.getWidth().bind(stage.widthProperty());
|
||||||
|
config.getHeight().bind(stage.heightProperty());
|
||||||
|
|
||||||
|
if (SplashScreen.getSplashScreen() != null) {
|
||||||
|
SplashScreen.getSplashScreen().close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stop() {
|
||||||
|
TimiFXExamples.getConfigLoader().dump();
|
||||||
|
trayFX.remove();
|
||||||
|
stage.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 主窗体 */
|
||||||
|
@IOCReturn
|
||||||
|
public Stage getStage() {
|
||||||
|
return stage;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,62 @@
|
|||||||
|
package com.imyeyu.fx.ui.examples.service;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.ui.examples.component.AbstractPane;
|
||||||
|
import com.imyeyu.fx.ui.examples.component.RootLayout;
|
||||||
|
import com.imyeyu.fx.ui.examples.component.sidebar.SidebarItem;
|
||||||
|
import com.imyeyu.inject.annotation.Inject;
|
||||||
|
import com.imyeyu.inject.annotation.Service;
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||||
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 页面服务
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2021-12-26 10:57
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class PageService {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private RootLayout root;
|
||||||
|
|
||||||
|
private SidebarItem prev;
|
||||||
|
|
||||||
|
private final ObjectProperty<SidebarItem> activatedPageProperty;
|
||||||
|
|
||||||
|
public PageService() {
|
||||||
|
activatedPageProperty = new SimpleObjectProperty<>();
|
||||||
|
activatedPageProperty.addListener((obs, prev, now) -> {
|
||||||
|
this.prev = prev;
|
||||||
|
if (now != null && now.getIOCPage() instanceof AbstractPane pane) {
|
||||||
|
pane.show();
|
||||||
|
root.setCenter(pane);
|
||||||
|
}
|
||||||
|
if (prev != null && prev.getIOCPage() instanceof AbstractPane pane) {
|
||||||
|
pane.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跳转页面
|
||||||
|
*
|
||||||
|
* @param page 页面
|
||||||
|
*/
|
||||||
|
public void to(SidebarItem page) {
|
||||||
|
activatedPageProperty.set(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 返回上一个页面 */
|
||||||
|
public void back() {
|
||||||
|
to(prev);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return 当前页面监听 */
|
||||||
|
public ReadOnlyObjectProperty<SidebarItem> activatedPageProperty() {
|
||||||
|
return activatedPageProperty;
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/test/java/com/imyeyu/fx/ui/examples/util/Resources.java
Normal file
32
src/test/java/com/imyeyu/fx/ui/examples/util/Resources.java
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package com.imyeyu.fx.ui.examples.util;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import com.imyeyu.fx.ui.components.TrayFX;
|
||||||
|
import com.imyeyu.fx.ui.examples.TimiFXExamples;
|
||||||
|
import com.imyeyu.fx.ui.examples.bean.Config;
|
||||||
|
import com.imyeyu.inject.annotation.IOCReturn;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 静态资源
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-08-26 15:23
|
||||||
|
*/
|
||||||
|
@com.imyeyu.inject.annotation.Resources
|
||||||
|
public class Resources implements TimiFXUI {
|
||||||
|
|
||||||
|
public static final Image ICON_X64 = new Image(RESOURCE + "icon.png", 64, 64, true, false);
|
||||||
|
|
||||||
|
/** @return 配置 */
|
||||||
|
@IOCReturn
|
||||||
|
public Config config() {
|
||||||
|
return TimiFXExamples.getConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return 托盘 */
|
||||||
|
@IOCReturn
|
||||||
|
public TrayFX trayFX() {
|
||||||
|
return TrayFX.getInstance();
|
||||||
|
}
|
||||||
|
}
|
||||||
44
src/test/java/com/imyeyu/fx/ui/examples/view/ViewMain.java
Normal file
44
src/test/java/com/imyeyu/fx/ui/examples/view/ViewMain.java
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package com.imyeyu.fx.ui.examples.view;
|
||||||
|
|
||||||
|
import javafx.application.Application;
|
||||||
|
import javafx.scene.PerspectiveCamera;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import com.imyeyu.fx.ui.examples.bean.Config;
|
||||||
|
import com.imyeyu.fx.ui.examples.component.RootLayout;
|
||||||
|
import com.imyeyu.inject.annotation.Inject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主界面
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-05-03 14:53
|
||||||
|
*/
|
||||||
|
public abstract class ViewMain extends Application implements TimiFXUI {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
protected Config config;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private RootLayout root;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start(final Stage stage) {
|
||||||
|
PerspectiveCamera perspectiveCamera = new PerspectiveCamera(false);
|
||||||
|
perspectiveCamera.setTranslateX(0);
|
||||||
|
perspectiveCamera.setTranslateY(0);
|
||||||
|
perspectiveCamera.setTranslateZ(0);
|
||||||
|
|
||||||
|
Scene scene = new Scene(root);
|
||||||
|
scene.getStylesheets().addAll(CSS_STYLE, CSS_FONT);
|
||||||
|
scene.setCamera(perspectiveCamera);
|
||||||
|
stage.setTitle(TimiFXUI.MULTILINGUAL.text("fx.example.title"));
|
||||||
|
stage.getIcons().add(new Image(RESOURCE + "icon.png"));
|
||||||
|
stage.setScene(scene);
|
||||||
|
stage.setWidth(config.getWidth().get());
|
||||||
|
stage.setHeight(config.getHeight().get());
|
||||||
|
stage.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,225 @@
|
|||||||
|
package com.imyeyu.fx.ui.examples.view.pages;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.task.RunAsync;
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import com.imyeyu.fx.ui.components.alert.AbstractAlert;
|
||||||
|
import com.imyeyu.fx.ui.components.alert.AlertButton;
|
||||||
|
import com.imyeyu.fx.ui.components.alert.AlertConfirm;
|
||||||
|
import com.imyeyu.fx.ui.components.alert.AlertFileBlendSelector;
|
||||||
|
import com.imyeyu.fx.ui.components.alert.AlertFilePathSelector;
|
||||||
|
import com.imyeyu.fx.ui.components.alert.AlertFileSelector;
|
||||||
|
import com.imyeyu.fx.ui.components.alert.AlertLoading;
|
||||||
|
import com.imyeyu.fx.ui.components.alert.AlertTextArea;
|
||||||
|
import com.imyeyu.fx.ui.components.alert.AlertTextField;
|
||||||
|
import com.imyeyu.fx.ui.components.alert.AlertTips;
|
||||||
|
import com.imyeyu.fx.ui.components.alert.AlertType;
|
||||||
|
import com.imyeyu.fx.ui.examples.component.AbstractDemoPane;
|
||||||
|
import com.imyeyu.fx.ui.examples.component.sidebar.SidebarItem;
|
||||||
|
import com.imyeyu.fx.utils.Column;
|
||||||
|
import com.imyeyu.inject.annotation.Component;
|
||||||
|
import com.imyeyu.java.bean.timi.TimiCode;
|
||||||
|
import com.imyeyu.java.bean.timi.TimiException;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.CheckBox;
|
||||||
|
import javafx.scene.control.ComboBox;
|
||||||
|
import javafx.scene.control.ListView;
|
||||||
|
import javafx.scene.control.SelectionMode;
|
||||||
|
import javafx.scene.layout.GridPane;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 弹出窗体
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-09-01 14:39
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class AlertDemo extends AbstractDemoPane {
|
||||||
|
|
||||||
|
public AlertDemo() {
|
||||||
|
title.setText(SidebarItem.ALERT.getText());
|
||||||
|
document.sync("https://doc.imyeyu.net/timi-fx/net/imyeyu/timifx/component/alert/package-summary.html");
|
||||||
|
source.sync("https://git.imyeyu.net/Timi/timi-fx/src/master/src/main/java/net/imyeyu/timifx/component/alert");
|
||||||
|
|
||||||
|
// 一般
|
||||||
|
Button info = new Button(TimiFXUI.MULTILINGUAL.text("info"));
|
||||||
|
info.getStyleClass().add(CSS.BORDER_BLT);
|
||||||
|
Button warning = new Button(TimiFXUI.MULTILINGUAL.text("warning"));
|
||||||
|
warning.getStyleClass().add(CSS.BORDER_BLT);
|
||||||
|
Button warningDanger = new Button(TimiFXUI.MULTILINGUAL.text("warning"));
|
||||||
|
warningDanger.getStyleClass().add(CSS.BORDER_BLT);
|
||||||
|
Button error = new Button(TimiFXUI.MULTILINGUAL.text("error"));
|
||||||
|
error.getStyleClass().add(CSS.BORDER_BLT);
|
||||||
|
Button deadlyError = new Button(TimiFXUI.MULTILINGUAL.text("fx.example.alert.deadly_error"));
|
||||||
|
|
||||||
|
// 询问
|
||||||
|
Button confirmInfo = new Button(TimiFXUI.MULTILINGUAL.text("info"));
|
||||||
|
confirmInfo.getStyleClass().add(CSS.BORDER_BLT);
|
||||||
|
Button confirmWarning = new Button(TimiFXUI.MULTILINGUAL.text("warning"));
|
||||||
|
confirmWarning.getStyleClass().add(CSS.BORDER_BLT);
|
||||||
|
Button confirmWarningDanger = new Button(TimiFXUI.MULTILINGUAL.text("warning"));
|
||||||
|
confirmWarningDanger.getStyleClass().add(CSS.BORDER_BLT);
|
||||||
|
Button confirmError = new Button(TimiFXUI.MULTILINGUAL.text("error"));
|
||||||
|
confirmError.getStyleClass().add(CSS.BORDER_BLT);
|
||||||
|
Button confirmCancel = new Button(TimiFXUI.MULTILINGUAL.text("fx.example.alert.can_cancel"));
|
||||||
|
|
||||||
|
// 输入
|
||||||
|
Button textField = new Button(TimiFXUI.MULTILINGUAL.text("fx.example.alert.text_field"));
|
||||||
|
textField.getStyleClass().add(CSS.BORDER_BLT);
|
||||||
|
Button textArea = new Button(TimiFXUI.MULTILINGUAL.text("fx.example.alert.text_area"));
|
||||||
|
|
||||||
|
// 文件
|
||||||
|
Button file = new Button(TimiFXUI.MULTILINGUAL.text("file.select"));
|
||||||
|
file.getStyleClass().add(CSS.BORDER_BLT);
|
||||||
|
Button directory = new Button(TimiFXUI.MULTILINGUAL.text("file.select.directory"));
|
||||||
|
directory.getStyleClass().add(CSS.BORDER_BLT);
|
||||||
|
Button fileBlend = new Button(TimiFXUI.MULTILINGUAL.text("fx.example.alert.file_blend"));
|
||||||
|
|
||||||
|
// 加载中
|
||||||
|
Button loading = new Button(TimiFXUI.MULTILINGUAL.text("loading"));
|
||||||
|
loading.getStyleClass().add(CSS.BORDER_BLT);
|
||||||
|
|
||||||
|
// 自定义
|
||||||
|
Button custom = new Button(TimiFXUI.MULTILINGUAL.text("custom"));
|
||||||
|
|
||||||
|
setCenter(new GridPane() {{
|
||||||
|
getColumnConstraints().addAll(Column.KEY, Column.KEY);
|
||||||
|
setHgap(12);
|
||||||
|
setVgap(6);
|
||||||
|
setPadding(new Insets(20));
|
||||||
|
|
||||||
|
int row = 0;
|
||||||
|
addRow(row++, TimiFXUI.label(TimiFXUI.MULTILINGUAL.text("commonly")), new HBox() {{
|
||||||
|
getChildren().addAll(info, warning, warningDanger, error, deadlyError);
|
||||||
|
}});
|
||||||
|
addRow(row++, TimiFXUI.label(TimiFXUI.MULTILINGUAL.text("confirmation")), new HBox() {{
|
||||||
|
getChildren().addAll(confirmInfo, confirmWarning, confirmWarningDanger, confirmError, confirmCancel);
|
||||||
|
}});
|
||||||
|
addRow(row++, TimiFXUI.label(TimiFXUI.MULTILINGUAL.text("input")), new HBox(textField, textArea));
|
||||||
|
addRow(row++, TimiFXUI.label(TimiFXUI.MULTILINGUAL.text("file")), new HBox() {{
|
||||||
|
setSpacing(6);
|
||||||
|
setAlignment(Pos.CENTER_LEFT);
|
||||||
|
getChildren().addAll(new HBox(file, directory, fileBlend), TimiFXUI.label(TimiFXUI.MULTILINGUAL.text("fx.example.alert.multi_tips")));
|
||||||
|
}});
|
||||||
|
addRow(row, TimiFXUI.label(TimiFXUI.MULTILINGUAL.text("other")), new HBox(loading, custom));
|
||||||
|
}});
|
||||||
|
|
||||||
|
// ---------- 事件 ----------
|
||||||
|
|
||||||
|
String text = "TimiFX";
|
||||||
|
|
||||||
|
// 一般
|
||||||
|
info.setOnAction(e -> AlertTips.info(getScene().getWindow(), text));
|
||||||
|
warning.setOnAction(e -> AlertTips.warn(getScene().getWindow(), text));
|
||||||
|
warningDanger.setOnAction(e -> AlertTips.warnDanger(getScene().getWindow(), text));
|
||||||
|
error.setOnAction(e -> AlertTips.error(getScene().getWindow(), text));
|
||||||
|
deadlyError.setOnAction(e -> AlertTextArea.error(getScene().getWindow(), new TimiException(TimiCode.ERROR)));
|
||||||
|
|
||||||
|
AlertConfirm alertConfirm = new AlertConfirm(text) {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onConfirm() {
|
||||||
|
AlertTips.info(getScene().getWindow(), TimiFXUI.MULTILINGUAL.text("yes"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCancel() {
|
||||||
|
AlertTips.info(getScene().getWindow(), TimiFXUI.MULTILINGUAL.text("cancel"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
alertConfirm.setOnCloseRequest(e -> AlertTips.info(getScene().getWindow(), TimiFXUI.MULTILINGUAL.text("cancel")));
|
||||||
|
|
||||||
|
// 询问
|
||||||
|
confirmInfo.setOnAction(e -> {
|
||||||
|
alertConfirm.setType(AlertType.INFORMATION);
|
||||||
|
alertConfirm.autoSize().showRelativeCenter(getScene().getWindow());
|
||||||
|
});
|
||||||
|
confirmWarning.setOnAction(e -> {
|
||||||
|
alertConfirm.setType(AlertType.WARNING);
|
||||||
|
alertConfirm.autoSize().showRelativeCenter(getScene().getWindow());
|
||||||
|
});
|
||||||
|
confirmWarningDanger.setOnAction(e -> {
|
||||||
|
alertConfirm.setType(AlertType.WARNING_DANGER);
|
||||||
|
alertConfirm.autoSize().showRelativeCenter(getScene().getWindow());
|
||||||
|
});
|
||||||
|
confirmError.setOnAction(e -> {
|
||||||
|
alertConfirm.setType(AlertType.ERROR);
|
||||||
|
alertConfirm.autoSize().showRelativeCenter(getScene().getWindow());
|
||||||
|
});
|
||||||
|
confirmCancel.setOnAction(e -> {
|
||||||
|
AlertTips alert = new AlertTips(text);
|
||||||
|
alert.setType(AlertType.WARNING_DANGER);
|
||||||
|
alert.setButton(AlertButton.yes(), AlertButton.no(), AlertButton.cancel());
|
||||||
|
alert.setOnActionEvent(action -> {
|
||||||
|
switch (action) {
|
||||||
|
case YES -> AlertTips.info(getScene().getWindow(), TimiFXUI.MULTILINGUAL.text("yes"));
|
||||||
|
case NO -> AlertTips.info(getScene().getWindow(), TimiFXUI.MULTILINGUAL.text("no"));
|
||||||
|
case CANCEL -> AlertTips.info(getScene().getWindow(), TimiFXUI.MULTILINGUAL.text("cancel"));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
alert.setOnCloseRequest(closeE -> AlertTips.info(getScene().getWindow(), TimiFXUI.MULTILINGUAL.text("cancel")));
|
||||||
|
alert.autoSize().showRelativeCenter(getScene().getWindow());
|
||||||
|
});
|
||||||
|
|
||||||
|
// 输入
|
||||||
|
textField.setOnAction(e -> AlertTextField.confirm(getScene().getWindow(), text, value -> AlertTips.info(getScene().getWindow(), value)));
|
||||||
|
textArea.setOnAction(e -> AlertTextArea.info(getScene().getWindow(), text));
|
||||||
|
|
||||||
|
// 文件
|
||||||
|
file.setOnAction(e -> new AlertFileSelector(SelectionMode.SINGLE).showRelativeCenter(getScene().getWindow()));
|
||||||
|
directory.setOnAction(e -> new AlertFilePathSelector(SelectionMode.SINGLE).showRelativeCenter(getScene().getWindow()));
|
||||||
|
fileBlend.setOnAction(e -> new AlertFileBlendSelector(SelectionMode.MULTIPLE).showRelativeCenter(getScene().getWindow()));
|
||||||
|
|
||||||
|
// 加载中
|
||||||
|
loading.setOnAction(e -> {
|
||||||
|
AlertLoading alert = new AlertLoading(TimiFXUI.MULTILINGUAL.text("loading"));
|
||||||
|
alert.showRelativeCenter(getScene().getWindow());
|
||||||
|
RunAsync.later(alert::close, 2000);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 自定义
|
||||||
|
CustomAlert customAlert = new CustomAlert();
|
||||||
|
custom.setOnAction(e -> customAlert.showRelativeCenter(getScene().getWindow()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义窗体
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-09-02 15:21
|
||||||
|
*/
|
||||||
|
private static class CustomAlert extends AbstractAlert {
|
||||||
|
|
||||||
|
public CustomAlert() {
|
||||||
|
ListView<String> list = new ListView<>();
|
||||||
|
ComboBox<String> comboBox = new ComboBox<>();
|
||||||
|
CheckBox checkBox = new CheckBox("CheckBox");
|
||||||
|
checkBox.setSelected(true);
|
||||||
|
|
||||||
|
root.setPadding(new Insets(8, 12, 8, 12));
|
||||||
|
root.setTop(TimiFXUI.label("Custom Tips"));
|
||||||
|
root.setCenter(list);
|
||||||
|
|
||||||
|
setType(AlertType.INFORMATION);
|
||||||
|
setTitle(TimiFXUI.MULTILINGUAL.text("custom"));
|
||||||
|
setWidth(520);
|
||||||
|
setHeight(460);
|
||||||
|
setRightButtons(AlertButton.confirm(), AlertButton.cancel());
|
||||||
|
|
||||||
|
btnPane.setPadding(new Insets(12, 0, 0, 0));
|
||||||
|
leftButtons.getChildren().addAll(comboBox, checkBox);
|
||||||
|
for (int i = 0; i < 20; i++) {
|
||||||
|
list.getItems().add("item " + i);
|
||||||
|
comboBox.getItems().add("item " + i);
|
||||||
|
}
|
||||||
|
comboBox.getSelectionModel().select(0);
|
||||||
|
|
||||||
|
// ---------- 事件 ----------
|
||||||
|
|
||||||
|
setOnActionEvent(action -> true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,173 @@
|
|||||||
|
package com.imyeyu.fx.ui.examples.view.pages;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.BindingUtils;
|
||||||
|
import com.imyeyu.fx.TimiFX;
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import com.imyeyu.fx.ui.components.SelectableLabel;
|
||||||
|
import com.imyeyu.fx.ui.components.TextAreaEditor;
|
||||||
|
import com.imyeyu.fx.ui.components.alert.AlertTips;
|
||||||
|
import com.imyeyu.fx.ui.components.popup.PopupTipsService;
|
||||||
|
import com.imyeyu.fx.ui.examples.TimiFXExamples;
|
||||||
|
import com.imyeyu.fx.ui.examples.component.AbstractDemoPane;
|
||||||
|
import com.imyeyu.fx.ui.examples.component.sidebar.SidebarItem;
|
||||||
|
import com.imyeyu.fx.ui.examples.ctrl.Main;
|
||||||
|
import com.imyeyu.fx.utils.AnimationRenderer;
|
||||||
|
import com.imyeyu.fx.utils.Column;
|
||||||
|
import com.imyeyu.inject.TimiInject;
|
||||||
|
import com.imyeyu.inject.annotation.Component;
|
||||||
|
import com.imyeyu.io.IO;
|
||||||
|
import com.imyeyu.utils.OS;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.geometry.Point3D;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.Slider;
|
||||||
|
import javafx.scene.control.SplitPane;
|
||||||
|
import javafx.scene.layout.BorderPane;
|
||||||
|
import javafx.scene.layout.GridPane;
|
||||||
|
import javafx.scene.paint.PhongMaterial;
|
||||||
|
import javafx.scene.shape.Box;
|
||||||
|
import javafx.scene.shape.CullFace;
|
||||||
|
import javafx.scene.shape.DrawMode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 动画渲染器
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2023-04-13 10:46
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class AnimationRendererDemo extends AbstractDemoPane implements OS.FileSystem {
|
||||||
|
|
||||||
|
private final AnimationRenderer renderer;
|
||||||
|
|
||||||
|
public AnimationRendererDemo() {
|
||||||
|
title.setText(SidebarItem.ANIMATION_RENDERER.getText());
|
||||||
|
document.sync("https://doc.imyeyu.net/timi-fx/net/imyeyu/timifx/extend/AnimationRenderer.html");
|
||||||
|
source.sync("https://git.imyeyu.net/Timi/timi-fx/src/master/src/main/java/net/imyeyu/timifx/extend/AnimationRenderer.java");
|
||||||
|
tips.setText(TimiFXUI.MULTILINGUAL.text("fx.example.animation_renderer.tips"));
|
||||||
|
|
||||||
|
// 动画旋转立方体
|
||||||
|
Box box = new Box(128, 128, 128);
|
||||||
|
box.setDrawMode(DrawMode.LINE);
|
||||||
|
box.setCullFace(CullFace.BACK);
|
||||||
|
box.setMaterial(new PhongMaterial(Colorful.BLACK));
|
||||||
|
box.setTranslateY(60);
|
||||||
|
box.setRotationAxis(new Point3D(0, 64, 0));
|
||||||
|
renderer = new AnimationRenderer();
|
||||||
|
renderer.addRenderCallback(deltaSecond -> box.setRotate(box.getRotate() + 90 * deltaSecond));
|
||||||
|
|
||||||
|
String value = System.getProperty("javafx.animation.fullspeed");
|
||||||
|
boolean isFullSpeed = value != null && value.equalsIgnoreCase("true");
|
||||||
|
|
||||||
|
// 当前状态
|
||||||
|
Label labelStatus = TimiFXUI.label(TimiFXUI.MULTILINGUAL.text("fx.example.animation_renderer.status"));
|
||||||
|
SelectableLabel status = new SelectableLabel("-Djavafx.animation.fullspeed=" + isFullSpeed);
|
||||||
|
|
||||||
|
// 重启
|
||||||
|
Button restart = new Button(TimiFXUI.MULTILINGUAL.textArgs("fx.example.animation_renderer.restart", TimiFXUI.MULTILINGUAL.text(isFullSpeed ? "disable" : "enable")));
|
||||||
|
|
||||||
|
// 示例
|
||||||
|
Label labelDemo = TimiFXUI.title(TimiFXUI.MULTILINGUAL.text("example"));
|
||||||
|
TextAreaEditor demo = new TextAreaEditor();
|
||||||
|
demo.getStyleClass().add(CSS.BORDER_N);
|
||||||
|
demo.setEditable(false);
|
||||||
|
demo.setText("""
|
||||||
|
Box box = new Box(128, 128, 128);
|
||||||
|
box.setDrawMode(DrawMode.LINE);
|
||||||
|
box.setCullFace(CullFace.BACK);
|
||||||
|
box.setMaterial(new PhongMaterial(RED));
|
||||||
|
box.setRotationAxis(new Point3D(0, 64, 0));
|
||||||
|
|
||||||
|
// 每秒 90 度旋转一个 3D 立方体,默认 60 FPS
|
||||||
|
AnimationRenderer renderer = new AnimationRenderer();
|
||||||
|
renderer.addRenderCallback(deltaSecond -> {
|
||||||
|
box.setRotate(box.getRotate() + 90 * deltaSecond);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 控制 FPS
|
||||||
|
Slider fps = new Slider(5, 240, 60);
|
||||||
|
fps.valueProperty().addListener((obs, o, newFps) -> {
|
||||||
|
renderer.setPrefFPS(newFps.intValue())
|
||||||
|
});
|
||||||
|
""".trim());
|
||||||
|
|
||||||
|
// 预设 FPS
|
||||||
|
Label labelPrefFPS = TimiFXUI.label(TimiFXUI.MULTILINGUAL.text("fx.example.animation_renderer.pref_fps"));
|
||||||
|
Slider prefFPS = new Slider(5, 240, 60);
|
||||||
|
prefFPS.valueProperty().addListener((obs, o, newFps) -> renderer.setPrefFPS(newFps.intValue()));
|
||||||
|
PopupTipsService.installBindingText(prefFPS, BindingUtils.integerStringBinding(prefFPS.valueProperty()));
|
||||||
|
renderer.setPrefFPS((int) prefFPS.getValue());
|
||||||
|
|
||||||
|
// 当前渲染 FPS
|
||||||
|
Label labelFPS = TimiFXUI.label(TimiFXUI.MULTILINGUAL.text("fps"));
|
||||||
|
Label fps = new Label();
|
||||||
|
fps.textProperty().bind(renderer.fpsProperty().asString("%d"));
|
||||||
|
|
||||||
|
Label labelMPF = TimiFXUI.label(TimiFXUI.MULTILINGUAL.text("mpf"));
|
||||||
|
Label mpf = new Label();
|
||||||
|
mpf.textProperty().bind(renderer.mpfProperty().asString("%.2f ms"));
|
||||||
|
|
||||||
|
setCenter(new BorderPane() {{
|
||||||
|
setTop(new GridPane() {{
|
||||||
|
getColumnConstraints().addAll(Column.KEY, Column.VALUE_FILL);
|
||||||
|
setVgap(6);
|
||||||
|
setHgap(6);
|
||||||
|
setPadding(new Insets(20));
|
||||||
|
|
||||||
|
addRow(0, labelStatus, status);
|
||||||
|
add(restart, 1, 1);
|
||||||
|
}});
|
||||||
|
setCenter(new BorderPane() {{
|
||||||
|
setBorder(Stroke.TOP);
|
||||||
|
setTop(labelDemo);
|
||||||
|
setCenter(new SplitPane() {{
|
||||||
|
setBorder(Stroke.TOP);
|
||||||
|
setDividerPositions(.5);
|
||||||
|
getItems().addAll(demo, new BorderPane() {{
|
||||||
|
setTop(new GridPane() {{
|
||||||
|
getColumnConstraints().addAll(Column.KEY, Column.VALUE_FILL);
|
||||||
|
setHgap(6);
|
||||||
|
setVgap(6);
|
||||||
|
setPadding(new Insets(8, 12, 8, 12));
|
||||||
|
setBorder(Stroke.BOTTOM);
|
||||||
|
|
||||||
|
int row = 0;
|
||||||
|
addRow(row++, labelPrefFPS, prefFPS);
|
||||||
|
addRow(row++, labelFPS, fps);
|
||||||
|
addRow(row, labelMPF, mpf);
|
||||||
|
}});
|
||||||
|
setCenter(box);
|
||||||
|
}});
|
||||||
|
}});
|
||||||
|
}});
|
||||||
|
}});
|
||||||
|
|
||||||
|
// ---------- 事件 ----------
|
||||||
|
|
||||||
|
restart.setOnAction(e -> {
|
||||||
|
// JRE
|
||||||
|
String jre = System.getProperty("java.home") + SEP + "bin" + SEP + "java";
|
||||||
|
// 启动参数
|
||||||
|
String param = " -Djavafx.animation.fullspeed=%s -XX:+UnlockExperimentalVMOptions -XX:+UseZGC -jar ".formatted(!isFullSpeed);
|
||||||
|
// 启动 Jar
|
||||||
|
String jar = IO.getJarAbsolutePath(getClass());
|
||||||
|
// 重启
|
||||||
|
try {
|
||||||
|
TimiFX.doRestart(TimiFXExamples.getInjectApp().injector().di(Main.class), jre + param + jar);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
AlertTips.error(getScene().getWindow(), TimiFXUI.MULTILINGUAL.text("tips.restart.error"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onShow() {
|
||||||
|
renderer.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onHide() {
|
||||||
|
renderer.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,103 @@
|
|||||||
|
package com.imyeyu.fx.ui.examples.view.pages;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import com.imyeyu.fx.ui.components.TextAreaEditor;
|
||||||
|
import com.imyeyu.fx.ui.components.TextFlower;
|
||||||
|
import com.imyeyu.fx.ui.components.TitleLabel;
|
||||||
|
import com.imyeyu.fx.ui.components.XHyperlink;
|
||||||
|
import com.imyeyu.fx.ui.examples.bean.Config;
|
||||||
|
import com.imyeyu.fx.ui.examples.component.AbstractDemoPane;
|
||||||
|
import com.imyeyu.fx.ui.examples.component.sidebar.SidebarItem;
|
||||||
|
import com.imyeyu.inject.annotation.Component;
|
||||||
|
import com.imyeyu.inject.annotation.Inject;
|
||||||
|
import com.imyeyu.inject.annotation.InvokeForInjected;
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.layout.BorderPane;
|
||||||
|
import javafx.scene.layout.GridPane;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置绑定示例
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-09-01 14:31
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class BindingsConfigDemo extends AbstractDemoPane {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private Config config;
|
||||||
|
|
||||||
|
private final Label stageSize, interpolatorDuration;
|
||||||
|
|
||||||
|
public BindingsConfigDemo() {
|
||||||
|
title.setText(SidebarItem.BINDING_CONFIG.getText());
|
||||||
|
document.sync("https://doc.imyeyu.net/timi-fx/net/imyeyu/timifx/config/BindingsConfig.html");
|
||||||
|
source.sync("https://git.imyeyu.net/Timi/timi-fx/src/master/src/main/java/net/imyeyu/timifx/config/BindingsConfig.java");
|
||||||
|
|
||||||
|
// timi-java 说明
|
||||||
|
TextFlower timijavaTips = new TextFlower().matcher(TimiFXUI.MULTILINGUAL.text("fx.example.binding_config.tips"));
|
||||||
|
XHyperlink timijavaDescription = new XHyperlink("https://www.imyeyu.net/article/public/aid117.html#配置系统");
|
||||||
|
XHyperlink timijavaDocument = new XHyperlink("https://doc.imyeyu.net/timi-java/net/imyeyu/timijava/config/package-summary.html");
|
||||||
|
XHyperlink timijavaSource = new XHyperlink("https://git.imyeyu.net/Timi/timi-java/src/master/src/main/java/net/imyeyu/timijava/config");
|
||||||
|
|
||||||
|
// 窗体尺寸
|
||||||
|
stageSize = new Label();
|
||||||
|
|
||||||
|
// 动画插值器持续时间
|
||||||
|
interpolatorDuration = new Label();
|
||||||
|
|
||||||
|
// 代码
|
||||||
|
TextAreaEditor code = new TextAreaEditor();
|
||||||
|
code.setEditable(false);
|
||||||
|
code.setText("""
|
||||||
|
// config 为 timi-java 的配置对象
|
||||||
|
|
||||||
|
// 窗体尺寸,单向绑定
|
||||||
|
BindingsConfig.cfg(config).bindDoubleProperty(stage.widthProperty(), Config.section("Main").key("Width"));
|
||||||
|
BindingsConfig.cfg(config).bindDoubleProperty(stage.heightProperty(), Config.section("Main").key("Height"));
|
||||||
|
|
||||||
|
// 对组件双向绑定,duration 为 Slider
|
||||||
|
BindingsConfig.cfg(config).bindDoubleProperty(duration, Config.section("Interpolator").key("Duration"));
|
||||||
|
""".trim());
|
||||||
|
|
||||||
|
setCenter(new VBox() {{
|
||||||
|
setSpacing(12);
|
||||||
|
getChildren().addAll(new GridPane() {{
|
||||||
|
setVgap(4);
|
||||||
|
setHgap(6);
|
||||||
|
setPadding(new Insets(20));
|
||||||
|
|
||||||
|
int row = 0;
|
||||||
|
add(timijavaTips, 0, row++, 2, 1);
|
||||||
|
addRow(row++, TimiFXUI.label(TimiFXUI.MULTILINGUAL.text("description")), timijavaDescription);
|
||||||
|
addRow(row++, TimiFXUI.label(TimiFXUI.MULTILINGUAL.text("document")), timijavaDocument);
|
||||||
|
addRow(row, TimiFXUI.label(TimiFXUI.MULTILINGUAL.text("source")), timijavaSource);
|
||||||
|
}}, new BorderPane() {{
|
||||||
|
setPadding(new Insets(20));
|
||||||
|
setTop(new TitleLabel(TimiFXUI.MULTILINGUAL.text("example")));
|
||||||
|
setCenter(new GridPane() {{
|
||||||
|
setVgap(4);
|
||||||
|
setHgap(6);
|
||||||
|
setPadding(new Insets(20));
|
||||||
|
|
||||||
|
addRow(0, TimiFXUI.label(TimiFXUI.MULTILINGUAL.text("fx.example.binding_config.stage_size")), stageSize);
|
||||||
|
addRow(1, TimiFXUI.label(TimiFXUI.MULTILINGUAL.text("fx.example.binding_config.interpolator_duration")), interpolatorDuration);
|
||||||
|
}});
|
||||||
|
setBottom(new BorderPane() {{
|
||||||
|
setMargin(code, new Insets(12, 0, 0, 0));
|
||||||
|
setTop(new TextFlower().matcher(TimiFXUI.MULTILINGUAL.text("fx.example.binding_config.description")));
|
||||||
|
setCenter(code);
|
||||||
|
}});
|
||||||
|
}});
|
||||||
|
}});
|
||||||
|
}
|
||||||
|
|
||||||
|
@InvokeForInjected
|
||||||
|
private void config() {
|
||||||
|
stageSize.textProperty().bind(Bindings.createStringBinding(() -> "[%s, %s]".formatted(config.getWidth().get(), config.getHeight().get()), config.getWidth(), config.getHeight()));
|
||||||
|
interpolatorDuration.textProperty().bind(config.getInterpolator().getDuration().asString());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,118 @@
|
|||||||
|
package com.imyeyu.fx.ui.examples.view.pages;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import com.imyeyu.fx.ui.components.TitleLabel;
|
||||||
|
import com.imyeyu.fx.ui.components.XHyperlink;
|
||||||
|
import com.imyeyu.fx.ui.examples.component.AbstractDemoPane;
|
||||||
|
import com.imyeyu.fx.ui.examples.component.sidebar.SidebarItem;
|
||||||
|
import com.imyeyu.fx.utils.SmoothScroll;
|
||||||
|
import com.imyeyu.inject.annotation.Component;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.scene.control.ScrollPane;
|
||||||
|
import javafx.scene.layout.BorderPane;
|
||||||
|
import javafx.scene.layout.GridPane;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扩展类
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-09-06 00:07
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class ExtendDemo extends AbstractDemoPane {
|
||||||
|
|
||||||
|
public ExtendDemo() {
|
||||||
|
title.setText(SidebarItem.EXTEND_TOOLS.getText());
|
||||||
|
document.sync("https://doc.imyeyu.net/timi-fx/net/imyeyu/timifx/extend/package-summary.html");
|
||||||
|
source.sync("https://git.imyeyu.net/Timi/timi-fx/src/master/src/main/java/net/imyeyu/timifx/extend");
|
||||||
|
tips.setText(TimiFXUI.MULTILINGUAL.text("fx.example.extend_tools.tips"));
|
||||||
|
tips.setBorder(Stroke.BOTTOM);
|
||||||
|
|
||||||
|
setCenter(new ScrollPane() {{
|
||||||
|
setPadding(new Insets(20));
|
||||||
|
setFitToWidth(true);
|
||||||
|
setContent(new VBox() {{
|
||||||
|
setPadding(new Insets(32, 20, 10, 20));
|
||||||
|
setSpacing(6);
|
||||||
|
getChildren().addAll(new Item() {{
|
||||||
|
title.setText(TimiFXUI.MULTILINGUAL.text("fx.example.extend_tools.x_border"));
|
||||||
|
document.sync("https://doc.imyeyu.net/timi-fx/net/imyeyu/timifx/extend/XBorder.html");
|
||||||
|
source.sync("https://git.imyeyu.net/Timi/timi-fx/src/master/src/main/java/net/imyeyu/timifx/extend/XBorder.java");
|
||||||
|
}}, new Item() {{
|
||||||
|
title.setText(TimiFXUI.MULTILINGUAL.text("fx.example.extend_tools.bg_fill"));
|
||||||
|
document.sync("https://doc.imyeyu.net/timi-fx/net/imyeyu/timifx/extend/BgFill.html");
|
||||||
|
source.sync("https://git.imyeyu.net/Timi/timi-fx/src/master/src/main/java/net/imyeyu/timifx/extend/BgFill.java");
|
||||||
|
}}, new Item() {{
|
||||||
|
title.setText(TimiFXUI.MULTILINGUAL.text("fx.example.extend_tools.bg_image"));
|
||||||
|
document.sync("https://doc.imyeyu.net/timi-fx/net/imyeyu/timifx/extend/BgImage.html");
|
||||||
|
source.sync("https://git.imyeyu.net/Timi/timi-fx/src/master/src/main/java/net/imyeyu/timifx/extend/BgImage.java");
|
||||||
|
}}, new Item() {{
|
||||||
|
title.setText(TimiFXUI.MULTILINGUAL.text("fx.example.extend_tools.column"));
|
||||||
|
document.sync("https://doc.imyeyu.net/timi-fx/net/imyeyu/timifx/extend/Column.html");
|
||||||
|
source.sync("https://git.imyeyu.net/Timi/timi-fx/src/master/src/main/java/net/imyeyu/timifx/extend/Column.java");
|
||||||
|
}}, new Item() {{
|
||||||
|
title.setText(TimiFXUI.MULTILINGUAL.text("fx.example.extend_tools.row"));
|
||||||
|
document.sync("https://doc.imyeyu.net/timi-fx/net/imyeyu/timifx/extend/Row.html");
|
||||||
|
source.sync("https://git.imyeyu.net/Timi/timi-fx/src/master/src/main/java/net/imyeyu/timifx/extend/Row.java");
|
||||||
|
}}, new Item() {{
|
||||||
|
title.setText(TimiFXUI.MULTILINGUAL.text("fx.example.extend_tools.directory_selector"));
|
||||||
|
document.sync("https://doc.imyeyu.net/timi-fx/net/imyeyu/timifx/extend/DirectorySelector.html");
|
||||||
|
source.sync("https://git.imyeyu.net/Timi/timi-fx/src/master/src/main/java/net/imyeyu/timifx/extend/DirectorySelector.java");
|
||||||
|
}}, new Item() {{
|
||||||
|
title.setText(TimiFXUI.MULTILINGUAL.text("fx.example.extend_tools.file_selector"));
|
||||||
|
document.sync("https://doc.imyeyu.net/timi-fx/net/imyeyu/timifx/extend/FileSelector.html");
|
||||||
|
source.sync("https://git.imyeyu.net/Timi/timi-fx/src/master/src/main/java/net/imyeyu/timifx/extend/FileSelector.java");
|
||||||
|
}}, new Item() {{
|
||||||
|
title.setText(TimiFXUI.MULTILINGUAL.text("fx.example.extend_tools.logic_bindings"));
|
||||||
|
document.sync("https://doc.imyeyu.net/timi-fx/net/imyeyu/timifx/extend/LogicBindings.html");
|
||||||
|
source.sync("https://git.imyeyu.net/Timi/timi-fx/src/master/src/main/java/net/imyeyu/timifx/extend/LogicBindings.java");
|
||||||
|
}}, new Item() {{
|
||||||
|
title.setText(TimiFXUI.MULTILINGUAL.text("fx.example.extend_tools.no_selection_model"));
|
||||||
|
document.sync("https://doc.imyeyu.net/timi-fx/net/imyeyu/timifx/extend/NoSelectionModel.html");
|
||||||
|
source.sync("https://git.imyeyu.net/Timi/timi-fx/src/master/src/main/java/net/imyeyu/timifx/extend/NoSelectionModel.java");
|
||||||
|
}}, new Item() {{
|
||||||
|
title.setText(TimiFXUI.MULTILINGUAL.text("fx.example.extend_tools.text_flower"));
|
||||||
|
document.sync("https://doc.imyeyu.net/timi-fx/net/imyeyu/timifx/extend/TextFlower.html");
|
||||||
|
source.sync("https://git.imyeyu.net/Timi/timi-fx/src/master/src/main/java/net/imyeyu/timifx/extend/TextFlower.java");
|
||||||
|
}}, new Item() {{
|
||||||
|
title.setText(TimiFXUI.MULTILINGUAL.text("fx.example.extend_tools.x_anchor_pane"));
|
||||||
|
document.sync("https://doc.imyeyu.net/timi-fx/net/imyeyu/timifx/extend/XAnchorPane.html");
|
||||||
|
source.sync("https://git.imyeyu.net/Timi/timi-fx/src/master/src/main/java/net/imyeyu/timifx/extend/XAnchorPane.java");
|
||||||
|
}});
|
||||||
|
}});
|
||||||
|
SmoothScroll.scrollPane(this);
|
||||||
|
}});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列表项
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-09-06 02:17
|
||||||
|
*/
|
||||||
|
private static class Item extends BorderPane {
|
||||||
|
|
||||||
|
/** 标题 */
|
||||||
|
final TitleLabel title = new TitleLabel();
|
||||||
|
|
||||||
|
/** 文档 */
|
||||||
|
final XHyperlink document = new XHyperlink();
|
||||||
|
|
||||||
|
/** 源码 */
|
||||||
|
final XHyperlink source = new XHyperlink();
|
||||||
|
|
||||||
|
public Item() {
|
||||||
|
setPadding(new Insets(0, 0, 32, 0));
|
||||||
|
setTop(title);
|
||||||
|
setCenter(new GridPane() {{
|
||||||
|
setHgap(6);
|
||||||
|
setVgap(4);
|
||||||
|
setPadding(new Insets(10, 0, 10, 20));
|
||||||
|
|
||||||
|
addRow(0, TimiFXUI.label(TimiFXUI.MULTILINGUAL.text("document")), document);
|
||||||
|
addRow(1, TimiFXUI.label(TimiFXUI.MULTILINGUAL.text("source")), source);
|
||||||
|
}});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,77 @@
|
|||||||
|
package com.imyeyu.fx.ui.examples.view.pages;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import com.imyeyu.fx.ui.components.popup.PopupTipsService;
|
||||||
|
import com.imyeyu.fx.ui.components.popup.tips.AbstractPopupTips;
|
||||||
|
import com.imyeyu.fx.ui.examples.component.AbstractDemoPane;
|
||||||
|
import com.imyeyu.fx.ui.examples.component.sidebar.SidebarItem;
|
||||||
|
import com.imyeyu.inject.annotation.Component;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.ListView;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.scene.layout.BorderPane;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 弹出提示
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-09-01 14:40
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class PopupTipsDemo extends AbstractDemoPane {
|
||||||
|
|
||||||
|
public PopupTipsDemo() {
|
||||||
|
title.setText(SidebarItem.POPUP_TIPS.getText());
|
||||||
|
document.sync("https://doc.imyeyu.net/timi-fx/net/imyeyu/timifx/component/popup/PopupTipsService.html");
|
||||||
|
source.sync("https://git.imyeyu.net/Timi/timi-fx/src/master/src/main/java/net/imyeyu/timifx/component/popup/AbstractPopupTips.java");
|
||||||
|
tips.setText(TimiFXUI.MULTILINGUAL.text("fx.example.popup_tips.tips"));
|
||||||
|
|
||||||
|
// 一般
|
||||||
|
Button text = new Button(TimiFXUI.MULTILINGUAL.text("text"));
|
||||||
|
PopupTipsService.installText(text, TimiFXUI.MULTILINGUAL.text("fx.example.demo.text"));
|
||||||
|
|
||||||
|
// 长文本
|
||||||
|
Button textLong = new Button(TimiFXUI.MULTILINGUAL.text("fx.example.popup_tips.text_long"));
|
||||||
|
textLong.getStyleClass().add(CSS.BORDER_TRB);
|
||||||
|
PopupTipsService.installText(textLong, TimiFXUI.MULTILINGUAL.text("fx.example.demo.text_long"));
|
||||||
|
|
||||||
|
// 图片
|
||||||
|
Button image = new Button(TimiFXUI.MULTILINGUAL.text("image"));
|
||||||
|
image.getStyleClass().add(CSS.BORDER_TRB);
|
||||||
|
PopupTipsService.installImage(image, new Image(RESOURCE + "icon.png", 64, 64, false, false));
|
||||||
|
|
||||||
|
// 自定义
|
||||||
|
Button custom = new Button(TimiFXUI.MULTILINGUAL.text("custom"));
|
||||||
|
custom.getStyleClass().add(CSS.BORDER_TRB);
|
||||||
|
|
||||||
|
// 保持显示
|
||||||
|
Button keepShow = new Button(TimiFXUI.MULTILINGUAL.text("fx.example.popup_tips.keep_show"));
|
||||||
|
keepShow.getStyleClass().add(CSS.BORDER_TRB);
|
||||||
|
{
|
||||||
|
ListView<String> list = new ListView<>();
|
||||||
|
for (int i = 0; i < 20; i++) {
|
||||||
|
list.getItems().add("item " + i);
|
||||||
|
}
|
||||||
|
BorderPane customPane = new BorderPane();
|
||||||
|
BorderPane.setMargin(list, new Insets(12, 0, 0, 0));
|
||||||
|
customPane.setTop(new Button(TimiFXUI.MULTILINGUAL.text("custom")));
|
||||||
|
customPane.setCenter(list);
|
||||||
|
customPane.setPadding(new Insets(20));
|
||||||
|
customPane.setPrefHeight(320);
|
||||||
|
customPane.setBackground(BG.DEFAULT);
|
||||||
|
AbstractPopupTips<BorderPane> customTips = new AbstractPopupTips<>(customPane);
|
||||||
|
PopupTipsService.installTips(custom, customTips);
|
||||||
|
PopupTipsService.installTips(keepShow, customTips);
|
||||||
|
|
||||||
|
keepShow.setOnAction(e -> customTips.setKeepShow(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
setCenter(new VBox() {{
|
||||||
|
setPadding(new Insets(20));
|
||||||
|
getChildren().add(new HBox(text, textLong, image, custom, keepShow));
|
||||||
|
}});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,77 @@
|
|||||||
|
package com.imyeyu.fx.ui.examples.view.pages;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.task.RunAsyncScheduled;
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import com.imyeyu.fx.ui.components.TextAreaEditor;
|
||||||
|
import com.imyeyu.fx.ui.components.TextFlower;
|
||||||
|
import com.imyeyu.fx.ui.examples.component.AbstractDemoPane;
|
||||||
|
import com.imyeyu.fx.ui.examples.component.sidebar.SidebarItem;
|
||||||
|
import com.imyeyu.inject.annotation.Component;
|
||||||
|
import com.imyeyu.utils.Time;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.layout.BorderPane;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
import javafx.util.Duration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步线程
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-09-01 14:37
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class RunAsyncDemo extends AbstractDemoPane {
|
||||||
|
|
||||||
|
public RunAsyncDemo() {
|
||||||
|
title.setText(SidebarItem.RUN_ASYNC.getText());
|
||||||
|
document.sync("https://doc.imyeyu.net/timi-fx/net/imyeyu/timifx/service/RunAsync.html");
|
||||||
|
source.sync("https://git.imyeyu.net/Timi/timi-fx/src/master/src/main/java/net/imyeyu/timifx/service/RunAsync.java");
|
||||||
|
|
||||||
|
Label title = new Label(TimiFXUI.MULTILINGUAL.text("important"));
|
||||||
|
title.setTextFill(Colorful.RED);
|
||||||
|
|
||||||
|
// 提示 0
|
||||||
|
TextFlower tips0 = new TextFlower().textStart();
|
||||||
|
tips0.matcher(TimiFXUI.MULTILINGUAL.text("fx.example.run_async.tips0"));
|
||||||
|
|
||||||
|
// 提示 1
|
||||||
|
TextFlower tips1 = new TextFlower().textStart();
|
||||||
|
tips1.matcher(TimiFXUI.MULTILINGUAL.text("fx.example.run_async.tips1"));
|
||||||
|
|
||||||
|
// 时间示例
|
||||||
|
Label time = new Label();
|
||||||
|
RunAsyncScheduled.call(Duration.seconds(1), Time::now, t -> time.setText(Time.toDateTime(t)));
|
||||||
|
|
||||||
|
// 扩展说明
|
||||||
|
TextFlower description = new TextFlower();
|
||||||
|
description.matcher(TimiFXUI.MULTILINGUAL.text("fx.example.run_async.description"));
|
||||||
|
|
||||||
|
// 代码示例
|
||||||
|
TextAreaEditor code = new TextAreaEditor();
|
||||||
|
code.setEditable(false);
|
||||||
|
code.setText("""
|
||||||
|
// 每秒执行一次并返回,参数 3 (FX 线程)的入参是参数 2 (非 FX 线程)的返回
|
||||||
|
// RunAsyncScheduled.call(Duration.seconds(1), () -> "result", result -> {});
|
||||||
|
|
||||||
|
RunAsyncScheduled.call(Duration.seconds(1), () -> {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
return now;
|
||||||
|
}, millis -> {
|
||||||
|
String datetime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(millis);
|
||||||
|
label.setText(datetime);
|
||||||
|
});
|
||||||
|
""".trim());
|
||||||
|
|
||||||
|
setCenter(new BorderPane() {{
|
||||||
|
setMargin(code, new Insets(4, 0, 0, 0));
|
||||||
|
setPadding(new Insets(20));
|
||||||
|
setTop(new VBox() {{
|
||||||
|
setMargin(time, new Insets(20, 0, 0, 0));
|
||||||
|
setSpacing(4);
|
||||||
|
getChildren().addAll(title, tips0, tips1, time, description);
|
||||||
|
}});
|
||||||
|
setCenter(code);
|
||||||
|
}});
|
||||||
|
}
|
||||||
|
}
|
||||||
188
src/test/java/com/imyeyu/fx/ui/examples/view/pages/Style.java
Normal file
188
src/test/java/com/imyeyu/fx/ui/examples/view/pages/Style.java
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
package com.imyeyu.fx.ui.examples.view.pages;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import com.imyeyu.fx.ui.components.TitleLabel;
|
||||||
|
import com.imyeyu.fx.ui.components.XHyperlink;
|
||||||
|
import com.imyeyu.fx.ui.examples.component.AbstractPane;
|
||||||
|
import com.imyeyu.fx.utils.BgFill;
|
||||||
|
import com.imyeyu.fx.utils.Column;
|
||||||
|
import com.imyeyu.inject.annotation.Component;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.effect.DropShadow;
|
||||||
|
import javafx.scene.layout.Background;
|
||||||
|
import javafx.scene.layout.BorderPane;
|
||||||
|
import javafx.scene.layout.GridPane;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
import javafx.scene.paint.Paint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主题样式
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-08-26 15:44
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class Style extends AbstractPane {
|
||||||
|
|
||||||
|
public Style() {
|
||||||
|
TitleLabel titleTimiFX = new TitleLabel("TimiFX");
|
||||||
|
Label labelTimiFXDocument = TimiFXUI.label(TimiFXUI.MULTILINGUAL.text("document"));
|
||||||
|
XHyperlink timiFXDocument = new XHyperlink("https://doc.imyeyu.net/timi-fx/net/imyeyu/timifx/TimiFX.html");
|
||||||
|
Label labelTimiFXSource = TimiFXUI.label(TimiFXUI.MULTILINGUAL.text("source"));
|
||||||
|
XHyperlink timiFXSource = new XHyperlink("https://git.imyeyu.net/Timi/timi-fx/src/master/src/main/java/net/imyeyu/timifx/TimiFX.java");
|
||||||
|
Label labelTimiFXDescription = TimiFXUI.label(TimiFXUI.MULTILINGUAL.text("description"));
|
||||||
|
Label timiFXDescription = new Label(TimiFXUI.MULTILINGUAL.text("fx.example.style.timifx"));
|
||||||
|
|
||||||
|
// 标题颜色
|
||||||
|
TitleLabel titleColor = new TitleLabel(TimiFXUI.MULTILINGUAL.text("color"));
|
||||||
|
|
||||||
|
// 聚焦颜色
|
||||||
|
Label labelFocusColor = TimiFXUI.label(TimiFXUI.MULTILINGUAL.text("focus"));
|
||||||
|
Label colorFocus = colorLabel(TimiFXUI.MULTILINGUAL.text("default"), Colorful.FOCUSED_DEFAULT);
|
||||||
|
colorFocus.setTextFill(Colorful.WHITE);
|
||||||
|
Label colorFocusLight = colorLabel(TimiFXUI.MULTILINGUAL.text("light"), Colorful.FOCUSED_LIGHT);
|
||||||
|
Label colorFocusDark = colorLabel(TimiFXUI.MULTILINGUAL.text("dark"), Colorful.FOCUSED_DARK);
|
||||||
|
colorFocusDark.setTextFill(Colorful.WHITE);
|
||||||
|
|
||||||
|
// 图标颜色
|
||||||
|
Label labelIconColor = TimiFXUI.label(TimiFXUI.MULTILINGUAL.text("icon"));
|
||||||
|
Label colorIcon = colorLabel(TimiFXUI.MULTILINGUAL.text("default"), Colorful.ICON);
|
||||||
|
colorIcon.setTextFill(Colorful.WHITE);
|
||||||
|
Label colorIconHover = colorLabel(TimiFXUI.MULTILINGUAL.text("hover"), Colorful.ICON_HOVER);
|
||||||
|
|
||||||
|
// 边框颜色
|
||||||
|
Label labelBorderColor = TimiFXUI.label(TimiFXUI.MULTILINGUAL.text("border"));
|
||||||
|
Label colorBorder = colorLabel(TimiFXUI.MULTILINGUAL.text("default"), Colorful.BORDER);
|
||||||
|
Label colorBorderDisable = colorLabel(TimiFXUI.MULTILINGUAL.text("disable"), Paint.valueOf("#E1E1E1"));
|
||||||
|
|
||||||
|
// 背景颜色
|
||||||
|
Label labelBackgroundColor = TimiFXUI.label(TimiFXUI.MULTILINGUAL.text("background"));
|
||||||
|
Label backgroundColor = colorLabel(TimiFXUI.MULTILINGUAL.text("default"), BG.DEFAULT);
|
||||||
|
backgroundColor.setBorder(Stroke.DEFAULT);
|
||||||
|
Label backgroundColorTitle = colorLabel(TimiFXUI.MULTILINGUAL.text("title"), BG.TITLE);
|
||||||
|
backgroundColorTitle.setPrefWidth(100);
|
||||||
|
Label backgroundColorTitleFill = colorLabel(TimiFXUI.MULTILINGUAL.text("fx.example.style.color.bg.title_fill"), BG.TITLE_FILL);
|
||||||
|
backgroundColorTitleFill.setPrefWidth(100);
|
||||||
|
|
||||||
|
// 投影
|
||||||
|
TitleLabel titleShadow = new TitleLabel(TimiFXUI.MULTILINGUAL.text("shadow"));
|
||||||
|
Label shadowPopup = shadowLabel(TimiFXUI.MULTILINGUAL.text("fx.example.style.shadow.popup"), Shadow.POPUP);
|
||||||
|
Label shadowImage = shadowLabel(TimiFXUI.MULTILINGUAL.text("image"), Shadow.IMAGE);
|
||||||
|
Label shadowDown = shadowLabel(TimiFXUI.MULTILINGUAL.text("fx.example.style.shadow.down"), Shadow.DOWN);
|
||||||
|
|
||||||
|
// 样式文件
|
||||||
|
TitleLabel styleFile = new TitleLabel(TimiFXUI.MULTILINGUAL.text("fx.example.style.file"));
|
||||||
|
Label labelFile = TimiFXUI.label(TimiFXUI.MULTILINGUAL.text("source"));
|
||||||
|
XHyperlink file = new XHyperlink("https://git.imyeyu.net/Timi/timi-fx/src/master/src/main/resources/timifx/style.css");
|
||||||
|
|
||||||
|
setCenter(new VBox() {{
|
||||||
|
setSpacing(16);
|
||||||
|
setPadding(new Insets(20));
|
||||||
|
|
||||||
|
final Insets CONTENT_PADDING = new Insets(10, 20, 4, 20);
|
||||||
|
getChildren().addAll(new BorderPane() {{
|
||||||
|
setTop(titleTimiFX);
|
||||||
|
setCenter(new GridPane() {{
|
||||||
|
getColumnConstraints().addAll(Column.KEY, Column.VALUE_FILL);
|
||||||
|
setVgap(8);
|
||||||
|
setHgap(10);
|
||||||
|
setPadding(CONTENT_PADDING);
|
||||||
|
|
||||||
|
int row = 0;
|
||||||
|
addRow(row++, labelTimiFXDocument, timiFXDocument);
|
||||||
|
addRow(row++, labelTimiFXSource, timiFXSource);
|
||||||
|
addRow(row, labelTimiFXDescription, timiFXDescription);
|
||||||
|
}});
|
||||||
|
}}, new BorderPane() {{
|
||||||
|
setTop(titleColor);
|
||||||
|
setCenter(new GridPane() {{
|
||||||
|
getColumnConstraints().addAll(Column.KEY, Column.VALUE_FILL);
|
||||||
|
setVgap(8);
|
||||||
|
setHgap(10);
|
||||||
|
setPadding(CONTENT_PADDING);
|
||||||
|
|
||||||
|
int row = 0;
|
||||||
|
addRow(row++, labelFocusColor, new HBox() {{
|
||||||
|
setSpacing(8);
|
||||||
|
getChildren().addAll(colorFocus, colorFocusLight, colorFocusDark);
|
||||||
|
}});
|
||||||
|
addRow(row++, labelIconColor, new HBox() {{
|
||||||
|
setSpacing(8);
|
||||||
|
getChildren().addAll(colorIcon, colorIconHover);
|
||||||
|
}});
|
||||||
|
addRow(row++, labelBorderColor, new HBox() {{
|
||||||
|
setSpacing(8);
|
||||||
|
getChildren().addAll(colorBorder, colorBorderDisable);
|
||||||
|
}});
|
||||||
|
addRow(row, labelBackgroundColor, new HBox() {{
|
||||||
|
setSpacing(8);
|
||||||
|
getChildren().addAll(backgroundColor, backgroundColorTitle, backgroundColorTitleFill);
|
||||||
|
}});
|
||||||
|
}});
|
||||||
|
}}, new BorderPane() {{
|
||||||
|
setTop(titleShadow);
|
||||||
|
setCenter(new HBox() {{
|
||||||
|
BorderPane.setMargin(this, CONTENT_PADDING);
|
||||||
|
setPadding(new Insets(20));
|
||||||
|
setSpacing(24);
|
||||||
|
setBorder(Stroke.DEFAULT);
|
||||||
|
setBackground(BG.WHITE);
|
||||||
|
getChildren().addAll(shadowPopup, shadowImage, shadowDown);
|
||||||
|
}});
|
||||||
|
}}, new BorderPane() {{
|
||||||
|
setTop(styleFile);
|
||||||
|
setCenter(new GridPane() {{
|
||||||
|
getColumnConstraints().addAll(Column.KEY, Column.VALUE_FILL);
|
||||||
|
setVgap(8);
|
||||||
|
setHgap(10);
|
||||||
|
setPadding(CONTENT_PADDING);
|
||||||
|
addRow(0, labelFile, file);
|
||||||
|
}});
|
||||||
|
}});
|
||||||
|
}});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造背景颜色标签
|
||||||
|
*
|
||||||
|
* @param text 文本
|
||||||
|
* @param color 颜色
|
||||||
|
* @return 标签
|
||||||
|
*/
|
||||||
|
private Label colorLabel(String text, Paint color) {
|
||||||
|
return colorLabel(text, new BgFill(color).build());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造背景颜色标签
|
||||||
|
*
|
||||||
|
* @param text 文本
|
||||||
|
* @param color 背景颜色
|
||||||
|
* @return 标签
|
||||||
|
*/
|
||||||
|
private Label colorLabel(String text, Background color) {
|
||||||
|
Label label = new Label(text);
|
||||||
|
label.setBorder(Stroke.TP);
|
||||||
|
label.setPadding(new Insets(4, 16, 4, 16));
|
||||||
|
label.setBackground(color);
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造投影标签
|
||||||
|
*
|
||||||
|
* @param text 文本
|
||||||
|
* @param shadow 投影
|
||||||
|
* @return 标签
|
||||||
|
*/
|
||||||
|
private Label shadowLabel(String text, DropShadow shadow) {
|
||||||
|
Label label = new Label(text);
|
||||||
|
label.setEffect(shadow);
|
||||||
|
label.setBorder(Stroke.DEFAULT);
|
||||||
|
label.setPadding(new Insets(4, 16, 4, 16));
|
||||||
|
label.setBackground(BG.DEFAULT);
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
}
|
||||||
174
src/test/java/com/imyeyu/fx/ui/examples/view/pages/Welcome.java
Normal file
174
src/test/java/com/imyeyu/fx/ui/examples/view/pages/Welcome.java
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
package com.imyeyu.fx.ui.examples.view.pages;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.TimiFX;
|
||||||
|
import com.imyeyu.fx.ui.MinecraftFont;
|
||||||
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
|
import com.imyeyu.fx.ui.components.TextFlower;
|
||||||
|
import com.imyeyu.fx.ui.components.alert.AlertConfirm;
|
||||||
|
import com.imyeyu.fx.ui.components.alert.AlertTips;
|
||||||
|
import com.imyeyu.fx.ui.examples.TimiFXExamples;
|
||||||
|
import com.imyeyu.fx.ui.examples.bean.Config;
|
||||||
|
import com.imyeyu.fx.ui.examples.component.AbstractPane;
|
||||||
|
import com.imyeyu.fx.ui.examples.component.TimiVersionLabel;
|
||||||
|
import com.imyeyu.fx.ui.examples.ctrl.Main;
|
||||||
|
import com.imyeyu.fx.ui.examples.util.Resources;
|
||||||
|
import com.imyeyu.fx.utils.Column;
|
||||||
|
import com.imyeyu.inject.TimiInject;
|
||||||
|
import com.imyeyu.inject.annotation.Component;
|
||||||
|
import com.imyeyu.inject.annotation.Inject;
|
||||||
|
import com.imyeyu.inject.annotation.InvokeForInjected;
|
||||||
|
import com.imyeyu.io.IO;
|
||||||
|
import com.imyeyu.java.bean.Language;
|
||||||
|
import com.imyeyu.utils.OS;
|
||||||
|
import com.imyeyu.utils.Time;
|
||||||
|
import javafx.geometry.HPos;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.control.ComboBox;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
|
import javafx.scene.layout.GridPane;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
import javafx.scene.text.TextAlignment;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import javafx.util.StringConverter;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 欢迎页
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2022-08-26 14:55
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class Welcome extends AbstractPane implements OS.FileSystem {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private Config config;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private Stage stage;
|
||||||
|
|
||||||
|
private final ComboBox<Language> language;
|
||||||
|
|
||||||
|
public Welcome() {
|
||||||
|
Label title = new Label(TimiFXUI.MULTILINGUAL.text("fx.example.title"), new ImageView(Resources.ICON_X64));
|
||||||
|
title.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
title.setAlignment(Pos.CENTER);
|
||||||
|
title.setBackground(BG.TITLE_FILL);
|
||||||
|
title.setBorder(Stroke.BOTTOM);
|
||||||
|
title.setPadding(new Insets(8, 0, 8, 0));
|
||||||
|
MinecraftFont.css(title, MinecraftFont.X32);
|
||||||
|
|
||||||
|
// 文档和源码
|
||||||
|
TextFlower docSource = new TextFlower();
|
||||||
|
docSource.matcher(TimiFXUI.MULTILINGUAL.text("fx.example.welcome.top"));
|
||||||
|
docSource.setTextAlignment(TextAlignment.CENTER);
|
||||||
|
|
||||||
|
// 提示
|
||||||
|
Label tips = new Label(TimiFXUI.MULTILINGUAL.text("fx.example.welcome.tips"));
|
||||||
|
tips.setAlignment(Pos.CENTER);
|
||||||
|
|
||||||
|
// 语言
|
||||||
|
Label labelLang = new Label(TimiFXUI.MULTILINGUAL.text("lang"));
|
||||||
|
labelLang.setTextFill(Colorful.GRAY);
|
||||||
|
|
||||||
|
language = new ComboBox<>();
|
||||||
|
language.getItems().addAll(Language.values());
|
||||||
|
language.setConverter(new StringConverter<>() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString(Language language) {
|
||||||
|
return language.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Language fromString(String string) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 版权
|
||||||
|
TextFlower license = new TextFlower().matcher(TimiFXUI.MULTILINGUAL.text("fx.example.welcome.license"));
|
||||||
|
license.setTextAlignment(TextAlignment.CENTER);
|
||||||
|
|
||||||
|
// 开发者
|
||||||
|
Label develop = new Label(TimiFXUI.MULTILINGUAL.textArgs("developer.arg", "夜雨"));
|
||||||
|
develop.setAlignment(Pos.CENTER);
|
||||||
|
TextFlower blog = new TextFlower().matcher(TimiFXUI.MULTILINGUAL.text("blog"));
|
||||||
|
blog.setTextAlignment(TextAlignment.CENTER);
|
||||||
|
Label copyright = new Label(TimiFXUI.MULTILINGUAL.textArgs("copyright", "夜雨", Time.yearFull.format(new Date())));
|
||||||
|
copyright.setAlignment(Pos.CENTER);
|
||||||
|
Label versionTimiFX = new Label(TimiFXUI.MULTILINGUAL.textArgs("fx.example.welcome.version.timifx", "0.0.1"));
|
||||||
|
TimiVersionLabel version = new TimiVersionLabel(TimiFXExamples.VERSION, TimiFXExamples.class.getSimpleName()) {{
|
||||||
|
version.setGraphic(new Label(TimiFXUI.MULTILINGUAL.text("fx.example.welcome.version")));
|
||||||
|
}};
|
||||||
|
|
||||||
|
setTop(title);
|
||||||
|
setCenter(new VBox() {{
|
||||||
|
setSpacing(20);
|
||||||
|
setPadding(new Insets(20));
|
||||||
|
setAlignment(Pos.TOP_CENTER);
|
||||||
|
getChildren().addAll(docSource, tips, new VBox() {{
|
||||||
|
setPadding(new Insets(40));
|
||||||
|
getChildren().add(new Label(TimiFXUI.MULTILINGUAL.text("fx.example.welcome.tips0")));
|
||||||
|
getChildren().add(new Label(TimiFXUI.MULTILINGUAL.text("fx.example.welcome.tips1")));
|
||||||
|
getChildren().add(new Label(TimiFXUI.MULTILINGUAL.text("fx.example.welcome.tips2")));
|
||||||
|
getChildren().add(new TextFlower() {{
|
||||||
|
setPadding(new Insets(40, 0, 0, 0));
|
||||||
|
matcher(TimiFXUI.MULTILINGUAL.text("fx.example.welcome.tips3"));
|
||||||
|
}});
|
||||||
|
getChildren().add(new GridPane() {{
|
||||||
|
getColumnConstraints().addAll(Column.build(HPos.RIGHT), Column.VALUE);
|
||||||
|
setHgap(8);
|
||||||
|
setVgap(6);
|
||||||
|
setPadding(new Insets(12, 0, 4, 0));
|
||||||
|
|
||||||
|
int row = 0;
|
||||||
|
addRow(row++, new Label("timi-java"), TimiFXUI.label(TimiFXUI.MULTILINGUAL.text("fx.example.welcome.timijava")));
|
||||||
|
addRow(row, new Label("timi-fx-icon"), TimiFXUI.label(TimiFXUI.MULTILINGUAL.text("fx.example.welcome.timifx_icon")));
|
||||||
|
}});
|
||||||
|
}});
|
||||||
|
}});
|
||||||
|
setBottom(new VBox() {{
|
||||||
|
setSpacing(6);
|
||||||
|
setPadding(new Insets(16));
|
||||||
|
setAlignment(Pos.CENTER);
|
||||||
|
getChildren().addAll(new HBox() {{
|
||||||
|
setSpacing(6);
|
||||||
|
setAlignment(Pos.CENTER);
|
||||||
|
getChildren().addAll(TimiFXUI.label(TimiFXUI.MULTILINGUAL.text("lang")), language);
|
||||||
|
}});
|
||||||
|
getChildren().addAll(versionTimiFX, version, develop, license, blog, copyright);
|
||||||
|
}});
|
||||||
|
}
|
||||||
|
|
||||||
|
@InvokeForInjected
|
||||||
|
private void injected() {
|
||||||
|
language.valueProperty().bind(config.getLanguage());
|
||||||
|
|
||||||
|
// 修改语言
|
||||||
|
language.valueProperty().addListener((obs, o, newLang) -> {
|
||||||
|
new AlertConfirm(TimiFXUI.MULTILINGUAL.text("tips.restart")) {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onConfirm() {
|
||||||
|
// JRE
|
||||||
|
String jre = System.getProperty("java.home") + SEP + "bin" + SEP + "java";
|
||||||
|
// 启动参数
|
||||||
|
String param = " -XX:+UnlockExperimentalVMOptions -XX:+UseZGC -jar ";
|
||||||
|
// 启动 Jar
|
||||||
|
String jar = IO.getJarAbsolutePath(getClass());
|
||||||
|
// 重启
|
||||||
|
try {
|
||||||
|
TimiFX.doRestart(TimiFXExamples.getInjectApp().injector().di(Main.class), jre + param + jar);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
AlertTips.error(this, TimiFXUI.MULTILINGUAL.text("tips.restart.error"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.showRelativeCenter(getScene().getWindow());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user