Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b44404c469 | ||
|
|
0d53d521bf | ||
|
|
a9c14bfa0b | ||
|
|
6ef823b5ba | ||
|
|
d577e1fe30 | ||
|
|
db1ef60add |
No files matched your search
@@ -1,11 +0,0 @@
|
|||||||
blank_issues_enabled: false
|
|
||||||
contact_links:
|
|
||||||
- name: QQ 群
|
|
||||||
url: https://docs.hmcl.net/groups.html
|
|
||||||
about: Hello Minecraft! Launcher 的官方 QQ 交流群。
|
|
||||||
- name: Discord 服务器
|
|
||||||
url: https://discord.gg/jVvC7HfM6U
|
|
||||||
about: Hello Minecraft! Launcher 的官方 Discord 服务器。
|
|
||||||
- name: GitHub Issue
|
|
||||||
url: https://github.com/HMCL-dev/HMCL/issues/new/choose
|
|
||||||
about: 本仓库为镜像仓库,Bug 报告与功能请求请在 GitHub 上提交。
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
# Default: normalize all text files to LF in the repository and on checkout.
|
|
||||||
# Git auto-detects binary files and leaves them untouched.
|
|
||||||
* text=auto eol=lf
|
|
||||||
|
|
||||||
# ─── Explicitly text – source & build ────────────────────────────────────────
|
|
||||||
*.java text eol=lf
|
|
||||||
*.js text eol=lf
|
|
||||||
*.kt text eol=lf
|
|
||||||
*.kts text eol=lf
|
|
||||||
*.groovy text eol=lf
|
|
||||||
*.gradle text eol=lf
|
|
||||||
|
|
||||||
# ─── Configuration & data ─────────────────────────────────────────────────────
|
|
||||||
*.properties text eol=lf
|
|
||||||
*.toml text eol=lf
|
|
||||||
*.xml text eol=lf
|
|
||||||
*.json text eol=lf
|
|
||||||
*.yaml text eol=lf
|
|
||||||
*.yml text eol=lf
|
|
||||||
*.csv text eol=lf
|
|
||||||
*.html text eol=lf
|
|
||||||
*.css text eol=lf
|
|
||||||
*.txt text eol=lf
|
|
||||||
*.md text eol=lf
|
|
||||||
*.svg text eol=lf
|
|
||||||
*.MF text eol=lf
|
|
||||||
|
|
||||||
# Gradle dependency-lock files
|
|
||||||
*.lockfile text eol=lf
|
|
||||||
|
|
||||||
# ─── Scripts ──────────────────────────────────────────────────────────────────
|
|
||||||
# Unix shell scripts must always use LF so they stay executable on Linux/macOS.
|
|
||||||
*.sh text eol=lf
|
|
||||||
gradlew text eol=lf
|
|
||||||
|
|
||||||
# CI pipeline scripts
|
|
||||||
Jenkinsfile text eol=lf
|
|
||||||
|
|
||||||
# Windows batch files must keep CRLF so cmd.exe can parse them.
|
|
||||||
*.bat text eol=crlf
|
|
||||||
|
|
||||||
# ─── Binary files ─────────────────────────────────────────────────────────────
|
|
||||||
# Raster / animated images
|
|
||||||
*.png binary
|
|
||||||
*.apng binary
|
|
||||||
*.jpg binary
|
|
||||||
*.jpeg binary
|
|
||||||
*.webp binary
|
|
||||||
*.gif binary
|
|
||||||
*.ico binary
|
|
||||||
|
|
||||||
# Archives & compiled artifacts
|
|
||||||
*.jar binary
|
|
||||||
*.zip binary
|
|
||||||
*.class binary
|
|
||||||
|
|
||||||
# Cryptographic / certificate files
|
|
||||||
*.der binary
|
|
||||||
|
|
||||||
# Minecraft schematic
|
|
||||||
*.litematic binary
|
|
||||||
*.nbt binary
|
|
||||||
*.schem binary
|
|
||||||
*.schematic binary
|
|
||||||
|
|
||||||
# Compiled / intermediate build artefacts
|
|
||||||
*.bin binary
|
|
||||||
*.probe binary
|
|
||||||
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
blank_issues_enabled: false
|
|
||||||
contact_links:
|
|
||||||
- name: QQ 群
|
|
||||||
url: https://docs.hmcl.net/groups.html
|
|
||||||
about: Hello Minecraft! Launcher 的官方 QQ 交流群。
|
|
||||||
- name: GitHub Issue
|
|
||||||
url: https://github.com/HMCL-dev/HMCL/issues/new/choose
|
|
||||||
about: 本仓库为镜像仓库,Bug 报告与功能请求请在 GitHub 上提交。
|
|
||||||
@@ -9,15 +9,15 @@ body:
|
|||||||
value: |
|
value: |
|
||||||
提交前请确认:
|
提交前请确认:
|
||||||
|
|
||||||
* 该问题确实是 **HMCL 的错误**,而**不是 Minecraft 非正常退出**。如果你的 Minecraft 非正常退出,请前往 [QQ 群](https://docs.hmcl.net/groups.html)/[Discord 服务器](https://discord.gg/jVvC7HfM6U) 获取帮助。
|
* 该问题确实是 **HMCL 的错误**,而**不是 Minecraft 非正常退出**,如果你的 Minecraft 非正常退出,请前往 [QQ 群](https://docs.hmcl.net/groups.html)/[Discord 服务器](https://discord.gg/jVvC7HfM6U) 获取帮助。
|
||||||
* 你的启动器版本是**最新的预览版本**。你可以从 [GitHub Actions](https://github.com/HMCL-dev/HMCL/actions/workflows/gradle.yml?query=branch%3Amain+event%3Apush) 或 [nightly.link](https://nightly.link/HMCL-dev/HMCL/workflows/gradle/main) 下载最新预览版本。
|
* 你的启动器版本是**最新的预览版本**,可以点击 [此处](https://zkitefly.github.io/HMCL-Snapshot-Update/) 下载最新预览版本。
|
||||||
|
|
||||||
如果你的问题并不属于上述两类,你可以选取另一种 Issue 类型,或者直接前往 [QQ 群](https://docs.hmcl.net/groups.html)/[Discord 服务器](https://discord.gg/jVvC7HfM6U) 获取帮助。
|
如果你的问题并不属于上述两类,你可以选取另一种 Issue 类型,或者直接前往 [QQ 群](https://docs.hmcl.net/groups.html)/[Discord 服务器](https://discord.gg/jVvC7HfM6U) 获取帮助。
|
||||||
|
|
||||||
Before submitting, please confirm:
|
Before submitting, please confirm:
|
||||||
|
|
||||||
* The issue is indeed **a bug of HMCL**, not **Minecraft abnormal exit**. If your Minecraft exits abnormally, please go to the [QQ group](https://docs.hmcl.net/groups.html) or [Discord server](https://discord.gg/jVvC7HfM6U) for help.
|
* The issue is indeed **a bug of HMCL**, not **Minecraft abnormal exit**. If your Minecraft exits abnormally, please go to the [QQ group](https://docs.hmcl.net/groups.html) or [Discord server](https://discord.gg/jVvC7HfM6U) for help.
|
||||||
* Your launcher is the **latest nightly build**. You can download the latest nightly build from [GitHub Actions](https://github.com/HMCL-dev/HMCL/actions/workflows/gradle.yml?query=branch%3Amain+event%3Apush) or [nightly.link](https://nightly.link/HMCL-dev/HMCL/workflows/gradle/main).
|
* Your launcher is the **latest nightly build**. You can click [here](https://zkitefly.github.io/HMCL-Snapshot-Update/en) to download the latest nightly build.
|
||||||
|
|
||||||
If your issue does not fall into the above two categories, you can choose another type of issue or directly go to the [QQ group](https://docs.hmcl.net/groups.html) or [Discord server](https://discord.gg/jVvC7HfM6U) for help.
|
If your issue does not fall into the above two categories, you can choose another type of issue or directly go to the [QQ group](https://docs.hmcl.net/groups.html) or [Discord server](https://discord.gg/jVvC7HfM6U) for help.
|
||||||
- type: textarea
|
- type: textarea
|
||||||
@@ -38,7 +38,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: 启动器崩溃报告 / 启动器日志文件 | Launcher Crash Report / Launcher Log File
|
label: 启动器崩溃报告 / 启动器日志文件 | Launcher Crash Report / Launcher Log File
|
||||||
description: |
|
description: |
|
||||||
如果你的启动器崩溃了,请将崩溃报告填入 (或将文件拖入) 下方。
|
如果你的启动器崩溃了,请将崩溃报告填入(或将文件拖入)下方。
|
||||||
如果你的启动器没有崩溃,请在遇到问题后**不要退出启动器**,在启动器的 “设置 → 通用 → 调试” 一栏中点击 “导出启动器日志”,并将导出的日志拖到下方的输入栏中。
|
如果你的启动器没有崩溃,请在遇到问题后**不要退出启动器**,在启动器的 “设置 → 通用 → 调试” 一栏中点击 “导出启动器日志”,并将导出的日志拖到下方的输入栏中。
|
||||||
**请注意:启动器崩溃报告或日志文件是诊断问题的重要依据,请务必上传!**
|
**请注意:启动器崩溃报告或日志文件是诊断问题的重要依据,请务必上传!**
|
||||||
|
|
||||||
|
|||||||
@@ -1,120 +0,0 @@
|
|||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
module.exports = async ({ github, context, core }) => {
|
|
||||||
const prNumberPath = process.env.PULL_REQUEST_NUMBER_PATH;
|
|
||||||
if (!prNumberPath || !fs.existsSync(prNumberPath) || !fs.statSync(prNumberPath).isFile()) {
|
|
||||||
core.setFailed("Pull request number file not found.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const reportsPath = process.env.REPORTS_PATH;
|
|
||||||
if (!reportsPath || !fs.existsSync(reportsPath) || !fs.statSync(reportsPath).isFile()) {
|
|
||||||
core.setFailed("Reports file not found.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const PULL_REQUEST_NUMBER = parseInt(fs.readFileSync(prNumberPath, "utf8").trim(), 10);
|
|
||||||
if (isNaN(PULL_REQUEST_NUMBER)) {
|
|
||||||
core.setFailed("Failed to parse the pull request number.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { owner, repo } = context.repo;
|
|
||||||
|
|
||||||
let pr;
|
|
||||||
try {
|
|
||||||
const response = await github.rest.pulls.get({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
pull_number: PULL_REQUEST_NUMBER,
|
|
||||||
});
|
|
||||||
pr = response.data;
|
|
||||||
} catch (error) {
|
|
||||||
core.setFailed(`Failed to fetch PR #${PULL_REQUEST_NUMBER}: ${error.message}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
core.info(`Current PR state: ${pr.state}`);
|
|
||||||
if (pr.state !== "open") {
|
|
||||||
core.setFailed("The pull request is not open. Skipping comment creation.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const run = context.payload.workflow_run;
|
|
||||||
if (!run) {
|
|
||||||
core.setFailed("context.payload.workflow_run is undefined. Ensure this script runs on the 'workflow_run' event.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pr.head.sha !== run.head_sha) {
|
|
||||||
core.setFailed("PR head SHA does not match the workflow run head SHA. Skipping.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pr.head.ref !== run.head_branch) {
|
|
||||||
core.setFailed("PR head branch does not match the workflow run head branch. Skipping.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pr.head.repo.full_name !== run.head_repository.full_name) {
|
|
||||||
core.setFailed("PR head repository fullname does not match the workflow run head repository fullname. Skipping.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const comments = [];
|
|
||||||
const maxCommentCount = 10;
|
|
||||||
const reports = fs.readFileSync(reportsPath, "utf8").trim().split("\n").filter(Boolean);
|
|
||||||
for (const report of reports) {
|
|
||||||
if (comments.length >= maxCommentCount) break;
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(report);
|
|
||||||
if (typeof data !== "object" || Array.isArray(data)) continue;
|
|
||||||
|
|
||||||
const { location, message, severity, code } = data;
|
|
||||||
if (typeof location !== "object" || Array.isArray(location)) continue;
|
|
||||||
if (typeof message !== "string") continue;
|
|
||||||
const severities = ["UNKNOWN_SEVERITY", "ERROR", "WARNING", "INFO", 0, 1, 2, 3];
|
|
||||||
if (!severities.includes(severity)) continue;
|
|
||||||
const severityText = typeof severity === "string" ? severity : severities[severity];
|
|
||||||
if (typeof code !== "object" || Array.isArray(code)) continue;
|
|
||||||
|
|
||||||
const path = location.path;
|
|
||||||
const line = location.range?.start?.line;
|
|
||||||
const codeText = code.value ? `[${code.value}] ` : "";
|
|
||||||
|
|
||||||
if (path && line) {
|
|
||||||
comments.push({
|
|
||||||
path,
|
|
||||||
line,
|
|
||||||
side: "RIGHT",
|
|
||||||
body: `**[${severityText}]** ${codeText}\n\n${message}`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
core.warning(`Failed to parse report: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (comments.length === 0) {
|
|
||||||
core.info("No diagnostics found to report as comments.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
core.info(`Successfully prepared ${comments.length} comment${comments.length > 1 ? "s" : ""} to post.`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await github.rest.pulls.createReview({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
pull_number: PULL_REQUEST_NUMBER,
|
|
||||||
commit_id: run.head_sha,
|
|
||||||
body: "🤖 Static analysis found the following issues (max count 10):",
|
|
||||||
event: "COMMENT",
|
|
||||||
comments: comments
|
|
||||||
});
|
|
||||||
core.info("Review comments successfully posted to the pull request.");
|
|
||||||
} catch (error) {
|
|
||||||
core.setFailed(`Failed to post review comments to GitHub: ${error.message}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
module.exports = async ({ github, context, core }) => {
|
|
||||||
const owner = context.repo.owner;
|
|
||||||
const repo = context.repo.repo;
|
|
||||||
const ignoredFilePatterns = [
|
|
||||||
/(?:^|\/)\.cnb\/.+\.ya?ml$/i,
|
|
||||||
/(?:^|\/)\.gitee\/.+\.ya?ml$/i,
|
|
||||||
/(?:^|\/)\.github\/.+\.ya?ml$/i,
|
|
||||||
/(?:^|\/)\.idea\/.+\.xml$/i,
|
|
||||||
/(?:^|\/)\.editorconfig$/i,
|
|
||||||
/(?:^|\/)\.gitattributes$/i,
|
|
||||||
/(?:^|\/)\.gitignore$/i,
|
|
||||||
/(?:^|\/).+\.kts$/i,
|
|
||||||
/(?:^|\/).+\.md$/i,
|
|
||||||
/(?:^|\/)LICENSE(?:_.*)?$/i,
|
|
||||||
/(?:^|\/)gradlew(?:\.bat|\.ps1)?$/i,
|
|
||||||
/(?:^|\/)gradle\/wrapper\/gradle-wrapper\.properties$/i,
|
|
||||||
/(?:^|\/)lang\/.+\.properties$/i,
|
|
||||||
/(?:^|\/)l10n\/.+\.properties$/i,
|
|
||||||
/(?:^|\/)checkstyle\.xml$/i,
|
|
||||||
/(?:^|\/).+\.(?:png|jpe?g|gif|ico|svg|json|ttf|woff2?|webp|avif|jxl|heic|mp4|webm|mp3|ogg|wav|txt|toml)$/i
|
|
||||||
];
|
|
||||||
|
|
||||||
async function processPR(issueNumber) {
|
|
||||||
const files = await github.paginate(github.rest.pulls.listFiles, {
|
|
||||||
owner, repo, pull_number: issueNumber, per_page: 100,
|
|
||||||
});
|
|
||||||
|
|
||||||
const countedFiles = [];
|
|
||||||
const ignoredFiles = [];
|
|
||||||
let totalChanges = 0;
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
const ignored = ignoredFilePatterns.some(pattern => pattern.test(file.filename));
|
|
||||||
if (ignored) {
|
|
||||||
ignoredFiles.push(file.filename);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileChanges = file.changes ?? ((file.additions || 0) + (file.deletions || 0));
|
|
||||||
totalChanges += fileChanges;
|
|
||||||
countedFiles.push(`${file.filename} (${fileChanges})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
let targetLabel;
|
|
||||||
if (totalChanges <= 9) targetLabel = "1+";
|
|
||||||
else if (totalChanges <= 39) targetLabel = "10+";
|
|
||||||
else if (totalChanges <= 99) targetLabel = "40+";
|
|
||||||
else if (totalChanges <= 499) targetLabel = "100+";
|
|
||||||
else if (totalChanges <= 999) targetLabel = "500+";
|
|
||||||
else if (totalChanges <= 1999) targetLabel = "1000+";
|
|
||||||
else if (totalChanges <= 4999) targetLabel = "2000+";
|
|
||||||
else targetLabel = "5000+";
|
|
||||||
|
|
||||||
const currentLabels = await github.paginate(github.rest.issues.listLabelsOnIssue, {
|
|
||||||
owner, repo, issue_number: issueNumber, per_page: 100,
|
|
||||||
});
|
|
||||||
|
|
||||||
const sizeLabels = currentLabels.map(label => label.name).filter(name => /^\d+\+$/.test(name));
|
|
||||||
|
|
||||||
for (const name of sizeLabels) {
|
|
||||||
if (name !== targetLabel) {
|
|
||||||
await github.rest.issues.removeLabel({
|
|
||||||
owner, repo, issue_number: issueNumber, name,
|
|
||||||
}).catch(() => { });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!currentLabels.some(label => label.name === targetLabel)) {
|
|
||||||
await github.rest.issues.addLabels({
|
|
||||||
owner, repo, issue_number: issueNumber, labels: [targetLabel],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
core.info(`Pull request #${issueNumber} has ${totalChanges} counted changed lines, applied ${targetLabel}.`);
|
|
||||||
core.info(`Ignored files: ${ignoredFiles.length ? ignoredFiles.join(", ") : "none"}`);
|
|
||||||
core.info(`Counted files: ${countedFiles.length ? countedFiles.join(", ") : "none"}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (context.eventName === "workflow_dispatch") {
|
|
||||||
core.info("Manual trigger detected. Fetching all open pull requests...");
|
|
||||||
|
|
||||||
const openPRs = await github.paginate(github.rest.pulls.list, {
|
|
||||||
owner, repo, state: "open", per_page: 100,
|
|
||||||
});
|
|
||||||
|
|
||||||
const unlabelledPRs = openPRs.filter(pr => {
|
|
||||||
return !pr.labels.some(label => /^\d+\+$/.test(label.name));
|
|
||||||
});
|
|
||||||
|
|
||||||
core.info(`Found ${openPRs.length} open pull request(s), ${unlabelledPRs.length} of them lack a size label.`);
|
|
||||||
|
|
||||||
for (const pr of unlabelledPRs) {
|
|
||||||
core.info(`----------------------------------------`);
|
|
||||||
core.info(`Processing unlabelled pull request #${pr.number}: ${pr.title}`);
|
|
||||||
await processPR(pr.number);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const pr = context.payload.pull_request;
|
|
||||||
if (!pr) {
|
|
||||||
core.info("No pull request found in payload, skipping.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await processPR(pr.number);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
name: Check Codes
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- '**.java'
|
||||||
|
- '**.properties'
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- '**.java'
|
||||||
|
- '**.properties'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Set up JDK 17
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'zulu'
|
||||||
|
java-version: '17'
|
||||||
|
java-package: 'jdk+fx'
|
||||||
|
- name: Setup Gradle
|
||||||
|
uses: gradle/actions/setup-gradle@v5
|
||||||
|
with:
|
||||||
|
cache-cleanup: never
|
||||||
|
- name: Check Codes
|
||||||
|
run: ./gradlew checkstyle checkTranslations --no-daemon --parallel --stacktrace
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
name: Check Update
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
# schedule:
|
||||||
|
# - cron: '30 * * * *'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-update:
|
||||||
|
if: ${{ github.repository_owner == 'HMCL-dev' }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
max-parallel: 1
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- channel: dev
|
||||||
|
task: checkUpdateDev
|
||||||
|
- channel: stable
|
||||||
|
task: checkUpdateStable
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: check-update-${{ matrix.channel }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
- name: Set up JDK
|
||||||
|
uses: actions/setup-java@v5
|
||||||
|
with:
|
||||||
|
distribution: 'temurin'
|
||||||
|
java-version: '25'
|
||||||
|
- name: Fetch tags
|
||||||
|
run: git fetch --all --tags
|
||||||
|
- name: Fetch last version
|
||||||
|
run: ./gradlew ${{ matrix.task }} --no-daemon --info --stacktrace
|
||||||
|
- name: Check for existing tags
|
||||||
|
run: if [ -z "$(git tag -l "$HMCL_TAG_NAME")" ]; then echo "continue=true" >> $GITHUB_ENV; fi
|
||||||
|
- name: Download artifacts
|
||||||
|
if: ${{ env.continue == 'true' }}
|
||||||
|
run: |
|
||||||
|
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.exe"
|
||||||
|
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.exe.sha256"
|
||||||
|
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.jar"
|
||||||
|
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.jar.sha256"
|
||||||
|
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.sh"
|
||||||
|
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.sh.sha256"
|
||||||
|
env:
|
||||||
|
GH_DOWNLOAD_BASE_URL: https://github.com/HMCL-dev/HMCL/releases/download
|
||||||
|
- name: Generate release note
|
||||||
|
if: ${{ env.continue == 'true' }}
|
||||||
|
run: |
|
||||||
|
if [ "${{ matrix.channel }}" = "stable" ]; then
|
||||||
|
echo "**This version is a stable version.**" >> RELEASE_NOTE
|
||||||
|
echo "" >> RELEASE_NOTE
|
||||||
|
fi
|
||||||
|
echo "The full changelogs can be found on the website: https://docs.hmcl.net/changelog/${{ matrix.channel }}.html" >> RELEASE_NOTE
|
||||||
|
echo "" >> RELEASE_NOTE
|
||||||
|
echo "*Notice: changelogs are written in Chinese.*" >> RELEASE_NOTE
|
||||||
|
echo "" >> RELEASE_NOTE
|
||||||
|
echo "| File Name | SHA-256 Checksum |" >> RELEASE_NOTE
|
||||||
|
echo "| --- | --- |" >> RELEASE_NOTE
|
||||||
|
echo "| [HMCL-$HMCL_VERSION.exe]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.exe) | \`$(cat HMCL-$HMCL_VERSION.exe.sha256)\` |" >> RELEASE_NOTE
|
||||||
|
echo "| [HMCL-$HMCL_VERSION.jar]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.jar) | \`$(cat HMCL-$HMCL_VERSION.jar.sha256)\` |" >> RELEASE_NOTE
|
||||||
|
echo "| [HMCL-$HMCL_VERSION.sh]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.sh) | \`$(cat HMCL-$HMCL_VERSION.sh.sha256)\` |" >> RELEASE_NOTE
|
||||||
|
env:
|
||||||
|
GH_DOWNLOAD_BASE_URL: https://github.com/HMCL-dev/HMCL/releases/download
|
||||||
|
- name: Create release
|
||||||
|
if: ${{ env.continue == 'true' }}
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
body_path: RELEASE_NOTE
|
||||||
|
files: |
|
||||||
|
HMCL-${{ env.HMCL_VERSION }}.exe
|
||||||
|
HMCL-${{ env.HMCL_VERSION }}.jar
|
||||||
|
HMCL-${{ env.HMCL_VERSION }}.sh
|
||||||
|
target_commitish: ${{ env.HMCL_COMMIT_SHA }}
|
||||||
|
name: ${{ env.HMCL_TAG_NAME }}
|
||||||
|
tag_name: ${{ env.HMCL_TAG_NAME }}
|
||||||
|
prerelease: true
|
||||||
@@ -1,86 +1,54 @@
|
|||||||
name: Java CI
|
name: Java CI
|
||||||
|
|
||||||
# Pushes to main and pull requests only. A branch with an open pull request used to build twice
|
|
||||||
# (once for the push, once for the pull request), which kept the runners queued.
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
tags:
|
|
||||||
- '*'
|
|
||||||
pull_request:
|
pull_request:
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- '**.md'
|
- '**.md'
|
||||||
|
|
||||||
# A newer run for the same pull request or branch replaces the older one.
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@v6
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: 'liberica'
|
distribution: 'zulu'
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
java-package: 'jdk+fx'
|
java-package: 'jdk+fx'
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/actions/setup-gradle@v5
|
uses: gradle/actions/setup-gradle@v5
|
||||||
with:
|
with:
|
||||||
cache-cleanup: never
|
cache-cleanup: never
|
||||||
# Only main writes the dependency cache; pull requests read it.
|
- name: Build with Gradle
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
|
run: ./gradlew build --no-daemon --parallel
|
||||||
# The repository's wrapper downloads Gradle itself, so cache that download too.
|
|
||||||
- name: Cache Gradle distribution
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.gradle/wrapper
|
|
||||||
.gradle/wrapper-neo
|
|
||||||
key: gradle-dist-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/wrapper/GradleWrapperNeo.java') }}
|
|
||||||
- name: Install Xvfb
|
|
||||||
run: |
|
|
||||||
# Installing is the slowest setup step, so skip it when the runner already has xvfb, and
|
|
||||||
# only refresh the package index when a plain install fails.
|
|
||||||
command -v xvfb-run >/dev/null || sudo apt-get install -y --no-install-recommends xvfb \
|
|
||||||
|| { sudo apt-get update && sudo apt-get install -y --no-install-recommends xvfb; }
|
|
||||||
# One Gradle run builds, tests and runs the code and translation checks, so the
|
|
||||||
# project is configured and compiled once instead of in two workflows.
|
|
||||||
- name: Build and check with Gradle
|
|
||||||
run: xvfb-run --auto-servernum ./gradlew build checkstyle checkTranslations --no-daemon --parallel
|
|
||||||
env:
|
env:
|
||||||
JAVA_TOOL_OPTIONS: -Djava.awt.headless=true
|
|
||||||
MICROSOFT_AUTH_ID: ${{ secrets.MICROSOFT_AUTH_ID }}
|
MICROSOFT_AUTH_ID: ${{ secrets.MICROSOFT_AUTH_ID }}
|
||||||
|
MICROSOFT_AUTH_SECRET: ${{ secrets.MICROSOFT_AUTH_SECRET }}
|
||||||
CURSEFORGE_API_KEY: ${{ secrets.CURSEFORGE_API_KEY }}
|
CURSEFORGE_API_KEY: ${{ secrets.CURSEFORGE_API_KEY }}
|
||||||
- name: Get short SHA
|
- name: Get short SHA
|
||||||
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
|
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
|
||||||
- name: Upload JAR
|
- name: Upload JAR
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: HMCL-${{ env.SHORT_SHA }}-jar
|
name: HMCL-${{ env.SHORT_SHA }}-jar
|
||||||
path: HMCL/build/libs/HMCL-*.jar
|
path: |
|
||||||
archive: false
|
HMCL/build/libs/HMCL-*.jar
|
||||||
|
HMCL/build/libs/HMCL-*.jar.sha256
|
||||||
- name: Upload EXE
|
- name: Upload EXE
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: HMCL-${{ env.SHORT_SHA }}-exe
|
name: HMCL-${{ env.SHORT_SHA }}-exe
|
||||||
path: HMCL/build/libs/HMCL-*.exe
|
path: |
|
||||||
archive: false
|
HMCL/build/libs/HMCL-*.exe
|
||||||
|
HMCL/build/libs/HMCL-*.exe.sha256
|
||||||
- name: Upload SH
|
- name: Upload SH
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: HMCL-${{ env.SHORT_SHA }}-sh
|
name: HMCL-${{ env.SHORT_SHA }}-sh
|
||||||
path: HMCL/build/libs/HMCL-*.sh
|
path: |
|
||||||
archive: false
|
HMCL/build/libs/HMCL-*.sh
|
||||||
- name: Upload DEB
|
HMCL/build/libs/HMCL-*.sh.sha256
|
||||||
uses: actions/upload-artifact@v7
|
|
||||||
with:
|
|
||||||
name: HMCL-${{ env.SHORT_SHA }}-deb
|
|
||||||
path: HMCL/build/libs/HMCL-*.deb
|
|
||||||
archive: false
|
|
||||||
@@ -11,9 +11,8 @@ concurrency:
|
|||||||
jobs:
|
jobs:
|
||||||
mirror:
|
mirror:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
target:
|
||||||
- name: Gitee
|
- name: Gitee
|
||||||
repo: gitee.com/huanghongxun/HMCL
|
repo: gitee.com/huanghongxun/HMCL
|
||||||
user: 'hmcl-sync'
|
user: 'hmcl-sync'
|
||||||
@@ -22,12 +21,12 @@ jobs:
|
|||||||
repo: cnb.cool/HMCL-dev/HMCL
|
repo: cnb.cool/HMCL-dev/HMCL
|
||||||
user: 'cnb'
|
user: 'cnb'
|
||||||
token: 'CNB_SYNC_TOKEN'
|
token: 'CNB_SYNC_TOKEN'
|
||||||
name: Mirror to ${{ matrix.name }}
|
name: Mirror to ${{ matrix.target.name }}
|
||||||
if: ${{ github.repository == 'HMCL-dev/HMCL' }}
|
if: ${{ github.repository == 'HMCL-dev/HMCL' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Mirror GitHub to ${{ matrix.name }}
|
- name: Mirror GitHub to ${{ matrix.target.name }}
|
||||||
run: |
|
run: |
|
||||||
git clone --mirror "https://github.com/${{ github.repository }}.git" -- repo
|
git clone --mirror "https://github.com/$GITHUB_REPOSITORY.git" -- repo
|
||||||
cd repo
|
cd repo
|
||||||
git push -f --prune "https://${{ matrix.user }}:${{ secrets[matrix.token] }}@${{ matrix.repo }}.git" "refs/heads/*:refs/heads/*" "refs/tags/*:refs/tags/*"
|
git push -f --prune "https://${{ matrix.target.user }}:${{ secrets[matrix.target.token] }}@${{ matrix.target.repo }}.git" "refs/heads/*:refs/heads/*" "refs/tags/*:refs/tags/*"
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
name: PR Size Label
|
|
||||||
|
|
||||||
# Disabled on this Gitea fork: act resolves actions/github-script from
|
|
||||||
# git.nest.potatolab.uk and that mirror 404s. Re-enable after mirroring
|
|
||||||
# actions/github-script or replacing the labeler with a plain shell step.
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
issues: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
label:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: false
|
|
||||||
steps:
|
|
||||||
- name: Disabled
|
|
||||||
run: echo "pr-size-label is disabled on this fork"
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
name: Create Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
# schedule:
|
|
||||||
# - cron: '30 * * * *'
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
check-update:
|
|
||||||
if: ${{ github.repository_owner == 'HMCL-dev' }}
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
max-parallel: 1
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- channel: dev
|
|
||||||
task: checkUpdateDev
|
|
||||||
- channel: stable
|
|
||||||
task: checkUpdateStable
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: check-update-${{ matrix.channel }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
fetch-tags: true
|
|
||||||
- name: Set up JDK
|
|
||||||
uses: actions/setup-java@v5
|
|
||||||
with:
|
|
||||||
distribution: 'temurin'
|
|
||||||
java-version: '25'
|
|
||||||
- name: Fetch last version
|
|
||||||
run: ./gradlew ${{ matrix.task }} --no-daemon --info --stacktrace
|
|
||||||
- name: Check for existing tags
|
|
||||||
run: if [ -z "$(git tag -l "$HMCL_TAG_NAME")" ]; then echo "continue=true" >> $GITHUB_ENV; fi
|
|
||||||
- name: Download artifacts
|
|
||||||
if: ${{ env.continue == 'true' }}
|
|
||||||
run: |
|
|
||||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.exe"
|
|
||||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.exe.sha256"
|
|
||||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.jar"
|
|
||||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.jar.sha256"
|
|
||||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.sh"
|
|
||||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.sh.sha256"
|
|
||||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.deb"
|
|
||||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.deb.sha256"
|
|
||||||
- name: Verify artifacts
|
|
||||||
if: ${{ env.continue == 'true' }}
|
|
||||||
run: |
|
|
||||||
export JAR_SHA256=$(cat HMCL-$HMCL_VERSION.jar.sha256 | tr -d '\n')
|
|
||||||
export EXE_SHA256=$(cat HMCL-$HMCL_VERSION.exe.sha256 | tr -d '\n')
|
|
||||||
export SH_SHA256=$(cat HMCL-$HMCL_VERSION.sh.sha256 | tr -d '\n')
|
|
||||||
export DEB_SHA256=$(cat HMCL-$HMCL_VERSION.deb.sha256 | tr -d '\n')
|
|
||||||
|
|
||||||
echo "$JAR_SHA256 HMCL-$HMCL_VERSION.jar" | sha256sum -c
|
|
||||||
echo "$EXE_SHA256 HMCL-$HMCL_VERSION.exe" | sha256sum -c
|
|
||||||
echo "$SH_SHA256 HMCL-$HMCL_VERSION.sh" | sha256sum -c
|
|
||||||
echo "$DEB_SHA256 HMCL-$HMCL_VERSION.deb" | sha256sum -c
|
|
||||||
- name: Generate release note
|
|
||||||
if: ${{ env.continue == 'true' }}
|
|
||||||
run: |
|
|
||||||
# GitHub Release Note
|
|
||||||
echo " **[Changelog](https://docs.hmcl.net/changelog/${{ matrix.channel }}.html#HMCL-$HMCL_VERSION)** (Chinese)" >> RELEASE_NOTE
|
|
||||||
echo "" >> RELEASE_NOTE
|
|
||||||
echo "| File | SHA-256 Checksum |" >> RELEASE_NOTE
|
|
||||||
echo "| --- | --- |" >> RELEASE_NOTE
|
|
||||||
echo "| [HMCL-$HMCL_VERSION.exe]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.exe) | \`$(cat HMCL-$HMCL_VERSION.exe.sha256)\` |" >> RELEASE_NOTE
|
|
||||||
echo "| [HMCL-$HMCL_VERSION.jar]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.jar) | \`$(cat HMCL-$HMCL_VERSION.jar.sha256)\` |" >> RELEASE_NOTE
|
|
||||||
echo "| [HMCL-$HMCL_VERSION.sh]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.sh) | \`$(cat HMCL-$HMCL_VERSION.sh.sha256)\` |" >> RELEASE_NOTE
|
|
||||||
echo "| [HMCL-$HMCL_VERSION.deb]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.deb) | \`$(cat HMCL-$HMCL_VERSION.deb.sha256)\` |" >> RELEASE_NOTE
|
|
||||||
|
|
||||||
# CNB Release Note
|
|
||||||
echo "[更新日志](https://docs.hmcl.net/changelog/${{ matrix.channel }}.html#HMCL-$HMCL_VERSION)" >> CNB_RELEASE_NOTE
|
|
||||||
echo "" >> CNB_RELEASE_NOTE
|
|
||||||
echo "| 文件 | SHA-256 校验码 |" >> CNB_RELEASE_NOTE
|
|
||||||
echo "| :--- | --- |" >> CNB_RELEASE_NOTE
|
|
||||||
echo "| [HMCL-$HMCL_VERSION.exe]($CNB_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.exe) | \`$(cat HMCL-$HMCL_VERSION.exe.sha256)\` |" >> CNB_RELEASE_NOTE
|
|
||||||
echo "| [HMCL-$HMCL_VERSION.jar]($CNB_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.jar) | \`$(cat HMCL-$HMCL_VERSION.jar.sha256)\` |" >> CNB_RELEASE_NOTE
|
|
||||||
echo "| [HMCL-$HMCL_VERSION.sh]($CNB_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.sh) | \`$(cat HMCL-$HMCL_VERSION.sh.sha256)\` |" >> CNB_RELEASE_NOTE
|
|
||||||
echo "| [HMCL-$HMCL_VERSION.deb]($CNB_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.deb) | \`$(cat HMCL-$HMCL_VERSION.deb.sha256)\` |" >> CNB_RELEASE_NOTE
|
|
||||||
|
|
||||||
env:
|
|
||||||
GH_DOWNLOAD_BASE_URL: https://github.com/${{ github.repository }}/releases/download
|
|
||||||
CNB_DOWNLOAD_BASE_URL: https://cnb.cool/HMCL-dev/HMCL/-/releases/download
|
|
||||||
- name: Create GitHub release
|
|
||||||
if: ${{ env.continue == 'true' }}
|
|
||||||
run: |
|
|
||||||
gh release create "${{ env.HMCL_TAG_NAME }}" \
|
|
||||||
"HMCL-${{ env.HMCL_VERSION }}.exe" \
|
|
||||||
"HMCL-${{ env.HMCL_VERSION }}.jar" \
|
|
||||||
"HMCL-${{ env.HMCL_VERSION }}.sh" \
|
|
||||||
"HMCL-${{ env.HMCL_VERSION }}.deb" \
|
|
||||||
--target "${{ env.HMCL_COMMIT_SHA }}" \
|
|
||||||
--title "${{ env.HMCL_TAG_NAME }}" \
|
|
||||||
--notes-file RELEASE_NOTE \
|
|
||||||
--prerelease
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
- name: Install git-cnb
|
|
||||||
if: ${{ env.continue == 'true' }}
|
|
||||||
run: go install "cnb.cool/looc/git-cnb@$GIT_CNB_VERSION"
|
|
||||||
env:
|
|
||||||
GIT_CNB_VERSION: '1.1.2'
|
|
||||||
- name: Create CNB release
|
|
||||||
if: ${{ env.continue == 'true' }}
|
|
||||||
run: |
|
|
||||||
echo "Uploading tags to CNB"
|
|
||||||
git fetch --tags
|
|
||||||
git push "https://cnb:${{ secrets.CNB_SYNC_TOKEN }}@cnb.cool/$CNB_REPO.git" "$HMCL_TAG_NAME"
|
|
||||||
|
|
||||||
echo "Creating CNB release"
|
|
||||||
~/go/bin/git-cnb release create \
|
|
||||||
--repo "$CNB_REPO" \
|
|
||||||
--tag "$HMCL_TAG_NAME" \
|
|
||||||
--name "HMCL $HMCL_VERSION" \
|
|
||||||
--body "$(cat CNB_RELEASE_NOTE)" \
|
|
||||||
--prerelease true
|
|
||||||
|
|
||||||
echo "Uploading HMCL-$HMCL_VERSION.jar"
|
|
||||||
~/go/bin/git-cnb release asset-upload --repo="$CNB_REPO" --tag-name "$HMCL_TAG_NAME" --file-name "HMCL-$HMCL_VERSION.jar"
|
|
||||||
|
|
||||||
echo "Uploading HMCL-$HMCL_VERSION.exe"
|
|
||||||
~/go/bin/git-cnb release asset-upload --repo="$CNB_REPO" --tag-name "$HMCL_TAG_NAME" --file-name "HMCL-$HMCL_VERSION.exe"
|
|
||||||
|
|
||||||
echo "Uploading HMCL-$HMCL_VERSION.sh"
|
|
||||||
~/go/bin/git-cnb release asset-upload --repo="$CNB_REPO" --tag-name "$HMCL_TAG_NAME" --file-name "HMCL-$HMCL_VERSION.sh"
|
|
||||||
|
|
||||||
echo "Uploading HMCL-$HMCL_VERSION.deb"
|
|
||||||
~/go/bin/git-cnb release asset-upload --repo="$CNB_REPO" --tag-name "$HMCL_TAG_NAME" --file-name "HMCL-$HMCL_VERSION.deb"
|
|
||||||
env:
|
|
||||||
CNB_TOKEN: ${{ secrets.CNB_SYNC_TOKEN }}
|
|
||||||
CNB_REPO: HMCL-dev/HMCL
|
|
||||||
+1
-17
@@ -4,10 +4,7 @@
|
|||||||
hs_err_pid*
|
hs_err_pid*
|
||||||
*.hprof
|
*.hprof
|
||||||
|
|
||||||
*.jfr
|
|
||||||
|
|
||||||
.gradle
|
.gradle
|
||||||
gradle.properties
|
|
||||||
|
|
||||||
*.lck
|
*.lck
|
||||||
*.1
|
*.1
|
||||||
@@ -18,9 +15,6 @@ gradle.properties
|
|||||||
NVIDIA
|
NVIDIA
|
||||||
minecraft-exported-crash-info*
|
minecraft-exported-crash-info*
|
||||||
hmcl-exported-logs-*
|
hmcl-exported-logs-*
|
||||||
terracotta-log-*
|
|
||||||
hmcl-mod-update-list-*
|
|
||||||
*.hmcl-theme
|
|
||||||
/.java/
|
/.java/
|
||||||
/.local/
|
/.local/
|
||||||
/.cache/
|
/.cache/
|
||||||
@@ -48,7 +42,6 @@ language-subtag-registry
|
|||||||
# eclipse
|
# eclipse
|
||||||
/bin/
|
/bin/
|
||||||
/HMCL/bin/
|
/HMCL/bin/
|
||||||
/HMCLBoot/bin/
|
|
||||||
/HMCLCore/bin/
|
/HMCLCore/bin/
|
||||||
/minecraft/libraries/HMCLTransformerDiscoveryService/bin/
|
/minecraft/libraries/HMCLTransformerDiscoveryService/bin/
|
||||||
/minecraft/libraries/HMCLMultiMCBootstrap/bin/
|
/minecraft/libraries/HMCLMultiMCBootstrap/bin/
|
||||||
@@ -70,13 +63,4 @@ language-subtag-registry
|
|||||||
# test
|
# test
|
||||||
/hmcl.json
|
/hmcl.json
|
||||||
/.hmcl.json
|
/.hmcl.json
|
||||||
/.hmcl/
|
/.hmcl/
|
||||||
|
|
||||||
# AI
|
|
||||||
/.gemini/
|
|
||||||
!/.gemini/config.yaml
|
|
||||||
.codex
|
|
||||||
/.gradle-*/
|
|
||||||
|
|
||||||
# Managed Java downloaded by the launcher when it is run from the repository root
|
|
||||||
/java/
|
|
||||||
Generated
-6
@@ -1,6 +0,0 @@
|
|||||||
<component name="CopyrightManager">
|
|
||||||
<copyright>
|
|
||||||
<option name="notice" value="SPDX-FileCopyrightText: Copyright (C) &#36;today.year The HMCL Authors SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL" />
|
|
||||||
<option name="myName" value="HMCL" />
|
|
||||||
</copyright>
|
|
||||||
</component>
|
|
||||||
Generated
-9
@@ -1,9 +0,0 @@
|
|||||||
<component name="CopyrightManager">
|
|
||||||
<settings default="HMCL">
|
|
||||||
<LanguageOptions name="JAVA">
|
|
||||||
<option name="fileTypeOverride" value="3" />
|
|
||||||
<option name="addBlankAfter" value="false" />
|
|
||||||
<option name="block" value="false" />
|
|
||||||
</LanguageOptions>
|
|
||||||
</settings>
|
|
||||||
</component>
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# Java Code Style Requirements
|
|
||||||
|
|
||||||
These rules apply to all Java code written or modified in this repository.
|
|
||||||
|
|
||||||
## Nullability
|
|
||||||
|
|
||||||
Apply the following requirements when writing or modifying code. Do not use them as code-review criteria.
|
|
||||||
|
|
||||||
- Every class declared in a newly added Java source file must be annotated with JetBrains Annotations `@NotNullByDefault`.
|
|
||||||
- When writing or modifying Java code, any type, field, parameter, return value, local variable, or generic type argument that may be `null` must be explicitly annotated with `@Nullable`.
|
|
||||||
- Nullability in code being written or modified must never be implicit.
|
|
||||||
|
|
||||||
## Immutability
|
|
||||||
|
|
||||||
- Immutable arrays and collections must be explicitly annotated with JetBrains Annotations `@Unmodifiable` or `@UnmodifiableView` as appropriate.
|
|
||||||
- For arrays, use type-use syntax such as `String @Unmodifiable []`.
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
Apply the following requirements when writing or modifying code. Do not use them as code-review criteria.
|
|
||||||
|
|
||||||
- Every class, field, and method must have documentation.
|
|
||||||
- Documentation must use `///` Markdown-style Javadoc comments.
|
|
||||||
- Keep documentation accurate and specific to the actual behavior, constraints, and side effects.
|
|
||||||
- Add concise implementation comments inside complex logic whenever they materially improve readability or explain non-obvious behavior.
|
|
||||||
+53
-119
@@ -1,16 +1,11 @@
|
|||||||
import org.jackhuang.hmcl.gradle.addon.FormatJsonFile
|
|
||||||
import org.jackhuang.hmcl.gradle.addon.UpdateAddonProjectInfos
|
|
||||||
import org.jackhuang.hmcl.gradle.ci.GitHubActionUtils
|
import org.jackhuang.hmcl.gradle.ci.GitHubActionUtils
|
||||||
import org.jackhuang.hmcl.gradle.ci.JenkinsUtils
|
import org.jackhuang.hmcl.gradle.ci.JenkinsUtils
|
||||||
import org.jackhuang.hmcl.gradle.l10n.CheckTranslations
|
import org.jackhuang.hmcl.gradle.l10n.CheckTranslations
|
||||||
import org.jackhuang.hmcl.gradle.l10n.CreateLanguageList
|
import org.jackhuang.hmcl.gradle.l10n.CreateLanguageList
|
||||||
import org.jackhuang.hmcl.gradle.l10n.CreateLocaleNamesResourceBundle
|
import org.jackhuang.hmcl.gradle.l10n.CreateLocaleNamesResourceBundle
|
||||||
import org.jackhuang.hmcl.gradle.l10n.SyncTranslations
|
|
||||||
import org.jackhuang.hmcl.gradle.l10n.UpsideDownTranslate
|
import org.jackhuang.hmcl.gradle.l10n.UpsideDownTranslate
|
||||||
import org.jackhuang.hmcl.gradle.pack.CreateDeb
|
import org.jackhuang.hmcl.gradle.mod.ParseModDataTask
|
||||||
import org.jackhuang.hmcl.gradle.pack.ReleaseType
|
|
||||||
import org.jackhuang.hmcl.gradle.utils.PropertiesUtils
|
import org.jackhuang.hmcl.gradle.utils.PropertiesUtils
|
||||||
import org.jackhuang.hmcl.gradle.utils.OfficialBuildValueSource
|
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.nio.file.FileSystems
|
import java.nio.file.FileSystems
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
@@ -32,6 +27,7 @@ val versionType = System.getenv("VERSION_TYPE") ?: if (isOfficial) "nightly" els
|
|||||||
val versionRoot = System.getenv("VERSION_ROOT") ?: projectConfig.getProperty("versionRoot") ?: "3"
|
val versionRoot = System.getenv("VERSION_ROOT") ?: projectConfig.getProperty("versionRoot") ?: "3"
|
||||||
|
|
||||||
val microsoftAuthId = System.getenv("MICROSOFT_AUTH_ID") ?: ""
|
val microsoftAuthId = System.getenv("MICROSOFT_AUTH_ID") ?: ""
|
||||||
|
val microsoftAuthSecret = System.getenv("MICROSOFT_AUTH_SECRET") ?: ""
|
||||||
val curseForgeApiKey = System.getenv("CURSEFORGE_API_KEY") ?: ""
|
val curseForgeApiKey = System.getenv("CURSEFORGE_API_KEY") ?: ""
|
||||||
|
|
||||||
val launcherExe = System.getenv("HMCL_LAUNCHER_EXE") ?: ""
|
val launcherExe = System.getenv("HMCL_LAUNCHER_EXE") ?: ""
|
||||||
@@ -54,27 +50,20 @@ if (buildNumber != null) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val embedResources = configurations.register("embedResources")
|
val embedResources by configurations.registering
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":HMCLCore"))
|
implementation(project(":HMCLCore"))
|
||||||
implementation(project(":HMCLBoot"))
|
implementation(project(":HMCLBoot"))
|
||||||
implementation("libs:JFoenix")
|
implementation("libs:JFoenix")
|
||||||
implementation(libs.jwebp)
|
implementation(libs.twelvemonkeys.imageio.webp)
|
||||||
implementation(libs.fxsvgimage)
|
|
||||||
implementation(libs.java.info)
|
implementation(libs.java.info)
|
||||||
implementation(libs.monet.fx)
|
|
||||||
implementation(libs.nayuki.qrcodegen)
|
|
||||||
implementation(libs.uuid.tools)
|
|
||||||
|
|
||||||
testImplementation(libs.jimfs)
|
|
||||||
|
|
||||||
if (launcherExe.isBlank()) {
|
if (launcherExe.isBlank()) {
|
||||||
implementation(libs.hmclauncher)
|
implementation(libs.hmclauncher)
|
||||||
}
|
}
|
||||||
|
|
||||||
embedResources(libs.authlib.injector)
|
embedResources(libs.authlib.injector)
|
||||||
embedResources(libs.lwjgl.unsafe.agent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun digest(algorithm: String, bytes: ByteArray): ByteArray = MessageDigest.getInstance(algorithm).digest(bytes)
|
fun digest(algorithm: String, bytes: ByteArray): ByteArray = MessageDigest.getInstance(algorithm).digest(bytes)
|
||||||
@@ -128,6 +117,10 @@ tasks.checkstyleMain {
|
|||||||
exclude("**/org/jackhuang/hmcl/ui/image/apng/**")
|
exclude("**/org/jackhuang/hmcl/ui/image/apng/**")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.compileJava {
|
||||||
|
options.compilerArgs.add("--add-exports=java.base/jdk.internal.loader=ALL-UNNAMED")
|
||||||
|
}
|
||||||
|
|
||||||
val addOpens = listOf(
|
val addOpens = listOf(
|
||||||
"java.base/java.lang",
|
"java.base/java.lang",
|
||||||
"java.base/java.lang.reflect",
|
"java.base/java.lang.reflect",
|
||||||
@@ -135,48 +128,42 @@ val addOpens = listOf(
|
|||||||
"javafx.base/com.sun.javafx.binding",
|
"javafx.base/com.sun.javafx.binding",
|
||||||
"javafx.base/com.sun.javafx.event",
|
"javafx.base/com.sun.javafx.event",
|
||||||
"javafx.base/com.sun.javafx.runtime",
|
"javafx.base/com.sun.javafx.runtime",
|
||||||
"javafx.base/javafx.beans.property",
|
|
||||||
"javafx.graphics/javafx.css",
|
"javafx.graphics/javafx.css",
|
||||||
"javafx.graphics/javafx.stage",
|
|
||||||
"javafx.graphics/javafx.scene",
|
|
||||||
"javafx.graphics/com.sun.glass.ui",
|
|
||||||
"javafx.graphics/com.sun.javafx.stage",
|
"javafx.graphics/com.sun.javafx.stage",
|
||||||
"javafx.graphics/com.sun.javafx.util",
|
|
||||||
"javafx.graphics/com.sun.prism",
|
"javafx.graphics/com.sun.prism",
|
||||||
"javafx.controls/com.sun.javafx.scene.control",
|
"javafx.controls/com.sun.javafx.scene.control",
|
||||||
"javafx.controls/com.sun.javafx.scene.control.behavior",
|
"javafx.controls/com.sun.javafx.scene.control.behavior",
|
||||||
"javafx.graphics/com.sun.javafx.tk.quantum",
|
|
||||||
"javafx.controls/javafx.scene.control",
|
|
||||||
"javafx.controls/javafx.scene.control.skin",
|
"javafx.controls/javafx.scene.control.skin",
|
||||||
"jdk.attach/sun.tools.attach",
|
"jdk.attach/sun.tools.attach",
|
||||||
)
|
)
|
||||||
|
|
||||||
tasks.compileJava {
|
val hmclProperties = buildList {
|
||||||
options.compilerArgs.addAll(addOpens.map { "--add-exports=$it=ALL-UNNAMED" })
|
add("hmcl.version" to project.version.toString())
|
||||||
|
add("hmcl.add-opens" to addOpens.joinToString(" "))
|
||||||
|
System.getenv("GITHUB_SHA")?.let {
|
||||||
|
add("hmcl.version.hash" to it)
|
||||||
|
}
|
||||||
|
add("hmcl.version.type" to versionType)
|
||||||
|
add("hmcl.microsoft.auth.id" to microsoftAuthId)
|
||||||
|
add("hmcl.microsoft.auth.secret" to microsoftAuthSecret)
|
||||||
|
add("hmcl.curseforge.apikey" to curseForgeApiKey)
|
||||||
|
add("hmcl.authlib-injector.version" to libs.authlib.injector.get().version!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
val createPropertiesFile = tasks.register<WriteProperties>("createPropertiesFile") {
|
val hmclPropertiesFile = layout.buildDirectory.file("hmcl.properties")
|
||||||
destinationFile.set(layout.buildDirectory.file("hmcl.properties"))
|
val createPropertiesFile by tasks.registering {
|
||||||
encoding = "UTF-8"
|
outputs.file(hmclPropertiesFile)
|
||||||
|
hmclProperties.forEach { (k, v) -> inputs.property(k, v) }
|
||||||
|
|
||||||
property("hmcl.version", project.version.toString())
|
doLast {
|
||||||
property("hmcl.add-opens", addOpens.joinToString(" "))
|
val targetFile = hmclPropertiesFile.get().asFile
|
||||||
System.getenv("GITHUB_SHA")?.let { property("hmcl.version.hash", it) }
|
targetFile.parentFile.mkdir()
|
||||||
property("hmcl.version.type", versionType)
|
targetFile.bufferedWriter().use {
|
||||||
property("hmcl.microsoft.auth.id", microsoftAuthId)
|
for ((k, v) in hmclProperties) {
|
||||||
property("hmcl.curseforge.apikey", curseForgeApiKey)
|
it.write("$k=$v\n")
|
||||||
property("hmcl.authlib-injector.version", libs.authlib.injector.get().version!!)
|
}
|
||||||
property("hmcl.lwjgl-unsafe-agent.version", libs.lwjgl.unsafe.agent.get().version!!)
|
}
|
||||||
property("hmcl.official", providers.of(OfficialBuildValueSource::class) {
|
}
|
||||||
parameters.publicKeyFile.set(layout.projectDirectory.file("src/main/resources/assets/hmcl_signature_publickey.der"))
|
|
||||||
parameters.privateKeyLocation.set(providers.environmentVariable("HMCL_SIGNATURE_KEY"))
|
|
||||||
// Pin the official public key independently of the replaceable resource file.
|
|
||||||
// Under HMCL’s license agreement, you **MUST** change the software name or the version number in
|
|
||||||
// a reasonable way to distinguish it from the original version.
|
|
||||||
// You may modify this hash if and only if you have changed the software name or version number
|
|
||||||
// in accordance with the license requirements.
|
|
||||||
parameters.expectedSha256.set("36aadcfde8626b9d4c7a74facb3fdfda47e703c205a673fbe6a56b1d419f8b8e")
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.jar {
|
tasks.jar {
|
||||||
@@ -198,7 +185,7 @@ tasks.shadowJar {
|
|||||||
exclude("META-INF/services/javax.imageio.spi.ImageInputStreamSpi")
|
exclude("META-INF/services/javax.imageio.spi.ImageInputStreamSpi")
|
||||||
|
|
||||||
listOf(
|
listOf(
|
||||||
"aix-*", "sunos-*", "openbsd-*", "dragonflybsd-*", "freebsd-*", "linux-*",
|
"aix-*", "sunos-*", "openbsd-*", "dragonflybsd-*", "freebsd-*", "linux-*", "darwin-*",
|
||||||
"*-ppc", "*-ppc64le", "*-s390x", "*-armel",
|
"*-ppc", "*-ppc64le", "*-s390x", "*-armel",
|
||||||
).forEach { exclude("com/sun/jna/$it/**") }
|
).forEach { exclude("com/sun/jna/$it/**") }
|
||||||
|
|
||||||
@@ -210,13 +197,12 @@ tasks.shadowJar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
manifest.attributes(
|
manifest.attributes(
|
||||||
"Created-By" to "Copyright(c) 2013-2026 The HMCL Authors",
|
"Created-By" to "Copyright(c) 2013-2025 huangyuhui.",
|
||||||
"Implementation-Version" to project.version.toString(),
|
"Implementation-Version" to project.version.toString(),
|
||||||
"Main-Class" to "org.jackhuang.hmcl.Main",
|
"Main-Class" to "org.jackhuang.hmcl.Main",
|
||||||
"Multi-Release" to "true",
|
"Multi-Release" to "true",
|
||||||
"Add-Opens" to addOpens.joinToString(" "),
|
"Add-Opens" to addOpens.joinToString(" "),
|
||||||
"Enable-Native-Access" to "ALL-UNNAMED",
|
"Enable-Native-Access" to "ALL-UNNAMED"
|
||||||
"Enable-Final-Field-Mutation" to "ALL-UNNAMED",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if (launcherExe.isNotBlank()) {
|
if (launcherExe.isNotBlank()) {
|
||||||
@@ -232,12 +218,13 @@ tasks.shadowJar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tasks.processResources {
|
tasks.processResources {
|
||||||
|
dependsOn(createPropertiesFile)
|
||||||
dependsOn(upsideDownTranslate)
|
dependsOn(upsideDownTranslate)
|
||||||
dependsOn(createLocaleNamesResourceBundle)
|
dependsOn(createLocaleNamesResourceBundle)
|
||||||
dependsOn(createLanguageList)
|
dependsOn(createLanguageList)
|
||||||
|
|
||||||
into("assets/") {
|
into("assets/") {
|
||||||
from(createPropertiesFile.flatMap { it.destinationFile })
|
from(hmclPropertiesFile)
|
||||||
from(embedResources)
|
from(embedResources)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,25 +233,22 @@ tasks.processResources {
|
|||||||
from(upsideDownTranslate.map { it.outputFile })
|
from(upsideDownTranslate.map { it.outputFile })
|
||||||
from(createLocaleNamesResourceBundle.map { it.outputDirectory })
|
from(createLocaleNamesResourceBundle.map { it.outputDirectory })
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun artifactFile(ext: String) = jarPath.resolveSibling(jarPath.nameWithoutExtension + '.' + ext)
|
val makeExecutables by tasks.registering {
|
||||||
|
|
||||||
val makeExecutables = tasks.register("makeExecutables") {
|
|
||||||
val extensions = listOf("exe", "sh")
|
val extensions = listOf("exe", "sh")
|
||||||
|
|
||||||
dependsOn(tasks.jar)
|
dependsOn(tasks.jar)
|
||||||
|
|
||||||
inputs.file(jarPath)
|
inputs.file(jarPath)
|
||||||
outputs.files(extensions.map { artifactFile(it) })
|
outputs.files(extensions.map { File(jarPath.parentFile, jarPath.nameWithoutExtension + '.' + it) })
|
||||||
|
|
||||||
doLast {
|
doLast {
|
||||||
val jarContent = jarPath.readBytes()
|
val jarContent = jarPath.readBytes()
|
||||||
|
|
||||||
ZipFile(jarPath).use { zipFile ->
|
ZipFile(jarPath).use { zipFile ->
|
||||||
for (extension in extensions) {
|
for (extension in extensions) {
|
||||||
val output = artifactFile(extension)
|
val output = File(jarPath.parentFile, jarPath.nameWithoutExtension + '.' + extension)
|
||||||
val entry = zipFile.getEntry("assets/HMCLauncher.$extension")
|
val entry = zipFile.getEntry("assets/HMCLauncher.$extension")
|
||||||
?: throw GradleException("HMCLauncher.$extension not found")
|
?: throw GradleException("HMCLauncher.$extension not found")
|
||||||
|
|
||||||
@@ -279,32 +263,8 @@ val makeExecutables = tasks.register("makeExecutables") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val makeDeb = tasks.register("makeDeb", CreateDeb::class) {
|
|
||||||
dependsOn(makeExecutables)
|
|
||||||
|
|
||||||
val debFile = layout.file(provider { artifactFile("deb") })
|
|
||||||
|
|
||||||
val debChannel = when (versionType) {
|
|
||||||
"stable" -> ReleaseType.STABLE
|
|
||||||
"dev" -> ReleaseType.DEVELOPMENT
|
|
||||||
else -> ReleaseType.NIGHTLY
|
|
||||||
}
|
|
||||||
|
|
||||||
version.set(project.version.toString())
|
|
||||||
releaseType.set(debChannel)
|
|
||||||
launcherClassName.set("org.jackhuang.hmcl.Launcher")
|
|
||||||
appShFile.set(layout.file(provider { artifactFile("sh") }))
|
|
||||||
iconFile.set(layout.projectDirectory.file("image/hmcl.png"))
|
|
||||||
outputFile.set(debFile)
|
|
||||||
|
|
||||||
doLast {
|
|
||||||
createChecksum(debFile.get().asFile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.build {
|
tasks.build {
|
||||||
dependsOn(makeExecutables)
|
dependsOn(makeExecutables)
|
||||||
dependsOn(makeDeb)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun parseToolOptions(options: String?): MutableList<String> {
|
fun parseToolOptions(options: String?): MutableList<String> {
|
||||||
@@ -357,11 +317,6 @@ fun parseToolOptions(options: String?): MutableList<String> {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tests use the same JavaFX reflective access as the packaged application.
|
|
||||||
tasks.withType<Test> {
|
|
||||||
jvmArgs(addOpens.map { "--add-opens=$it=ALL-UNNAMED" })
|
|
||||||
}
|
|
||||||
|
|
||||||
// For IntelliJ IDEA
|
// For IntelliJ IDEA
|
||||||
tasks.withType<JavaExec> {
|
tasks.withType<JavaExec> {
|
||||||
if (name != "run") {
|
if (name != "run") {
|
||||||
@@ -400,69 +355,48 @@ tasks.register<JavaExec>("run") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Check Translations
|
// Check Translations
|
||||||
|
|
||||||
tasks.register<CheckTranslations>("checkTranslations") {
|
tasks.register<CheckTranslations>("checkTranslations") {
|
||||||
val dir = layout.projectDirectory.dir("src/main/resources/assets/lang")
|
val dir = layout.projectDirectory.dir("src/main/resources/assets/lang")
|
||||||
|
|
||||||
englishFile.set(dir.file("I18N.properties"))
|
englishFile.set(dir.file("I18N.properties"))
|
||||||
simplifiedChineseFile.set(dir.file("I18N_zh_Hans.properties"))
|
simplifiedChineseFile.set(dir.file("I18N_zh_CN.properties"))
|
||||||
traditionalChineseFile.set(dir.file("I18N_zh_Hant.properties"))
|
traditionalChineseFile.set(dir.file("I18N_zh.properties"))
|
||||||
classicalChineseFile.set(dir.file("I18N_lzh.properties"))
|
classicalChineseFile.set(dir.file("I18N_lzh.properties"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// l10n
|
// l10n
|
||||||
|
|
||||||
tasks.register<SyncTranslations>("syncTranslations") {
|
|
||||||
group = "localization"
|
|
||||||
description = "Synchronizes translation keys and blank lines with I18N.properties."
|
|
||||||
|
|
||||||
val dir = layout.projectDirectory.dir("src/main/resources/assets/lang")
|
|
||||||
sourceFile.set(dir.file("I18N.properties"))
|
|
||||||
translationFiles.from(fileTree(dir) { include("I18N_*.properties") })
|
|
||||||
}
|
|
||||||
|
|
||||||
val generatedDir = layout.buildDirectory.dir("generated")
|
val generatedDir = layout.buildDirectory.dir("generated")
|
||||||
|
|
||||||
val upsideDownTranslate = tasks.register<UpsideDownTranslate>("upsideDownTranslate") {
|
val upsideDownTranslate by tasks.registering(UpsideDownTranslate::class) {
|
||||||
inputFile.set(layout.projectDirectory.file("src/main/resources/assets/lang/I18N.properties"))
|
inputFile.set(layout.projectDirectory.file("src/main/resources/assets/lang/I18N.properties"))
|
||||||
outputFile.set(generatedDir.map { it.file("generated/i18n/I18N_en_Qabs.properties") })
|
outputFile.set(generatedDir.map { it.file("generated/i18n/I18N_en_Qabs.properties") })
|
||||||
}
|
}
|
||||||
|
|
||||||
val createLanguageList = tasks.register<CreateLanguageList>("createLanguageList") {
|
val createLanguageList by tasks.registering(CreateLanguageList::class) {
|
||||||
resourceBundleDir.set(layout.projectDirectory.dir("src/main/resources/assets/lang"))
|
resourceBundleDir.set(layout.projectDirectory.dir("src/main/resources/assets/lang"))
|
||||||
resourceBundleBaseName.set("I18N")
|
resourceBundleBaseName.set("I18N")
|
||||||
additionalLanguages.set(listOf("en-Qabs"))
|
additionalLanguages.set(listOf("en-Qabs"))
|
||||||
outputFile.set(generatedDir.map { it.file("languages.json") })
|
outputFile.set(generatedDir.map { it.file("languages.json") })
|
||||||
}
|
}
|
||||||
|
|
||||||
val createLocaleNamesResourceBundle =
|
val createLocaleNamesResourceBundle by tasks.registering(CreateLocaleNamesResourceBundle::class) {
|
||||||
tasks.register<CreateLocaleNamesResourceBundle>("createLocaleNamesResourceBundle") {
|
dependsOn(createLanguageList)
|
||||||
dependsOn(createLanguageList)
|
|
||||||
|
|
||||||
languagesFile.set(createLanguageList.flatMap { it.outputFile })
|
languagesFile.set(createLanguageList.flatMap { it.outputFile })
|
||||||
outputDirectory.set(generatedDir.map { it.dir("generated/LocaleNames") })
|
outputDirectory.set(generatedDir.map { it.dir("generated/LocaleNames") })
|
||||||
}
|
}
|
||||||
|
|
||||||
// mcmod data
|
// mcmod data
|
||||||
|
|
||||||
tasks.register<FormatJsonFile>("formatModJson") {
|
tasks.register<ParseModDataTask>("parseModData") {
|
||||||
file.set(layout.projectDirectory.file("mod.json"))
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register<FormatJsonFile>("formatModpackJson") {
|
|
||||||
file.set(layout.projectDirectory.file("modpack.json"))
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register<UpdateAddonProjectInfos>("updateModProjectInfos") {
|
|
||||||
inputFile.set(layout.projectDirectory.file("mod.json"))
|
inputFile.set(layout.projectDirectory.file("mod.json"))
|
||||||
outputFile.set(layout.projectDirectory.file("src/main/resources/assets/addon/mod.csv"))
|
outputFile.set(layout.projectDirectory.file("src/main/resources/assets/mod_data.txt"))
|
||||||
projectType.set(UpdateAddonProjectInfos.AddonProjectType.MOD)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register<UpdateAddonProjectInfos>("updateModpackProjectInfos") {
|
tasks.register<ParseModDataTask>("parseModPackData") {
|
||||||
inputFile.set(layout.projectDirectory.file("modpack.json"))
|
inputFile.set(layout.projectDirectory.file("modpack.json"))
|
||||||
outputFile.set(layout.projectDirectory.file("src/main/resources/assets/addon/modpack.csv"))
|
outputFile.set(layout.projectDirectory.file("src/main/resources/assets/modpack_data.txt"))
|
||||||
projectType.set(UpdateAddonProjectInfos.AddonProjectType.MODPACK)
|
|
||||||
}
|
}
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 10 KiB |
@@ -1,129 +0,0 @@
|
|||||||
//
|
|
||||||
// Source code recreated from a .class file by IntelliJ IDEA
|
|
||||||
// (powered by Fernflower decompiler)
|
|
||||||
//
|
|
||||||
|
|
||||||
package com.jfoenix.controls;
|
|
||||||
|
|
||||||
import com.jfoenix.converters.ButtonTypeConverter;
|
|
||||||
import com.jfoenix.skins.JFXButtonSkin;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import javafx.beans.property.ObjectProperty;
|
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
|
||||||
import javafx.css.CssMetaData;
|
|
||||||
import javafx.css.SimpleStyleableObjectProperty;
|
|
||||||
import javafx.css.Styleable;
|
|
||||||
import javafx.css.StyleableObjectProperty;
|
|
||||||
import javafx.css.StyleableProperty;
|
|
||||||
import javafx.scene.Node;
|
|
||||||
import javafx.scene.control.Button;
|
|
||||||
import javafx.scene.control.Control;
|
|
||||||
import javafx.scene.control.Labeled;
|
|
||||||
import javafx.scene.control.Skin;
|
|
||||||
import javafx.scene.paint.Paint;
|
|
||||||
|
|
||||||
public class JFXButton extends Button {
|
|
||||||
private static final String DEFAULT_STYLE_CLASS = "jfx-button";
|
|
||||||
|
|
||||||
private List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
|
|
||||||
|
|
||||||
public JFXButton() {
|
|
||||||
this.initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
public JFXButton(String text) {
|
|
||||||
super(text);
|
|
||||||
this.initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
public JFXButton(String text, Node graphic) {
|
|
||||||
super(text, graphic);
|
|
||||||
this.initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initialize() {
|
|
||||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Skin<?> createDefaultSkin() {
|
|
||||||
return new JFXButtonSkin(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private final ObjectProperty<Paint> ripplerFill = new SimpleObjectProperty<>(this, "ripplerFill", null);
|
|
||||||
|
|
||||||
public final ObjectProperty<Paint> ripplerFillProperty() {
|
|
||||||
return this.ripplerFill;
|
|
||||||
}
|
|
||||||
|
|
||||||
public final Paint getRipplerFill() {
|
|
||||||
return this.ripplerFillProperty().get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public final void setRipplerFill(Paint ripplerFill) {
|
|
||||||
this.ripplerFillProperty().set(ripplerFill);
|
|
||||||
}
|
|
||||||
|
|
||||||
private final StyleableObjectProperty<ButtonType> buttonType = new SimpleStyleableObjectProperty<>(
|
|
||||||
JFXButton.StyleableProperties.BUTTON_TYPE, this, "buttonType", JFXButton.ButtonType.FLAT);
|
|
||||||
|
|
||||||
public ButtonType getButtonType() {
|
|
||||||
return this.buttonType == null ? JFXButton.ButtonType.FLAT : this.buttonType.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public StyleableObjectProperty<ButtonType> buttonTypeProperty() {
|
|
||||||
return this.buttonType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setButtonType(ButtonType type) {
|
|
||||||
this.buttonType.set(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
|
|
||||||
if (this.STYLEABLES == null) {
|
|
||||||
List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Control.getClassCssMetaData());
|
|
||||||
styleables.addAll(getClassCssMetaData());
|
|
||||||
styleables.addAll(Labeled.getClassCssMetaData());
|
|
||||||
this.STYLEABLES = List.copyOf(styleables);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.STYLEABLES;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
|
|
||||||
return JFXButton.StyleableProperties.CHILD_STYLEABLES;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void layoutChildren() {
|
|
||||||
super.layoutChildren();
|
|
||||||
this.setNeedsLayout(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum ButtonType {
|
|
||||||
FLAT,
|
|
||||||
RAISED;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class StyleableProperties {
|
|
||||||
private static final CssMetaData<JFXButton, ButtonType> BUTTON_TYPE;
|
|
||||||
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
|
|
||||||
|
|
||||||
static {
|
|
||||||
BUTTON_TYPE = new CssMetaData<>("-jfx-button-type", ButtonTypeConverter.getInstance(), JFXButton.ButtonType.FLAT) {
|
|
||||||
public boolean isSettable(JFXButton control) {
|
|
||||||
return control.buttonType == null || !control.buttonType.isBound();
|
|
||||||
}
|
|
||||||
|
|
||||||
public StyleableProperty<ButtonType> getStyleableProperty(JFXButton control) {
|
|
||||||
return control.buttonTypeProperty();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Control.getClassCssMetaData());
|
|
||||||
Collections.addAll(styleables, BUTTON_TYPE);
|
|
||||||
CHILD_STYLEABLES = List.copyOf(styleables);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.jfoenix.controls;
|
|
||||||
|
|
||||||
import com.jfoenix.skins.JFXCheckBoxSkin;
|
|
||||||
import javafx.css.*;
|
|
||||||
import javafx.css.converter.PaintConverter;
|
|
||||||
import javafx.scene.control.CheckBox;
|
|
||||||
import javafx.scene.control.Skin;
|
|
||||||
import javafx.scene.paint.Color;
|
|
||||||
import javafx.scene.paint.Paint;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/// JFXCheckBox is the material design implementation of a checkbox.
|
|
||||||
/// it shows ripple effect and a custom selection animation.
|
|
||||||
///
|
|
||||||
/// @author Shadi Shaheen
|
|
||||||
/// @version 1.0
|
|
||||||
/// @since 2016-03-09
|
|
||||||
public class JFXCheckBox extends CheckBox {
|
|
||||||
|
|
||||||
public JFXCheckBox(String text) {
|
|
||||||
super(text);
|
|
||||||
initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
public JFXCheckBox() {
|
|
||||||
initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initialize() {
|
|
||||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Skin<?> createDefaultSkin() {
|
|
||||||
return new JFXCheckBoxSkin(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/***************************************************************************
|
|
||||||
* *
|
|
||||||
* Stylesheet Handling *
|
|
||||||
* *
|
|
||||||
**************************************************************************/
|
|
||||||
|
|
||||||
/// Initialize the style class to 'jfx-check-box'.
|
|
||||||
///
|
|
||||||
/// This is the selector class from which CSS can be used to style
|
|
||||||
/// this control.
|
|
||||||
private static final String DEFAULT_STYLE_CLASS = "jfx-check-box";
|
|
||||||
|
|
||||||
/// checkbox color property when selected
|
|
||||||
private StyleableObjectProperty<Paint> checkedColor;
|
|
||||||
|
|
||||||
private static final Color DEFAULT_CHECKED_COLOR = Color.valueOf("#0F9D58");
|
|
||||||
|
|
||||||
public StyleableObjectProperty<Paint> checkedColorProperty() {
|
|
||||||
if (checkedColor == null) {
|
|
||||||
checkedColor = new SimpleStyleableObjectProperty<>(StyleableProperties.CHECKED_COLOR,
|
|
||||||
JFXCheckBox.this,
|
|
||||||
"checkedColor",
|
|
||||||
DEFAULT_CHECKED_COLOR);
|
|
||||||
}
|
|
||||||
return this.checkedColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Paint getCheckedColor() {
|
|
||||||
return checkedColor == null ? DEFAULT_CHECKED_COLOR : checkedColor.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCheckedColor(Paint color) {
|
|
||||||
this.checkedColor.set(color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* checkbox color property when not selected
|
|
||||||
*/
|
|
||||||
private StyleableObjectProperty<Paint> unCheckedColor;
|
|
||||||
|
|
||||||
private static final Color DEFAULT_UNCHECKED_COLOR = Color.valueOf("#5A5A5A");
|
|
||||||
|
|
||||||
public StyleableObjectProperty<Paint> unCheckedColorProperty() {
|
|
||||||
if (unCheckedColor == null) {
|
|
||||||
unCheckedColor = new SimpleStyleableObjectProperty<>(StyleableProperties.UNCHECKED_COLOR,
|
|
||||||
JFXCheckBox.this,
|
|
||||||
"unCheckedColor",
|
|
||||||
DEFAULT_UNCHECKED_COLOR);
|
|
||||||
}
|
|
||||||
return this.unCheckedColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Paint getUnCheckedColor() {
|
|
||||||
return unCheckedColor == null ? DEFAULT_UNCHECKED_COLOR : unCheckedColor.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setUnCheckedColor(Paint color) {
|
|
||||||
this.unCheckedColor.set(color);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class StyleableProperties {
|
|
||||||
private static final CssMetaData<JFXCheckBox, Paint> CHECKED_COLOR =
|
|
||||||
new CssMetaData<>("-jfx-checked-color",
|
|
||||||
PaintConverter.getInstance(), DEFAULT_CHECKED_COLOR) {
|
|
||||||
@Override
|
|
||||||
public boolean isSettable(JFXCheckBox control) {
|
|
||||||
return control.checkedColor == null || !control.checkedColor.isBound();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public StyleableProperty<Paint> getStyleableProperty(JFXCheckBox control) {
|
|
||||||
return control.checkedColorProperty();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
private static final CssMetaData<JFXCheckBox, Paint> UNCHECKED_COLOR =
|
|
||||||
new CssMetaData<>("-jfx-unchecked-color",
|
|
||||||
PaintConverter.getInstance(), DEFAULT_UNCHECKED_COLOR) {
|
|
||||||
@Override
|
|
||||||
public boolean isSettable(JFXCheckBox control) {
|
|
||||||
return control.unCheckedColor == null || !control.unCheckedColor.isBound();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public StyleableProperty<Paint> getStyleableProperty(JFXCheckBox control) {
|
|
||||||
return control.unCheckedColorProperty();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
|
|
||||||
|
|
||||||
static {
|
|
||||||
final List<CssMetaData<? extends Styleable, ?>> styleables =
|
|
||||||
new ArrayList<>(CheckBox.getClassCssMetaData());
|
|
||||||
Collections.addAll(styleables,
|
|
||||||
CHECKED_COLOR,
|
|
||||||
UNCHECKED_COLOR
|
|
||||||
);
|
|
||||||
CHILD_STYLEABLES = List.copyOf(styleables);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
|
|
||||||
return getClassCssMetaData();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
|
|
||||||
return StyleableProperties.CHILD_STYLEABLES;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
|
||||||
package com.jfoenix.controls;
|
|
||||||
|
|
||||||
import com.jfoenix.skins.JFXCheckBoxSkin;
|
|
||||||
import javafx.geometry.Pos;
|
|
||||||
import javafx.scene.Node;
|
|
||||||
import javafx.scene.control.CheckBoxTreeItem;
|
|
||||||
import javafx.scene.layout.HBox;
|
|
||||||
import org.jetbrains.annotations.NotNullByDefault;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
/// Displays a [CheckBoxTreeItem] with a material-design checkbox.
|
|
||||||
///
|
|
||||||
/// The checkbox's selected and indeterminate states are bidirectionally bound to
|
|
||||||
/// the current tree item. Reusing or clearing the cell removes the old bindings.
|
|
||||||
/// Installing a checkbox skin or switching tree items applies the selection state
|
|
||||||
/// immediately, without playing a selection animation.
|
|
||||||
/// Ordinary tree items retain the display provided by [JFXTreeCell].
|
|
||||||
/// Empty cells and `null` values display neither content nor a checkbox.
|
|
||||||
///
|
|
||||||
/// @param <T> the type of value stored in each tree item
|
|
||||||
@NotNullByDefault
|
|
||||||
public class JFXCheckTreeCell<T> extends JFXTreeCell<T> {
|
|
||||||
|
|
||||||
/// The checkbox bound to the currently displayed checkable tree item.
|
|
||||||
private final JFXCheckBox checkBox = new JFXCheckBox();
|
|
||||||
|
|
||||||
/// Places the checkbox before the graphic supplied by the superclass.
|
|
||||||
private final HBox graphic = new HBox();
|
|
||||||
|
|
||||||
/// The tree item whose selection properties are currently bound, if any.
|
|
||||||
private @Nullable CheckBoxTreeItem<?> boundTreeItem;
|
|
||||||
|
|
||||||
/// Creates an empty cell whose checkbox toggles the current tree item's selection.
|
|
||||||
public JFXCheckTreeCell() {
|
|
||||||
checkBox.setAllowIndeterminate(false);
|
|
||||||
checkBox.setFocusTraversable(false);
|
|
||||||
graphic.setAlignment(Pos.CENTER_LEFT);
|
|
||||||
graphic.setPickOnBounds(false);
|
|
||||||
checkBox.skinProperty().addListener(observable -> finishSelectionAnimation());
|
|
||||||
treeItemProperty().addListener(observable -> updateDisplay(getItem(), isEmpty()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Finishes animations from the previous item and immediately displays the current state.
|
|
||||||
private void finishSelectionAnimation() {
|
|
||||||
if (checkBox.getSkin() instanceof JFXCheckBoxSkin skin) {
|
|
||||||
skin.finishSelectionAnimation();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds the checkbox to the inherited display and updates its selection bindings.
|
|
||||||
@Override
|
|
||||||
protected void updateDisplay(@Nullable T item, boolean empty) {
|
|
||||||
// Release graphics before the superclass reuses or reparents them.
|
|
||||||
graphic.getChildren().clear();
|
|
||||||
super.updateDisplay(item, empty);
|
|
||||||
|
|
||||||
@Nullable CheckBoxTreeItem<?> treeItem = !empty && item != null
|
|
||||||
&& getTreeItem() instanceof CheckBoxTreeItem<?> checkTreeItem ? checkTreeItem : null;
|
|
||||||
if (boundTreeItem != treeItem) {
|
|
||||||
if (boundTreeItem != null) {
|
|
||||||
checkBox.selectedProperty().unbindBidirectional(boundTreeItem.selectedProperty());
|
|
||||||
checkBox.indeterminateProperty().unbindBidirectional(boundTreeItem.indeterminateProperty());
|
|
||||||
}
|
|
||||||
boundTreeItem = treeItem;
|
|
||||||
if (treeItem != null) {
|
|
||||||
checkBox.selectedProperty().bindBidirectional(treeItem.selectedProperty());
|
|
||||||
checkBox.indeterminateProperty().bindBidirectional(treeItem.indeterminateProperty());
|
|
||||||
} else {
|
|
||||||
checkBox.setSelected(false);
|
|
||||||
checkBox.setIndeterminate(false);
|
|
||||||
}
|
|
||||||
finishSelectionAnimation();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (treeItem != null) {
|
|
||||||
@Nullable Node itemGraphic = getGraphic();
|
|
||||||
graphic.getChildren().add(checkBox);
|
|
||||||
if (itemGraphic != null) {
|
|
||||||
graphic.getChildren().add(itemGraphic);
|
|
||||||
}
|
|
||||||
setGraphic(graphic);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.jfoenix.controls;
|
|
||||||
|
|
||||||
import com.jfoenix.utils.JFXNodeUtils;
|
|
||||||
import javafx.geometry.Insets;
|
|
||||||
import javafx.scene.Node;
|
|
||||||
import javafx.scene.layout.*;
|
|
||||||
import javafx.scene.paint.Color;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JFXClippedPane is a StackPane that clips its content if exceeding the pane bounds.
|
|
||||||
*
|
|
||||||
* @author Shadi Shaheen
|
|
||||||
* @version 1.0
|
|
||||||
* @since 2018-06-02
|
|
||||||
*/
|
|
||||||
public class JFXClippedPane extends StackPane {
|
|
||||||
|
|
||||||
private final Region clip = new Region();
|
|
||||||
|
|
||||||
public JFXClippedPane() {
|
|
||||||
super();
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
|
|
||||||
public JFXClippedPane(Node... children) {
|
|
||||||
super(children);
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void init() {
|
|
||||||
setClip(clip);
|
|
||||||
clip.setBackground(new Background(new BackgroundFill(Color.BLACK, new CornerRadii(2), Insets.EMPTY)));
|
|
||||||
backgroundProperty().addListener(observable -> JFXNodeUtils.updateBackground(getBackground(), clip));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void layoutChildren() {
|
|
||||||
super.layoutChildren();
|
|
||||||
clip.resizeRelocate(0, 0, getWidth(), getHeight());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.jfoenix.controls;
|
|
||||||
|
|
||||||
import com.jfoenix.skins.JFXColorPickerSkin;
|
|
||||||
import javafx.css.CssMetaData;
|
|
||||||
import javafx.css.SimpleStyleableBooleanProperty;
|
|
||||||
import javafx.css.Styleable;
|
|
||||||
import javafx.css.StyleableBooleanProperty;
|
|
||||||
import javafx.css.converter.BooleanConverter;
|
|
||||||
import javafx.scene.control.ColorPicker;
|
|
||||||
import javafx.scene.control.Skin;
|
|
||||||
import javafx.scene.paint.Color;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JFXColorPicker is the metrial design implementation of color picker.
|
|
||||||
*
|
|
||||||
* @author Shadi Shaheen
|
|
||||||
* @version 1.0
|
|
||||||
* @since 2016-03-09
|
|
||||||
*/
|
|
||||||
public class JFXColorPicker extends ColorPicker {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
public JFXColorPicker() {
|
|
||||||
initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
public JFXColorPicker(Color color) {
|
|
||||||
super(color);
|
|
||||||
initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected Skin<?> createDefaultSkin() {
|
|
||||||
return new JFXColorPickerSkin(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initialize() {
|
|
||||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the style class to 'jfx-color-picker'.
|
|
||||||
* <p>
|
|
||||||
* This is the selector class from which CSS can be used to style
|
|
||||||
* this control.
|
|
||||||
*/
|
|
||||||
private static final String DEFAULT_STYLE_CLASS = "jfx-color-picker";
|
|
||||||
|
|
||||||
private double[] preDefinedColors = null;
|
|
||||||
|
|
||||||
public double[] getPreDefinedColors() {
|
|
||||||
return preDefinedColors;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPreDefinedColors(double[] preDefinedColors) {
|
|
||||||
this.preDefinedColors = preDefinedColors;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* disable animation on button action
|
|
||||||
*/
|
|
||||||
private final StyleableBooleanProperty disableAnimation = new SimpleStyleableBooleanProperty(StyleableProperties.DISABLE_ANIMATION,
|
|
||||||
JFXColorPicker.this,
|
|
||||||
"disableAnimation",
|
|
||||||
false);
|
|
||||||
|
|
||||||
public final StyleableBooleanProperty disableAnimationProperty() {
|
|
||||||
return this.disableAnimation;
|
|
||||||
}
|
|
||||||
|
|
||||||
public final Boolean isDisableAnimation() {
|
|
||||||
return disableAnimation != null && this.disableAnimationProperty().get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public final void setDisableAnimation(final Boolean disabled) {
|
|
||||||
this.disableAnimationProperty().set(disabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class StyleableProperties {
|
|
||||||
|
|
||||||
private static final CssMetaData<JFXColorPicker, Boolean> DISABLE_ANIMATION =
|
|
||||||
new CssMetaData<JFXColorPicker, Boolean>("-jfx-disable-animation",
|
|
||||||
BooleanConverter.getInstance(), false) {
|
|
||||||
@Override
|
|
||||||
public boolean isSettable(JFXColorPicker control) {
|
|
||||||
return control.disableAnimation == null || !control.disableAnimation.isBound();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public StyleableBooleanProperty getStyleableProperty(JFXColorPicker control) {
|
|
||||||
return control.disableAnimationProperty();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
|
|
||||||
|
|
||||||
static {
|
|
||||||
final List<CssMetaData<? extends Styleable, ?>> styleables =
|
|
||||||
new ArrayList<>(ColorPicker.getClassCssMetaData());
|
|
||||||
Collections.addAll(styleables, DISABLE_ANIMATION);
|
|
||||||
CHILD_STYLEABLES = Collections.unmodifiableList(styleables);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
|
|
||||||
return getClassCssMetaData();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
|
|
||||||
return StyleableProperties.CHILD_STYLEABLES;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,357 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.jfoenix.controls;
|
|
||||||
|
|
||||||
import com.jfoenix.converters.base.NodeConverter;
|
|
||||||
import com.jfoenix.skins.JFXComboBoxListViewSkin;
|
|
||||||
import javafx.beans.property.ObjectProperty;
|
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
|
||||||
import javafx.collections.ObservableList;
|
|
||||||
import javafx.css.*;
|
|
||||||
import javafx.css.converter.BooleanConverter;
|
|
||||||
import javafx.css.converter.PaintConverter;
|
|
||||||
import javafx.geometry.Insets;
|
|
||||||
import javafx.geometry.Pos;
|
|
||||||
import javafx.scene.Node;
|
|
||||||
import javafx.scene.control.*;
|
|
||||||
import javafx.scene.layout.Background;
|
|
||||||
import javafx.scene.layout.BackgroundFill;
|
|
||||||
import javafx.scene.layout.StackPane;
|
|
||||||
import javafx.scene.paint.Color;
|
|
||||||
import javafx.scene.paint.Paint;
|
|
||||||
import javafx.util.StringConverter;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.ui.FXUtils.useJFXContextMenu;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JFXComboBox is the material design implementation of a combobox.
|
|
||||||
*
|
|
||||||
* @author Shadi Shaheen
|
|
||||||
* @version 1.0
|
|
||||||
* @since 2016-03-09
|
|
||||||
*/
|
|
||||||
public class JFXComboBox<T> extends ComboBox<T> {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
public JFXComboBox() {
|
|
||||||
initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
public JFXComboBox(ObservableList<T> items) {
|
|
||||||
super(items);
|
|
||||||
initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initialize() {
|
|
||||||
getStyleClass().add(DEFAULT_STYLE_CLASS);
|
|
||||||
this.setCellFactory(listView -> new JFXListCell<>() {
|
|
||||||
@Override
|
|
||||||
public void updateItem(T item, boolean empty) {
|
|
||||||
super.updateItem(item, empty);
|
|
||||||
updateDisplayText(this, item, empty);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// had to refactor the code out of the skin class to allow
|
|
||||||
// customization of the button cell
|
|
||||||
this.setButtonCell(new ListCell<>() {
|
|
||||||
{
|
|
||||||
// fixed clearing the combo box value is causing
|
|
||||||
// java prompt text to be shown because the button cell is not updated
|
|
||||||
JFXComboBox.this.valueProperty().addListener(observable -> {
|
|
||||||
if (JFXComboBox.this.getValue() == null) {
|
|
||||||
updateItem(null, true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void updateItem(T item, boolean empty) {
|
|
||||||
updateDisplayText(this, item, empty);
|
|
||||||
this.setVisible(item != null || !empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
useJFXContextMenu(editorProperty().get());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected Skin<?> createDefaultSkin() {
|
|
||||||
return new JFXComboBoxListViewSkin<T>(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the style class to 'jfx-combo-box'.
|
|
||||||
* <p>
|
|
||||||
* This is the selector class from which CSS can be used to style
|
|
||||||
* this control.
|
|
||||||
*/
|
|
||||||
private static final String DEFAULT_STYLE_CLASS = "jfx-combo-box";
|
|
||||||
|
|
||||||
/***************************************************************************
|
|
||||||
* *
|
|
||||||
* Node Converter Property *
|
|
||||||
* *
|
|
||||||
**************************************************************************/
|
|
||||||
/**
|
|
||||||
* Converts the user-typed input (when the ComboBox is
|
|
||||||
* {@link #editableProperty() editable}) to an object of type T, such that
|
|
||||||
* the input may be retrieved via the {@link #valueProperty() value} property.
|
|
||||||
*/
|
|
||||||
public ObjectProperty<NodeConverter<T>> nodeConverterProperty() {
|
|
||||||
return nodeConverter;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ObjectProperty<NodeConverter<T>> nodeConverter = new SimpleObjectProperty<>(this, "nodeConverter",
|
|
||||||
JFXComboBox.<T>defaultNodeConverter());
|
|
||||||
|
|
||||||
public final void setNodeConverter(NodeConverter<T> value) {
|
|
||||||
nodeConverterProperty().set(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public final NodeConverter<T> getNodeConverter() {
|
|
||||||
return nodeConverterProperty().get();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static <T> NodeConverter<T> defaultNodeConverter() {
|
|
||||||
return new NodeConverter<T>() {
|
|
||||||
@Override
|
|
||||||
public Node toNode(T object) {
|
|
||||||
if (object == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
StackPane selectedValueContainer = new StackPane();
|
|
||||||
selectedValueContainer.getStyleClass().add("combo-box-selected-value-container");
|
|
||||||
selectedValueContainer.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, null, null)));
|
|
||||||
Label selectedValueLabel = object instanceof Label ? new Label(((Label) object).getText()) : new Label(
|
|
||||||
object.toString());
|
|
||||||
selectedValueLabel.setTextFill(Color.BLACK);
|
|
||||||
selectedValueContainer.getChildren().add(selectedValueLabel);
|
|
||||||
StackPane.setAlignment(selectedValueLabel, Pos.CENTER_LEFT);
|
|
||||||
StackPane.setMargin(selectedValueLabel, new Insets(0, 0, 0, 5));
|
|
||||||
return selectedValueContainer;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
@Override
|
|
||||||
public T fromNode(Node node) {
|
|
||||||
return (T) node;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString(T object) {
|
|
||||||
if (object == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (object instanceof Label) {
|
|
||||||
return ((Label) object).getText();
|
|
||||||
}
|
|
||||||
return object.toString();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean updateDisplayText(ListCell<T> cell, T item, boolean empty) {
|
|
||||||
if (empty) {
|
|
||||||
// create empty cell
|
|
||||||
if (cell == null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
cell.setGraphic(null);
|
|
||||||
cell.setText(null);
|
|
||||||
return true;
|
|
||||||
} else if (item instanceof Node) {
|
|
||||||
Node currentNode = cell.getGraphic();
|
|
||||||
Node newNode = (Node) item;
|
|
||||||
// create a node from the selected node of the listview
|
|
||||||
// using JFXComboBox {@link #nodeConverterProperty() NodeConverter})
|
|
||||||
NodeConverter<T> nc = this.getNodeConverter();
|
|
||||||
Node node = nc == null ? null : nc.toNode(item);
|
|
||||||
if (currentNode == null || !currentNode.equals(newNode)) {
|
|
||||||
cell.setText(null);
|
|
||||||
cell.setGraphic(node == null ? newNode : node);
|
|
||||||
}
|
|
||||||
return node == null;
|
|
||||||
} else {
|
|
||||||
// run item through StringConverter if it isn't null
|
|
||||||
StringConverter<T> c = this.getConverter();
|
|
||||||
String s = item == null ? this.getPromptText() : (c == null ? item.toString() : c.toString(item));
|
|
||||||
cell.setText(s);
|
|
||||||
cell.setGraphic(null);
|
|
||||||
return s == null || s.isEmpty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/***************************************************************************
|
|
||||||
* *
|
|
||||||
* styleable Properties *
|
|
||||||
* *
|
|
||||||
**************************************************************************/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* set true to show a float the prompt text when focusing the field
|
|
||||||
*/
|
|
||||||
private StyleableBooleanProperty labelFloat = new SimpleStyleableBooleanProperty(StyleableProperties.LABEL_FLOAT,
|
|
||||||
JFXComboBox.this,
|
|
||||||
"lableFloat",
|
|
||||||
false);
|
|
||||||
|
|
||||||
public final StyleableBooleanProperty labelFloatProperty() {
|
|
||||||
return this.labelFloat;
|
|
||||||
}
|
|
||||||
|
|
||||||
public final boolean isLabelFloat() {
|
|
||||||
return this.labelFloatProperty().get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public final void setLabelFloat(final boolean labelFloat) {
|
|
||||||
this.labelFloatProperty().set(labelFloat);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* default color used when the field is unfocused
|
|
||||||
*/
|
|
||||||
private StyleableObjectProperty<Paint> unFocusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.UNFOCUS_COLOR,
|
|
||||||
JFXComboBox.this,
|
|
||||||
"unFocusColor",
|
|
||||||
Color.rgb(77,
|
|
||||||
77,
|
|
||||||
77));
|
|
||||||
|
|
||||||
public Paint getUnFocusColor() {
|
|
||||||
return unFocusColor == null ? Color.rgb(77, 77, 77) : unFocusColor.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public StyleableObjectProperty<Paint> unFocusColorProperty() {
|
|
||||||
return this.unFocusColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setUnFocusColor(Paint color) {
|
|
||||||
this.unFocusColor.set(color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* default color used when the field is focused
|
|
||||||
*/
|
|
||||||
private StyleableObjectProperty<Paint> focusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.FOCUS_COLOR,
|
|
||||||
JFXComboBox.this,
|
|
||||||
"focusColor",
|
|
||||||
Color.valueOf("#4059A9"));
|
|
||||||
|
|
||||||
public Paint getFocusColor() {
|
|
||||||
return focusColor == null ? Color.valueOf("#4059A9") : focusColor.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public StyleableObjectProperty<Paint> focusColorProperty() {
|
|
||||||
return this.focusColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setFocusColor(Paint color) {
|
|
||||||
this.focusColor.set(color);
|
|
||||||
}
|
|
||||||
|
|
||||||
private final static class StyleableProperties {
|
|
||||||
private static final CssMetaData<JFXComboBox<?>, Paint> UNFOCUS_COLOR = new CssMetaData<JFXComboBox<?>, Paint>(
|
|
||||||
"-jfx-unfocus-color",
|
|
||||||
PaintConverter.getInstance(),
|
|
||||||
Color.valueOf("#A6A6A6")) {
|
|
||||||
@Override
|
|
||||||
public boolean isSettable(JFXComboBox<?> control) {
|
|
||||||
return control.unFocusColor == null || !control.unFocusColor.isBound();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public StyleableProperty<Paint> getStyleableProperty(JFXComboBox<?> control) {
|
|
||||||
return control.unFocusColorProperty();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
private static final CssMetaData<JFXComboBox<?>, Paint> FOCUS_COLOR = new CssMetaData<JFXComboBox<?>, Paint>(
|
|
||||||
"-jfx-focus-color",
|
|
||||||
PaintConverter.getInstance(),
|
|
||||||
Color.valueOf("#3f51b5")) {
|
|
||||||
@Override
|
|
||||||
public boolean isSettable(JFXComboBox<?> control) {
|
|
||||||
return control.focusColor == null || !control.focusColor.isBound();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public StyleableProperty<Paint> getStyleableProperty(JFXComboBox<?> control) {
|
|
||||||
return control.focusColorProperty();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
private static final CssMetaData<JFXComboBox<?>, Boolean> LABEL_FLOAT = new CssMetaData<JFXComboBox<?>, Boolean>(
|
|
||||||
"-jfx-label-float",
|
|
||||||
BooleanConverter.getInstance(),
|
|
||||||
false) {
|
|
||||||
@Override
|
|
||||||
public boolean isSettable(JFXComboBox<?> control) {
|
|
||||||
return control.labelFloat == null || !control.labelFloat.isBound();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public StyleableBooleanProperty getStyleableProperty(JFXComboBox<?> control) {
|
|
||||||
return control.labelFloatProperty();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
|
|
||||||
|
|
||||||
static {
|
|
||||||
final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(
|
|
||||||
Control.getClassCssMetaData());
|
|
||||||
Collections.addAll(styleables, UNFOCUS_COLOR, FOCUS_COLOR, LABEL_FLOAT);
|
|
||||||
CHILD_STYLEABLES = Collections.unmodifiableList(styleables);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// inherit the styleable properties from parent
|
|
||||||
private List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
|
|
||||||
if (STYLEABLES == null) {
|
|
||||||
final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(
|
|
||||||
Control.getClassCssMetaData());
|
|
||||||
styleables.addAll(getClassCssMetaData());
|
|
||||||
styleables.addAll(Control.getClassCssMetaData());
|
|
||||||
STYLEABLES = Collections.unmodifiableList(styleables);
|
|
||||||
}
|
|
||||||
return STYLEABLES;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
|
|
||||||
return StyleableProperties.CHILD_STYLEABLES;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -23,13 +23,21 @@ import com.jfoenix.controls.events.JFXDialogEvent;
|
|||||||
import com.jfoenix.converters.DialogTransitionConverter;
|
import com.jfoenix.converters.DialogTransitionConverter;
|
||||||
import com.jfoenix.effects.JFXDepthManager;
|
import com.jfoenix.effects.JFXDepthManager;
|
||||||
import com.jfoenix.transitions.CachedTransition;
|
import com.jfoenix.transitions.CachedTransition;
|
||||||
import javafx.animation.*;
|
import javafx.animation.Interpolator;
|
||||||
|
import javafx.animation.KeyFrame;
|
||||||
|
import javafx.animation.KeyValue;
|
||||||
|
import javafx.animation.Timeline;
|
||||||
|
import javafx.animation.Transition;
|
||||||
import javafx.beans.DefaultProperty;
|
import javafx.beans.DefaultProperty;
|
||||||
import javafx.beans.property.BooleanProperty;
|
import javafx.beans.property.BooleanProperty;
|
||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.ObjectProperty;
|
||||||
import javafx.beans.property.ObjectPropertyBase;
|
import javafx.beans.property.ObjectPropertyBase;
|
||||||
import javafx.beans.property.SimpleBooleanProperty;
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
import javafx.css.*;
|
import javafx.css.CssMetaData;
|
||||||
|
import javafx.css.SimpleStyleableObjectProperty;
|
||||||
|
import javafx.css.Styleable;
|
||||||
|
import javafx.css.StyleableObjectProperty;
|
||||||
|
import javafx.css.StyleableProperty;
|
||||||
import javafx.event.Event;
|
import javafx.event.Event;
|
||||||
import javafx.event.EventHandler;
|
import javafx.event.EventHandler;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
@@ -41,6 +49,7 @@ import javafx.scene.image.WritableImage;
|
|||||||
import javafx.scene.input.MouseEvent;
|
import javafx.scene.input.MouseEvent;
|
||||||
import javafx.scene.layout.Background;
|
import javafx.scene.layout.Background;
|
||||||
import javafx.scene.layout.BackgroundFill;
|
import javafx.scene.layout.BackgroundFill;
|
||||||
|
import javafx.scene.layout.CornerRadii;
|
||||||
import javafx.scene.layout.Region;
|
import javafx.scene.layout.Region;
|
||||||
import javafx.scene.layout.StackPane;
|
import javafx.scene.layout.StackPane;
|
||||||
import javafx.scene.paint.Color;
|
import javafx.scene.paint.Color;
|
||||||
@@ -146,6 +155,7 @@ public class JFXDialog extends StackPane {
|
|||||||
});
|
});
|
||||||
|
|
||||||
contentHolder = new StackPane();
|
contentHolder = new StackPane();
|
||||||
|
contentHolder.setBackground(new Background(new BackgroundFill(Color.WHITE, new CornerRadii(2), null)));
|
||||||
JFXDepthManager.setDepth(contentHolder, 4);
|
JFXDepthManager.setDepth(contentHolder, 4);
|
||||||
contentHolder.setPickOnBounds(false);
|
contentHolder.setPickOnBounds(false);
|
||||||
// ensure stackpane is never resized beyond it's preferred size
|
// ensure stackpane is never resized beyond it's preferred size
|
||||||
@@ -371,14 +381,6 @@ public class JFXDialog extends StackPane {
|
|||||||
setCycleDuration(Duration.seconds(0.4));
|
setCycleDuration(Duration.seconds(0.4));
|
||||||
setDelay(Duration.ZERO);
|
setDelay(Duration.ZERO);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void starting() {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void stopping() {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
//
|
|
||||||
// Source code recreated from a .class file by IntelliJ IDEA
|
|
||||||
// (powered by Fernflower decompiler)
|
|
||||||
//
|
|
||||||
|
|
||||||
package com.jfoenix.controls;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import javafx.collections.ObservableList;
|
|
||||||
import javafx.geometry.Insets;
|
|
||||||
import javafx.geometry.Orientation;
|
|
||||||
import javafx.scene.Node;
|
|
||||||
import javafx.scene.layout.FlowPane;
|
|
||||||
import javafx.scene.layout.StackPane;
|
|
||||||
import javafx.scene.layout.VBox;
|
|
||||||
|
|
||||||
public class JFXDialogLayout extends StackPane {
|
|
||||||
private final StackPane heading = new StackPane();
|
|
||||||
private final StackPane body = new StackPane();
|
|
||||||
private final FlowPane actions = new FlowPane() {
|
|
||||||
protected double computeMinWidth(double height) {
|
|
||||||
if (this.getContentBias() == Orientation.HORIZONTAL) {
|
|
||||||
double maxPref = 0.0;
|
|
||||||
for (Node child : this.getChildren()) {
|
|
||||||
if (child.isManaged()) {
|
|
||||||
maxPref = Math.max(maxPref, child.minWidth(-1.0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Insets insets = this.getInsets();
|
|
||||||
return insets.getLeft() + this.snapSizeX(maxPref) + insets.getRight();
|
|
||||||
} else {
|
|
||||||
return this.computePrefWidth(height);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected double computeMinHeight(double width) {
|
|
||||||
if (this.getContentBias() == Orientation.VERTICAL) {
|
|
||||||
double maxPref = 0.0;
|
|
||||||
|
|
||||||
for (Node child : this.getChildren()) {
|
|
||||||
if (child.isManaged()) {
|
|
||||||
maxPref = Math.max(maxPref, child.minHeight(-1.0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Insets insets = this.getInsets();
|
|
||||||
return insets.getTop() + this.snapSizeY(maxPref) + insets.getBottom();
|
|
||||||
} else {
|
|
||||||
return this.computePrefHeight(width);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
private static final String DEFAULT_STYLE_CLASS = "jfx-dialog-layout";
|
|
||||||
|
|
||||||
public JFXDialogLayout() {
|
|
||||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
|
||||||
|
|
||||||
VBox layout = new VBox();
|
|
||||||
|
|
||||||
this.heading.getStyleClass().add("jfx-layout-heading");
|
|
||||||
this.heading.getStyleClass().add("title");
|
|
||||||
|
|
||||||
this.body.getStyleClass().add("jfx-layout-body");
|
|
||||||
this.body.prefHeightProperty().bind(this.prefHeightProperty());
|
|
||||||
this.body.prefWidthProperty().bind(this.prefWidthProperty());
|
|
||||||
|
|
||||||
this.actions.getStyleClass().add("jfx-layout-actions");
|
|
||||||
|
|
||||||
layout.getChildren().setAll(this.heading, this.body, this.actions);
|
|
||||||
|
|
||||||
this.getChildren().add(layout);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ObservableList<Node> getHeading() {
|
|
||||||
return this.heading.getChildren();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setHeading(Node... titleContent) {
|
|
||||||
this.heading.getChildren().setAll(titleContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ObservableList<Node> getBody() {
|
|
||||||
return this.body.getChildren();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setBody(Node... body) {
|
|
||||||
this.body.getChildren().setAll(body);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ObservableList<Node> getActions() {
|
|
||||||
return this.actions.getChildren();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setActions(Node... actions) {
|
|
||||||
this.actions.getChildren().setAll(actions);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setActions(List<? extends Node> actions) {
|
|
||||||
this.actions.getChildren().setAll(actions);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,274 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.jfoenix.controls;
|
|
||||||
|
|
||||||
import com.jfoenix.utils.JFXNodeUtils;
|
|
||||||
import javafx.animation.Interpolator;
|
|
||||||
import javafx.animation.KeyFrame;
|
|
||||||
import javafx.animation.KeyValue;
|
|
||||||
import javafx.animation.Timeline;
|
|
||||||
import javafx.application.Platform;
|
|
||||||
import javafx.geometry.Insets;
|
|
||||||
import javafx.scene.Node;
|
|
||||||
import javafx.scene.control.Label;
|
|
||||||
import javafx.scene.control.ListCell;
|
|
||||||
import javafx.scene.control.Tooltip;
|
|
||||||
import javafx.scene.layout.Region;
|
|
||||||
import javafx.scene.shape.Rectangle;
|
|
||||||
import javafx.scene.shape.Shape;
|
|
||||||
import javafx.util.Duration;
|
|
||||||
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/// material design implementation of ListCell
|
|
||||||
///
|
|
||||||
/// By default, JFXListCell will try to create a graphic node for the cell,
|
|
||||||
/// to override it you need to set graphic to null in [#updateItem(Object, boolean)] method.
|
|
||||||
///
|
|
||||||
/// NOTE: passive nodes (Labels and Shapes) will be set to mouse transparent in order to
|
|
||||||
/// show the ripple effect upon clicking , to change this behavior you can override the
|
|
||||||
/// method {[#makeChildrenTransparent()]
|
|
||||||
///
|
|
||||||
/// @author Shadi Shaheen
|
|
||||||
/// @version 1.0
|
|
||||||
/// @since 2016-03-09
|
|
||||||
public class JFXListCell<T> extends ListCell<T> {
|
|
||||||
|
|
||||||
protected JFXRippler cellRippler = new JFXRippler(this) {
|
|
||||||
@Override
|
|
||||||
protected Node getMask() {
|
|
||||||
Region clip = new Region();
|
|
||||||
JFXNodeUtils.updateBackground(JFXListCell.this.getBackground(), clip);
|
|
||||||
double width = control.getLayoutBounds().getWidth();
|
|
||||||
double height = control.getLayoutBounds().getHeight();
|
|
||||||
clip.resize(width, height);
|
|
||||||
return clip;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void positionControl(Node control) {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
protected Node cellContent;
|
|
||||||
private Rectangle clip;
|
|
||||||
|
|
||||||
private Timeline gapAnimation;
|
|
||||||
private boolean playExpandAnimation = false;
|
|
||||||
private boolean selectionChanged = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
public JFXListCell() {
|
|
||||||
initialize();
|
|
||||||
initListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* init listeners to update the vertical gap / selection animation
|
|
||||||
*/
|
|
||||||
private void initListeners() {
|
|
||||||
listViewProperty().addListener((listObj, oldList, newList) -> {
|
|
||||||
if (newList instanceof JFXListView<?> listView) {
|
|
||||||
listView.currentVerticalGapProperty().addListener((o, oldVal, newVal) -> {
|
|
||||||
cellRippler.rippler.setClip(null);
|
|
||||||
if (newVal.doubleValue() != 0) {
|
|
||||||
playExpandAnimation = true;
|
|
||||||
getListView().requestLayout();
|
|
||||||
} else {
|
|
||||||
// fake expand state
|
|
||||||
double gap = clip.getY() * 2;
|
|
||||||
gapAnimation = new Timeline(
|
|
||||||
new KeyFrame(Duration.millis(240),
|
|
||||||
new KeyValue(this.translateYProperty(),
|
|
||||||
-gap / 2 - (gap * (getIndex())),
|
|
||||||
Interpolator.EASE_BOTH)
|
|
||||||
));
|
|
||||||
gapAnimation.play();
|
|
||||||
gapAnimation.setOnFinished((finish) -> {
|
|
||||||
requestLayout();
|
|
||||||
Platform.runLater(() -> getListView().requestLayout());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
selectedProperty().addListener((o, oldVal, newVal) -> {
|
|
||||||
if (newVal) {
|
|
||||||
selectionChanged = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void layoutChildren() {
|
|
||||||
super.layoutChildren();
|
|
||||||
cellRippler.resizeRelocate(0, 0, getWidth(), getHeight());
|
|
||||||
double gap = getGap();
|
|
||||||
|
|
||||||
if (clip == null) {
|
|
||||||
clip = new Rectangle(0, gap / 2, getWidth(), getHeight() - gap);
|
|
||||||
setClip(clip);
|
|
||||||
} else {
|
|
||||||
if (gap != 0) {
|
|
||||||
if (playExpandAnimation || selectionChanged) {
|
|
||||||
// fake list collapse state
|
|
||||||
if (playExpandAnimation) {
|
|
||||||
this.setTranslateY(-gap / 2 + (-gap * (getIndex())));
|
|
||||||
clip.setY(gap / 2);
|
|
||||||
clip.setHeight(getHeight() - gap);
|
|
||||||
gapAnimation = new Timeline(new KeyFrame(Duration.millis(240),
|
|
||||||
new KeyValue(this.translateYProperty(),
|
|
||||||
0,
|
|
||||||
Interpolator.EASE_BOTH)));
|
|
||||||
playExpandAnimation = false;
|
|
||||||
} else if (selectionChanged) {
|
|
||||||
clip.setY(0);
|
|
||||||
clip.setHeight(getHeight());
|
|
||||||
gapAnimation = new Timeline(
|
|
||||||
new KeyFrame(Duration.millis(240),
|
|
||||||
new KeyValue(clip.yProperty(), gap / 2, Interpolator.EASE_BOTH),
|
|
||||||
new KeyValue(clip.heightProperty(), getHeight() - gap, Interpolator.EASE_BOTH)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
playExpandAnimation = false;
|
|
||||||
selectionChanged = false;
|
|
||||||
gapAnimation.play();
|
|
||||||
} else {
|
|
||||||
if (gapAnimation != null) {
|
|
||||||
gapAnimation.stop();
|
|
||||||
}
|
|
||||||
this.setTranslateY(0);
|
|
||||||
clip.setY(gap / 2);
|
|
||||||
clip.setHeight(getHeight() - gap);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.setTranslateY(0);
|
|
||||||
clip.setY(0);
|
|
||||||
clip.setHeight(getHeight());
|
|
||||||
}
|
|
||||||
clip.setX(0);
|
|
||||||
clip.setWidth(getWidth());
|
|
||||||
}
|
|
||||||
if (!getChildren().contains(cellRippler)) {
|
|
||||||
makeChildrenTransparent();
|
|
||||||
getChildren().add(0, cellRippler);
|
|
||||||
cellRippler.rippler.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* this method is used to set some nodes in cell content as mouse transparent nodes
|
|
||||||
* so clicking on them will trigger the ripple effect.
|
|
||||||
*/
|
|
||||||
protected void makeChildrenTransparent() {
|
|
||||||
for (Node child : getChildren()) {
|
|
||||||
if (child instanceof Label) {
|
|
||||||
Set<Node> texts = child.lookupAll("Text");
|
|
||||||
for (Node text : texts) {
|
|
||||||
text.setMouseTransparent(true);
|
|
||||||
}
|
|
||||||
} else if (child instanceof Shape) {
|
|
||||||
child.setMouseTransparent(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected void updateItem(T item, boolean empty) {
|
|
||||||
T oldItem = getItem();
|
|
||||||
boolean oldEmpty = isEmpty();
|
|
||||||
if (oldItem != item || oldEmpty != empty) cellRippler.releaseRippleImmediately();
|
|
||||||
super.updateItem(item, empty);
|
|
||||||
if (empty) {
|
|
||||||
setText(null);
|
|
||||||
setGraphic(null);
|
|
||||||
// remove empty (Trailing cells)
|
|
||||||
setMouseTransparent(true);
|
|
||||||
setStyle("-fx-background-color:TRANSPARENT;");
|
|
||||||
} else {
|
|
||||||
setMouseTransparent(false);
|
|
||||||
setStyle(null);
|
|
||||||
if (item instanceof Node newNode) {
|
|
||||||
setText(null);
|
|
||||||
Node currentNode = getGraphic();
|
|
||||||
if (currentNode == null || !currentNode.equals(newNode)) {
|
|
||||||
cellContent = newNode;
|
|
||||||
cellRippler.rippler.cacheRippleClip(false);
|
|
||||||
// build the Cell node
|
|
||||||
// RIPPLER ITEM : in case if the list item has its own rippler bind the list rippler and item rippler properties
|
|
||||||
if (newNode instanceof JFXRippler newRippler) {
|
|
||||||
// build cell container from exisiting rippler
|
|
||||||
cellRippler.ripplerFillProperty().bind(newRippler.ripplerFillProperty());
|
|
||||||
cellRippler.maskTypeProperty().bind(newRippler.maskTypeProperty());
|
|
||||||
cellRippler.positionProperty().bind(newRippler.positionProperty());
|
|
||||||
cellContent = newRippler.getControl();
|
|
||||||
}
|
|
||||||
((Region) cellContent).setMaxHeight(cellContent.prefHeight(-1));
|
|
||||||
setGraphic(cellContent);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setText(item == null ? "null" : item.toString());
|
|
||||||
setGraphic(null);
|
|
||||||
}
|
|
||||||
// show cell tooltip if it's toggled in JFXListView
|
|
||||||
if (getListView() instanceof JFXListView<?> listView && listView.isShowTooltip()) {
|
|
||||||
if (item instanceof Label label) {
|
|
||||||
setTooltip(new Tooltip(label.getText()));
|
|
||||||
} else if (getText() != null) {
|
|
||||||
setTooltip(new Tooltip(getText()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stylesheet Handling *
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the style class to 'jfx-list-cell'.
|
|
||||||
* <p>
|
|
||||||
* This is the selector class from which CSS can be used to style
|
|
||||||
* this control.
|
|
||||||
*/
|
|
||||||
private static final String DEFAULT_STYLE_CLASS = "jfx-list-cell";
|
|
||||||
|
|
||||||
private void initialize() {
|
|
||||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
|
||||||
this.setPadding(new Insets(8, 12, 8, 12));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected double computePrefHeight(double width) {
|
|
||||||
double gap = getGap();
|
|
||||||
return super.computePrefHeight(width) + gap;
|
|
||||||
}
|
|
||||||
|
|
||||||
private double getGap() {
|
|
||||||
return (getListView() instanceof JFXListView<?> listView)
|
|
||||||
? (listView.isExpanded() ? listView.currentVerticalGapProperty().get() : 0)
|
|
||||||
: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,258 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.jfoenix.controls;
|
|
||||||
|
|
||||||
import com.jfoenix.skins.JFXListViewSkin;
|
|
||||||
import javafx.beans.property.*;
|
|
||||||
import javafx.css.*;
|
|
||||||
import javafx.css.converter.BooleanConverter;
|
|
||||||
import javafx.css.converter.SizeConverter;
|
|
||||||
import javafx.scene.control.ListView;
|
|
||||||
import javafx.scene.control.Skin;
|
|
||||||
import javafx.scene.input.MouseEvent;
|
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
/// Material design implementation of List View
|
|
||||||
///
|
|
||||||
/// @author Shadi Shaheen
|
|
||||||
/// @version 1.0
|
|
||||||
/// @since 2016-03-09
|
|
||||||
public class JFXListView<T> extends ListView<T> {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
public JFXListView() {
|
|
||||||
this.setCellFactory(listView -> new JFXListCell<>());
|
|
||||||
initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected Skin<?> createDefaultSkin() {
|
|
||||||
return new JFXListViewSkin<>(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private IntegerProperty depth;
|
|
||||||
|
|
||||||
public IntegerProperty depthProperty() {
|
|
||||||
if (depth == null) {
|
|
||||||
depth = new SimpleIntegerProperty(this, "depth", 0);
|
|
||||||
}
|
|
||||||
return depth;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getDepth() {
|
|
||||||
return depth != null ? depth.get() : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDepth(int depth) {
|
|
||||||
depthProperty().set(depth);
|
|
||||||
}
|
|
||||||
|
|
||||||
private DoubleProperty currentVerticalGap;
|
|
||||||
|
|
||||||
DoubleProperty currentVerticalGapProperty() {
|
|
||||||
if (currentVerticalGap == null) {
|
|
||||||
currentVerticalGap = new SimpleDoubleProperty(this, "currentVerticalGap");
|
|
||||||
}
|
|
||||||
return currentVerticalGap;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateVerticalGap() {
|
|
||||||
if (isExpanded()) {
|
|
||||||
currentVerticalGapProperty().set(verticalGap.get());
|
|
||||||
} else {
|
|
||||||
currentVerticalGapProperty().set(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* this only works if the items were labels / strings
|
|
||||||
*/
|
|
||||||
private BooleanProperty showTooltip;
|
|
||||||
|
|
||||||
public final BooleanProperty showTooltipProperty() {
|
|
||||||
if (showTooltip == null) {
|
|
||||||
showTooltip = new SimpleBooleanProperty(this, "showTooltip", false);
|
|
||||||
}
|
|
||||||
return this.showTooltip;
|
|
||||||
}
|
|
||||||
|
|
||||||
public final boolean isShowTooltip() {
|
|
||||||
return showTooltip != null && showTooltip.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public final void setShowTooltip(final boolean showTooltip) {
|
|
||||||
this.showTooltipProperty().set(showTooltip);
|
|
||||||
}
|
|
||||||
|
|
||||||
/***************************************************************************
|
|
||||||
* *
|
|
||||||
* Stylesheet Handling *
|
|
||||||
* *
|
|
||||||
**************************************************************************/
|
|
||||||
|
|
||||||
/// Initialize the style class to 'jfx-list-view'.
|
|
||||||
///
|
|
||||||
/// This is the selector class from which CSS can be used to style
|
|
||||||
/// this control.
|
|
||||||
private static final String DEFAULT_STYLE_CLASS = "jfx-list-view";
|
|
||||||
|
|
||||||
private void initialize() {
|
|
||||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* propagate mouse events to the parent node ( e.g. to allow dragging while clicking on the list)
|
|
||||||
*/
|
|
||||||
public void propagateMouseEventsToParent() {
|
|
||||||
this.addEventHandler(MouseEvent.ANY, e -> {
|
|
||||||
e.consume();
|
|
||||||
this.getParent().fireEvent(e);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private StyleableDoubleProperty verticalGap;
|
|
||||||
|
|
||||||
public StyleableDoubleProperty verticalGapProperty() {
|
|
||||||
if (this.verticalGap == null) {
|
|
||||||
this.verticalGap = new StyleableDoubleProperty(0.0) {
|
|
||||||
@Override
|
|
||||||
public Object getBean() {
|
|
||||||
return JFXListView.this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return "verticalGap";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CssMetaData<? extends Styleable, Number> getCssMetaData() {
|
|
||||||
return StyleableProperties.VERTICAL_GAP;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void invalidated() {
|
|
||||||
updateVerticalGap();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return this.verticalGap;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Double getVerticalGap() {
|
|
||||||
return verticalGap == null ? 0.0 : verticalGap.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setVerticalGap(Double gap) {
|
|
||||||
verticalGapProperty().set(gap);
|
|
||||||
}
|
|
||||||
|
|
||||||
private StyleableBooleanProperty expanded;
|
|
||||||
|
|
||||||
public StyleableBooleanProperty expandedProperty() {
|
|
||||||
if (expanded == null) {
|
|
||||||
expanded = new StyleableBooleanProperty(false) {
|
|
||||||
@Override
|
|
||||||
public Object getBean() {
|
|
||||||
return JFXListView.this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return "expanded";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CssMetaData<? extends Styleable, Boolean> getCssMetaData() {
|
|
||||||
return StyleableProperties.EXPANDED;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void invalidated() {
|
|
||||||
updateVerticalGap();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.expanded;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Boolean isExpanded() {
|
|
||||||
return expanded != null && expanded.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setExpanded(Boolean expanded) {
|
|
||||||
expandedProperty().set(expanded);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class StyleableProperties {
|
|
||||||
private static final CssMetaData<JFXListView<?>, Number> VERTICAL_GAP =
|
|
||||||
new CssMetaData<>("-jfx-vertical-gap",
|
|
||||||
SizeConverter.getInstance(), 0) {
|
|
||||||
@Override
|
|
||||||
public boolean isSettable(JFXListView<?> control) {
|
|
||||||
return control.verticalGap == null || !control.verticalGap.isBound();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public StyleableDoubleProperty getStyleableProperty(JFXListView<?> control) {
|
|
||||||
return control.verticalGapProperty();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
private static final CssMetaData<JFXListView<?>, Boolean> EXPANDED =
|
|
||||||
new CssMetaData<>("-jfx-expanded",
|
|
||||||
BooleanConverter.getInstance(), false) {
|
|
||||||
@Override
|
|
||||||
public boolean isSettable(JFXListView<?> control) {
|
|
||||||
// it's only settable if the List is not shown yet
|
|
||||||
return control.getHeight() == 0 && (control.expanded == null || !control.expanded.isBound());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public StyleableBooleanProperty getStyleableProperty(JFXListView<?> control) {
|
|
||||||
return control.expandedProperty();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
|
|
||||||
|
|
||||||
static {
|
|
||||||
final List<CssMetaData<? extends Styleable, ?>> styleables =
|
|
||||||
new ArrayList<>(ListView.getClassCssMetaData());
|
|
||||||
Collections.addAll(styleables, VERTICAL_GAP, EXPANDED);
|
|
||||||
CHILD_STYLEABLES = List.copyOf(styleables);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
|
|
||||||
return getClassCssMetaData();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
|
|
||||||
return StyleableProperties.CHILD_STYLEABLES;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,299 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.jfoenix.controls;
|
|
||||||
|
|
||||||
import com.jfoenix.skins.JFXPasswordFieldSkin;
|
|
||||||
import com.jfoenix.validation.base.ValidatorBase;
|
|
||||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
|
||||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
|
||||||
import javafx.collections.FXCollections;
|
|
||||||
import javafx.collections.ObservableList;
|
|
||||||
import javafx.css.*;
|
|
||||||
import javafx.css.converter.BooleanConverter;
|
|
||||||
import javafx.css.converter.PaintConverter;
|
|
||||||
import javafx.scene.control.Control;
|
|
||||||
import javafx.scene.control.PasswordField;
|
|
||||||
import javafx.scene.control.Skin;
|
|
||||||
import javafx.scene.control.TextField;
|
|
||||||
import javafx.scene.paint.Color;
|
|
||||||
import javafx.scene.paint.Paint;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.ui.FXUtils.useJFXContextMenu;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JFXPasswordField is the material design implementation of a password Field.
|
|
||||||
*
|
|
||||||
* @author Shadi Shaheen
|
|
||||||
* @version 1.0
|
|
||||||
* @since 2016-03-09
|
|
||||||
*/
|
|
||||||
public class JFXPasswordField extends PasswordField {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
public JFXPasswordField() {
|
|
||||||
initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected Skin<?> createDefaultSkin() {
|
|
||||||
return new JFXPasswordFieldSkin(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initialize() {
|
|
||||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
|
||||||
useJFXContextMenu(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Prevents the legacy JFoenix skin from continuously requesting another layout pass.
|
|
||||||
// https://github.com/HMCL-dev/HMCL/issues/5822
|
|
||||||
// TODO: This method may no longer be needed after we update JFXTextFieldSkin
|
|
||||||
@Override
|
|
||||||
protected void layoutChildren() {
|
|
||||||
super.layoutChildren();
|
|
||||||
this.setNeedsLayout(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the style class to 'jfx-password-field'.
|
|
||||||
* <p>
|
|
||||||
* This is the selector class from which CSS can be used to style
|
|
||||||
* this control.
|
|
||||||
*/
|
|
||||||
private static final String DEFAULT_STYLE_CLASS = "jfx-password-field";
|
|
||||||
|
|
||||||
/***************************************************************************
|
|
||||||
* *
|
|
||||||
* Properties *
|
|
||||||
* *
|
|
||||||
**************************************************************************/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* holds the current active validator on the password field in case of validation error
|
|
||||||
*/
|
|
||||||
private ReadOnlyObjectWrapper<ValidatorBase> activeValidator = new ReadOnlyObjectWrapper<>();
|
|
||||||
|
|
||||||
public ValidatorBase getActiveValidator() {
|
|
||||||
return activeValidator == null ? null : activeValidator.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ReadOnlyObjectProperty<ValidatorBase> activeValidatorProperty() {
|
|
||||||
return this.activeValidator.getReadOnlyProperty();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* list of validators that will validate the password value upon calling
|
|
||||||
* {{@link #validate()}
|
|
||||||
*/
|
|
||||||
private ObservableList<ValidatorBase> validators = FXCollections.observableArrayList();
|
|
||||||
|
|
||||||
public ObservableList<ValidatorBase> getValidators() {
|
|
||||||
return validators;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setValidators(ValidatorBase... validators) {
|
|
||||||
this.validators.addAll(validators);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* validates the password value using the list of validators provided by the user
|
|
||||||
* {{@link #setValidators(ValidatorBase...)}
|
|
||||||
*
|
|
||||||
* @return true if the value is valid else false
|
|
||||||
*/
|
|
||||||
public boolean validate() {
|
|
||||||
for (ValidatorBase validator : validators) {
|
|
||||||
if (validator.getSrcControl() == null) {
|
|
||||||
validator.setSrcControl(this);
|
|
||||||
}
|
|
||||||
validator.validate();
|
|
||||||
if (validator.getHasErrors()) {
|
|
||||||
activeValidator.set(validator);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
activeValidator.set(null);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void resetValidation() {
|
|
||||||
pseudoClassStateChanged(ValidatorBase.PSEUDO_CLASS_ERROR, false);
|
|
||||||
activeValidator.set(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/***************************************************************************
|
|
||||||
* *
|
|
||||||
* styleable Properties *
|
|
||||||
* *
|
|
||||||
**************************************************************************/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* set true to show a float the prompt text when focusing the field
|
|
||||||
*/
|
|
||||||
private StyleableBooleanProperty labelFloat = new SimpleStyleableBooleanProperty(StyleableProperties.LABEL_FLOAT, JFXPasswordField.this, "lableFloat", false);
|
|
||||||
|
|
||||||
public final StyleableBooleanProperty labelFloatProperty() {
|
|
||||||
return this.labelFloat;
|
|
||||||
}
|
|
||||||
|
|
||||||
public final boolean isLabelFloat() {
|
|
||||||
return this.labelFloatProperty().get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public final void setLabelFloat(final boolean labelFloat) {
|
|
||||||
this.labelFloatProperty().set(labelFloat);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* default color used when the field is unfocused
|
|
||||||
*/
|
|
||||||
private StyleableObjectProperty<Paint> unFocusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.UNFOCUS_COLOR, JFXPasswordField.this, "unFocusColor", Color.rgb(77, 77, 77));
|
|
||||||
|
|
||||||
public Paint getUnFocusColor() {
|
|
||||||
return unFocusColor == null ? Color.rgb(77, 77, 77) : unFocusColor.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public StyleableObjectProperty<Paint> unFocusColorProperty() {
|
|
||||||
return this.unFocusColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setUnFocusColor(Paint color) {
|
|
||||||
this.unFocusColor.set(color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* default color used when the field is focused
|
|
||||||
*/
|
|
||||||
private StyleableObjectProperty<Paint> focusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.FOCUS_COLOR, JFXPasswordField.this, "focusColor", Color.valueOf("#4059A9"));
|
|
||||||
|
|
||||||
public Paint getFocusColor() {
|
|
||||||
return focusColor == null ? Color.valueOf("#4059A9") : focusColor.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public StyleableObjectProperty<Paint> focusColorProperty() {
|
|
||||||
return this.focusColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setFocusColor(Paint color) {
|
|
||||||
this.focusColor.set(color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* disable animation on validation
|
|
||||||
*/
|
|
||||||
private StyleableBooleanProperty disableAnimation = new SimpleStyleableBooleanProperty(StyleableProperties.DISABLE_ANIMATION, JFXPasswordField.this, "disableAnimation", false);
|
|
||||||
|
|
||||||
public final StyleableBooleanProperty disableAnimationProperty() {
|
|
||||||
return this.disableAnimation;
|
|
||||||
}
|
|
||||||
|
|
||||||
public final Boolean isDisableAnimation() {
|
|
||||||
return disableAnimation != null && this.disableAnimationProperty().get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public final void setDisableAnimation(final Boolean disabled) {
|
|
||||||
this.disableAnimationProperty().set(disabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
private final static class StyleableProperties {
|
|
||||||
private static final CssMetaData<JFXPasswordField, Paint> UNFOCUS_COLOR = new CssMetaData<JFXPasswordField, Paint>("-jfx-unfocus-color", PaintConverter.getInstance(), Color.valueOf("#A6A6A6")) {
|
|
||||||
@Override
|
|
||||||
public boolean isSettable(JFXPasswordField control) {
|
|
||||||
return control.unFocusColor == null || !control.unFocusColor.isBound();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public StyleableProperty<Paint> getStyleableProperty(JFXPasswordField control) {
|
|
||||||
return control.unFocusColorProperty();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
private static final CssMetaData<JFXPasswordField, Paint> FOCUS_COLOR = new CssMetaData<JFXPasswordField, Paint>("-jfx-focus-color", PaintConverter.getInstance(), Color.valueOf("#3f51b5")) {
|
|
||||||
@Override
|
|
||||||
public boolean isSettable(JFXPasswordField control) {
|
|
||||||
return control.focusColor == null || !control.focusColor.isBound();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public StyleableProperty<Paint> getStyleableProperty(JFXPasswordField control) {
|
|
||||||
return control.focusColorProperty();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private static final CssMetaData<JFXPasswordField, Boolean> LABEL_FLOAT = new CssMetaData<JFXPasswordField, Boolean>("-jfx-label-float", BooleanConverter.getInstance(), false) {
|
|
||||||
@Override
|
|
||||||
public boolean isSettable(JFXPasswordField control) {
|
|
||||||
return control.labelFloat == null || !control.labelFloat.isBound();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public StyleableBooleanProperty getStyleableProperty(JFXPasswordField control) {
|
|
||||||
return control.labelFloatProperty();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private static final CssMetaData<JFXPasswordField, Boolean> DISABLE_ANIMATION = new CssMetaData<JFXPasswordField, Boolean>("-fx-disable-animation", BooleanConverter.getInstance(), false) {
|
|
||||||
@Override
|
|
||||||
public boolean isSettable(JFXPasswordField control) {
|
|
||||||
return control.disableAnimation == null || !control.disableAnimation.isBound();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public StyleableBooleanProperty getStyleableProperty(JFXPasswordField control) {
|
|
||||||
return control.disableAnimationProperty();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
|
|
||||||
|
|
||||||
static {
|
|
||||||
final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Control.getClassCssMetaData());
|
|
||||||
Collections.addAll(styleables, UNFOCUS_COLOR, FOCUS_COLOR, LABEL_FLOAT, DISABLE_ANIMATION);
|
|
||||||
CHILD_STYLEABLES = Collections.unmodifiableList(styleables);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// inherit the styleable properties from parent
|
|
||||||
private List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
|
|
||||||
if (STYLEABLES == null) {
|
|
||||||
final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Control.getClassCssMetaData());
|
|
||||||
styleables.addAll(getClassCssMetaData());
|
|
||||||
styleables.addAll(TextField.getClassCssMetaData());
|
|
||||||
STYLEABLES = Collections.unmodifiableList(styleables);
|
|
||||||
}
|
|
||||||
return STYLEABLES;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
|
|
||||||
return StyleableProperties.CHILD_STYLEABLES;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.jfoenix.controls;
|
|
||||||
|
|
||||||
import com.jfoenix.skins.JFXPopupSkin;
|
|
||||||
import javafx.application.Platform;
|
|
||||||
import javafx.beans.DefaultProperty;
|
|
||||||
import javafx.beans.property.ObjectProperty;
|
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
|
||||||
import javafx.geometry.NodeOrientation;
|
|
||||||
import javafx.geometry.Point2D;
|
|
||||||
import javafx.scene.Node;
|
|
||||||
import javafx.scene.Scene;
|
|
||||||
import javafx.scene.control.PopupControl;
|
|
||||||
import javafx.scene.control.Skin;
|
|
||||||
import javafx.scene.layout.Pane;
|
|
||||||
import javafx.scene.layout.Region;
|
|
||||||
import javafx.stage.Window;
|
|
||||||
|
|
||||||
/// JFXPopup is the material design implementation of a popup.
|
|
||||||
///
|
|
||||||
/// @author Shadi Shaheen
|
|
||||||
/// @version 2.0
|
|
||||||
/// @since 2017-03-01
|
|
||||||
@DefaultProperty(value = "popupContent")
|
|
||||||
public class JFXPopup extends PopupControl {
|
|
||||||
|
|
||||||
public enum PopupHPosition {
|
|
||||||
RIGHT,
|
|
||||||
LEFT;
|
|
||||||
|
|
||||||
public PopupHPosition getOpposite() {
|
|
||||||
return (this == RIGHT) ? LEFT : RIGHT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum PopupVPosition {
|
|
||||||
TOP, BOTTOM
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates empty popup.
|
|
||||||
public JFXPopup() {
|
|
||||||
this(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// creates popup with a specified container and content
|
|
||||||
///
|
|
||||||
/// @param content the node that will be shown in the popup
|
|
||||||
public JFXPopup(Region content) {
|
|
||||||
setPopupContent(content);
|
|
||||||
initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initialize() {
|
|
||||||
this.setAutoFix(false);
|
|
||||||
this.setAutoHide(true);
|
|
||||||
this.setHideOnEscape(true);
|
|
||||||
this.setConsumeAutoHidingEvents(false);
|
|
||||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Skin<?> createDefaultSkin() {
|
|
||||||
return new JFXPopupSkin(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/***************************************************************************
|
|
||||||
* *
|
|
||||||
* Setters / Getters *
|
|
||||||
* *
|
|
||||||
**************************************************************************/
|
|
||||||
|
|
||||||
private final ObjectProperty<Region> popupContent = new SimpleObjectProperty<>(new Pane());
|
|
||||||
|
|
||||||
public final ObjectProperty<Region> popupContentProperty() {
|
|
||||||
return this.popupContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
public final Region getPopupContent() {
|
|
||||||
return this.popupContentProperty().get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public final void setPopupContent(final Region popupContent) {
|
|
||||||
this.popupContentProperty().set(popupContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
/***************************************************************************
|
|
||||||
* *
|
|
||||||
* Public API *
|
|
||||||
* *
|
|
||||||
**************************************************************************/
|
|
||||||
|
|
||||||
/// show the popup using the default position
|
|
||||||
public void show(Node node) {
|
|
||||||
this.show(node, PopupVPosition.TOP, PopupHPosition.LEFT, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// show the popup according to the specified position
|
|
||||||
///
|
|
||||||
/// @param vAlign can be TOP/BOTTOM
|
|
||||||
/// @param hAlign can be LEFT/RIGHT
|
|
||||||
public void show(Node node, PopupVPosition vAlign, PopupHPosition hAlign) {
|
|
||||||
this.show(node, vAlign, hAlign, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// show the popup according to the specified position with a certain offset
|
|
||||||
///
|
|
||||||
/// @param vAlign can be TOP/BOTTOM
|
|
||||||
/// @param hAlign can be LEFT/RIGHT
|
|
||||||
/// @param initOffsetX on the x-axis
|
|
||||||
/// @param initOffsetY on the y-axis
|
|
||||||
public void show(Node node, PopupVPosition vAlign, PopupHPosition hAlign, double initOffsetX, double initOffsetY) {
|
|
||||||
show(node, vAlign, hAlign, initOffsetX, initOffsetY, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void show(Node node, PopupVPosition vAlign, PopupHPosition hAlign, double initOffsetX, double initOffsetY, boolean attachToNode) {
|
|
||||||
if (!isShowing()) {
|
|
||||||
Scene scene = node.getScene();
|
|
||||||
if (scene == null || scene.getWindow() == null) {
|
|
||||||
throw new IllegalStateException("Can not show popup. The node must be attached to a scene/window.");
|
|
||||||
}
|
|
||||||
Window parent = scene.getWindow();
|
|
||||||
final Point2D origin = node.localToScene(0, 0);
|
|
||||||
|
|
||||||
boolean isRTL = node.getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT;
|
|
||||||
|
|
||||||
double anchorX = parent.getX() + scene.getX() + origin.getX() + (hAlign == PopupHPosition.RIGHT ? ((Region) node).getWidth() : 0);
|
|
||||||
double anchorY = parent.getY() + origin.getY() + scene.getY() + (vAlign == PopupVPosition.BOTTOM ? ((Region) node).getHeight() : 0);
|
|
||||||
|
|
||||||
if (attachToNode)
|
|
||||||
this.show(node, anchorX, anchorY);
|
|
||||||
else
|
|
||||||
this.show(parent, anchorX, anchorY);
|
|
||||||
|
|
||||||
((JFXPopupSkin) getSkin()).reset(vAlign, isRTL ? hAlign.getOpposite() : hAlign, isRTL ? -initOffsetX : initOffsetX, initOffsetY);
|
|
||||||
Platform.runLater(() -> ((JFXPopupSkin) getSkin()).animate());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void show(Window window, double x, double y, PopupVPosition vAlign, PopupHPosition hAlign, double initOffsetX, double initOffsetY) {
|
|
||||||
if (!isShowing()) {
|
|
||||||
if (window == null) {
|
|
||||||
throw new IllegalStateException("Can not show popup. The node must be attached to a scene/window.");
|
|
||||||
}
|
|
||||||
Window parent = window;
|
|
||||||
final double anchorX = parent.getX() + x + initOffsetX;
|
|
||||||
final double anchorY = parent.getY() + y + initOffsetY;
|
|
||||||
this.show(parent, anchorX, anchorY);
|
|
||||||
((JFXPopupSkin) getSkin()).reset(vAlign, hAlign, initOffsetX, initOffsetY);
|
|
||||||
Platform.runLater(() -> ((JFXPopupSkin) getSkin()).animate());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void hide() {
|
|
||||||
super.hide();
|
|
||||||
((JFXPopupSkin) getSkin()).init();
|
|
||||||
}
|
|
||||||
|
|
||||||
/***************************************************************************
|
|
||||||
* *
|
|
||||||
* Stylesheet Handling *
|
|
||||||
* *
|
|
||||||
**************************************************************************/
|
|
||||||
|
|
||||||
/// Initialize the style class to 'jfx-popup'.
|
|
||||||
///
|
|
||||||
/// This is the selector class from which CSS can be used to style
|
|
||||||
/// this control.
|
|
||||||
private static final String DEFAULT_STYLE_CLASS = "jfx-popup";
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.jfoenix.controls;
|
|
||||||
|
|
||||||
import com.jfoenix.skins.JFXProgressBarSkin;
|
|
||||||
import javafx.scene.control.ProgressBar;
|
|
||||||
import javafx.scene.control.Skin;
|
|
||||||
|
|
||||||
/// JFXProgressBar is the material design implementation of a progress bar.
|
|
||||||
///
|
|
||||||
/// @author Shadi Shaheen
|
|
||||||
/// @version 1.0
|
|
||||||
/// @since 2016-03-09
|
|
||||||
public class JFXProgressBar extends ProgressBar {
|
|
||||||
/// Initialize the style class to 'jfx-progress-bar'.
|
|
||||||
///
|
|
||||||
/// This is the selector class from which CSS can be used to style
|
|
||||||
/// this control.
|
|
||||||
private static final String DEFAULT_STYLE_CLASS = "jfx-progress-bar";
|
|
||||||
|
|
||||||
public JFXProgressBar() {
|
|
||||||
initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
public JFXProgressBar(double progress) {
|
|
||||||
super(progress);
|
|
||||||
initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Skin<?> createDefaultSkin() {
|
|
||||||
return new JFXProgressBarSkin(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean smoothProgress = true;
|
|
||||||
|
|
||||||
public boolean isSmoothProgress() {
|
|
||||||
return smoothProgress;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSmoothProgress(boolean smoothProgress) {
|
|
||||||
this.smoothProgress = smoothProgress;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initialize() {
|
|
||||||
setPrefWidth(200);
|
|
||||||
getStyleClass().add(DEFAULT_STYLE_CLASS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.jfoenix.controls;
|
|
||||||
|
|
||||||
import com.jfoenix.skins.JFXRadioButtonSkin;
|
|
||||||
import javafx.css.*;
|
|
||||||
import javafx.css.converter.ColorConverter;
|
|
||||||
import javafx.scene.control.RadioButton;
|
|
||||||
import javafx.scene.control.Skin;
|
|
||||||
import javafx.scene.paint.Color;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/// JFXRadioButton is the material design implementation of a radio button.
|
|
||||||
///
|
|
||||||
/// @author Bashir Elias & Shadi Shaheen
|
|
||||||
/// @version 1.0
|
|
||||||
/// @since 2016-03-09
|
|
||||||
public class JFXRadioButton extends RadioButton {
|
|
||||||
|
|
||||||
public JFXRadioButton(String text) {
|
|
||||||
super(text);
|
|
||||||
initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
public JFXRadioButton() {
|
|
||||||
initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Skin<?> createDefaultSkin() {
|
|
||||||
return new JFXRadioButtonSkin(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initialize() {
|
|
||||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Initialize the style class to 'jfx-radio-button'.
|
|
||||||
///
|
|
||||||
/// This is the selector class from which CSS can be used to style
|
|
||||||
/// this control.
|
|
||||||
private static final String DEFAULT_STYLE_CLASS = "jfx-radio-button";
|
|
||||||
|
|
||||||
/// default color used when the radio button is selected
|
|
||||||
private StyleableObjectProperty<Color> selectedColor;
|
|
||||||
|
|
||||||
private static final Color DEFAULT_SELECTED_COLOR = Color.valueOf("#0F9D58");
|
|
||||||
|
|
||||||
public final StyleableObjectProperty<Color> selectedColorProperty() {
|
|
||||||
if (selectedColor == null) {
|
|
||||||
selectedColor = new SimpleStyleableObjectProperty<>(StyleableProperties.SELECTED_COLOR,
|
|
||||||
JFXRadioButton.this,
|
|
||||||
"selectedColor",
|
|
||||||
DEFAULT_SELECTED_COLOR);
|
|
||||||
}
|
|
||||||
return this.selectedColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
public final Color getSelectedColor() {
|
|
||||||
return selectedColor == null ? DEFAULT_SELECTED_COLOR : this.selectedColorProperty().get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public final void setSelectedColor(final Color selectedColor) {
|
|
||||||
this.selectedColorProperty().set(selectedColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// default color used when the radio button is not selected
|
|
||||||
private StyleableObjectProperty<Color> unSelectedColor;
|
|
||||||
|
|
||||||
private static final Color DEFAULT_UNSELECTED_COLOR = Color.valueOf("#5A5A5A");
|
|
||||||
|
|
||||||
public final StyleableObjectProperty<Color> unSelectedColorProperty() {
|
|
||||||
if (unSelectedColor == null) {
|
|
||||||
unSelectedColor = new SimpleStyleableObjectProperty<>(
|
|
||||||
StyleableProperties.UNSELECTED_COLOR,
|
|
||||||
JFXRadioButton.this,
|
|
||||||
"unSelectedColor",
|
|
||||||
DEFAULT_UNSELECTED_COLOR);
|
|
||||||
}
|
|
||||||
return this.unSelectedColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
public final Color getUnSelectedColor() {
|
|
||||||
return unSelectedColor == null ? DEFAULT_UNSELECTED_COLOR : this.unSelectedColorProperty().get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public final void setUnSelectedColor(final Color unSelectedColor) {
|
|
||||||
this.unSelectedColorProperty().set(unSelectedColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class StyleableProperties {
|
|
||||||
private static final CssMetaData<JFXRadioButton, Color> SELECTED_COLOR =
|
|
||||||
new CssMetaData<JFXRadioButton, Color>("-jfx-selected-color",
|
|
||||||
ColorConverter.getInstance(), DEFAULT_SELECTED_COLOR) {
|
|
||||||
@Override
|
|
||||||
public boolean isSettable(JFXRadioButton control) {
|
|
||||||
return control.selectedColor == null || !control.selectedColor.isBound();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public StyleableProperty<Color> getStyleableProperty(JFXRadioButton control) {
|
|
||||||
return control.selectedColorProperty();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
private static final CssMetaData<JFXRadioButton, Color> UNSELECTED_COLOR =
|
|
||||||
new CssMetaData<JFXRadioButton, Color>("-jfx-unselected-color",
|
|
||||||
ColorConverter.getInstance(), DEFAULT_UNSELECTED_COLOR) {
|
|
||||||
@Override
|
|
||||||
public boolean isSettable(JFXRadioButton control) {
|
|
||||||
return control.unSelectedColor == null || !control.unSelectedColor.isBound();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public StyleableProperty<Color> getStyleableProperty(JFXRadioButton control) {
|
|
||||||
return control.unSelectedColorProperty();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
|
|
||||||
|
|
||||||
static {
|
|
||||||
final List<CssMetaData<? extends Styleable, ?>> styleables =
|
|
||||||
new ArrayList<>(RadioButton.getClassCssMetaData());
|
|
||||||
Collections.addAll(styleables,
|
|
||||||
SELECTED_COLOR,
|
|
||||||
UNSELECTED_COLOR
|
|
||||||
);
|
|
||||||
CHILD_STYLEABLES = List.copyOf(styleables);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
|
|
||||||
return getClassCssMetaData();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
|
|
||||||
return StyleableProperties.CHILD_STYLEABLES;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,834 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.jfoenix.controls;
|
|
||||||
|
|
||||||
import com.jfoenix.converters.RipplerMaskTypeConverter;
|
|
||||||
import com.jfoenix.utils.JFXNodeUtils;
|
|
||||||
import javafx.animation.*;
|
|
||||||
import javafx.beans.DefaultProperty;
|
|
||||||
import javafx.beans.binding.Bindings;
|
|
||||||
import javafx.beans.property.ObjectProperty;
|
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
|
||||||
import javafx.css.*;
|
|
||||||
import javafx.css.converter.BooleanConverter;
|
|
||||||
import javafx.css.converter.PaintConverter;
|
|
||||||
import javafx.css.converter.SizeConverter;
|
|
||||||
import javafx.geometry.Bounds;
|
|
||||||
import javafx.scene.CacheHint;
|
|
||||||
import javafx.scene.Group;
|
|
||||||
import javafx.scene.Node;
|
|
||||||
import javafx.scene.input.MouseButton;
|
|
||||||
import javafx.scene.input.MouseEvent;
|
|
||||||
import javafx.scene.layout.Pane;
|
|
||||||
import javafx.scene.layout.Region;
|
|
||||||
import javafx.scene.layout.StackPane;
|
|
||||||
import javafx.scene.paint.Color;
|
|
||||||
import javafx.scene.paint.Paint;
|
|
||||||
import javafx.scene.shape.Circle;
|
|
||||||
import javafx.scene.shape.Rectangle;
|
|
||||||
import javafx.scene.shape.Shape;
|
|
||||||
import javafx.util.Duration;
|
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JFXRippler is the material design implementation of a ripple effect.
|
|
||||||
* the ripple effect can be applied to any node in the scene. JFXRippler is
|
|
||||||
* a {@link StackPane} container that holds a specified node (control node) and a ripple generator.
|
|
||||||
* <p>
|
|
||||||
* UPDATE NOTES:
|
|
||||||
* - fireEventProgrammatically(Event) method has been removed as the ripple controller is
|
|
||||||
* the control itself, so you can trigger manual ripple by firing mouse event on the control
|
|
||||||
* instead of JFXRippler
|
|
||||||
*
|
|
||||||
* @author Shadi Shaheen
|
|
||||||
* @version 1.0
|
|
||||||
* @since 2016-03-09
|
|
||||||
*/
|
|
||||||
@DefaultProperty(value = "control")
|
|
||||||
public class JFXRippler extends StackPane {
|
|
||||||
public enum RipplerPos {
|
|
||||||
FRONT, BACK
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum RipplerMask {
|
|
||||||
CIRCLE, RECT, FIT
|
|
||||||
}
|
|
||||||
|
|
||||||
protected RippleGenerator rippler;
|
|
||||||
protected Pane ripplerPane;
|
|
||||||
protected Node control;
|
|
||||||
|
|
||||||
protected static final double RIPPLE_MAX_RADIUS = 300;
|
|
||||||
private static final Interpolator RIPPLE_INTERPOLATOR = Interpolator.SPLINE(0.0825,
|
|
||||||
0.3025,
|
|
||||||
0.0875,
|
|
||||||
0.9975); //0.1, 0.54, 0.28, 0.95);
|
|
||||||
|
|
||||||
private boolean forceOverlay = false;
|
|
||||||
|
|
||||||
/// creates empty rippler node
|
|
||||||
public JFXRippler() {
|
|
||||||
this(null, RipplerMask.RECT, RipplerPos.FRONT);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// creates a rippler for the specified control
|
|
||||||
public JFXRippler(Node control) {
|
|
||||||
this(control, RipplerMask.RECT, RipplerPos.FRONT);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// creates a rippler for the specified control
|
|
||||||
///
|
|
||||||
/// @param pos can be either FRONT/BACK (position the ripple effect infront of or behind the control)
|
|
||||||
public JFXRippler(Node control, RipplerPos pos) {
|
|
||||||
this(control, RipplerMask.RECT, pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// creates a rippler for the specified control and apply the specified mask to it
|
|
||||||
///
|
|
||||||
/// @param mask can be either rectangle/cricle
|
|
||||||
public JFXRippler(Node control, RipplerMask mask) {
|
|
||||||
this(control, mask, RipplerPos.FRONT);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// creates a rippler for the specified control, mask and position.
|
|
||||||
///
|
|
||||||
/// @param mask can be either rectangle/cricle
|
|
||||||
/// @param pos can be either FRONT/BACK (position the ripple effect infront of or behind the control)
|
|
||||||
public JFXRippler(Node control, RipplerMask mask, RipplerPos pos) {
|
|
||||||
initialize();
|
|
||||||
|
|
||||||
setMaskType(mask);
|
|
||||||
setPosition(pos);
|
|
||||||
createRippleUI();
|
|
||||||
setControl(control);
|
|
||||||
|
|
||||||
// listen to control position changed
|
|
||||||
position.addListener(observable -> updateControlPosition());
|
|
||||||
|
|
||||||
setPickOnBounds(false);
|
|
||||||
setCache(true);
|
|
||||||
setCacheHint(CacheHint.SPEED);
|
|
||||||
setCacheShape(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected final void createRippleUI() {
|
|
||||||
// create rippler panels
|
|
||||||
rippler = new RippleGenerator();
|
|
||||||
ripplerPane = new StackPane();
|
|
||||||
ripplerPane.setMouseTransparent(true);
|
|
||||||
ripplerPane.getChildren().add(rippler);
|
|
||||||
getChildren().add(ripplerPane);
|
|
||||||
}
|
|
||||||
|
|
||||||
/***************************************************************************
|
|
||||||
* *
|
|
||||||
* Setters / Getters *
|
|
||||||
* *
|
|
||||||
**************************************************************************/
|
|
||||||
|
|
||||||
public void setControl(Node control) {
|
|
||||||
if (control != null) {
|
|
||||||
this.control = control;
|
|
||||||
// position control
|
|
||||||
positionControl(control);
|
|
||||||
// add control listeners to generate / release ripples
|
|
||||||
initControlListeners();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Override this method to create JFXRippler for a control outside the ripple
|
|
||||||
protected void positionControl(Node control) {
|
|
||||||
if (this.position.get() == RipplerPos.BACK) {
|
|
||||||
getChildren().add(control);
|
|
||||||
} else {
|
|
||||||
getChildren().add(0, control);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void updateControlPosition() {
|
|
||||||
if (this.position.get() == RipplerPos.BACK) {
|
|
||||||
ripplerPane.toBack();
|
|
||||||
} else {
|
|
||||||
ripplerPane.toFront();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Node getControl() {
|
|
||||||
return control;
|
|
||||||
}
|
|
||||||
|
|
||||||
// methods that can be changed by extending the rippler class
|
|
||||||
|
|
||||||
/// generate the clipping mask
|
|
||||||
///
|
|
||||||
/// @return the mask node
|
|
||||||
protected Node getMask() {
|
|
||||||
double borderWidth = ripplerPane.getBorder() != null ? ripplerPane.getBorder().getInsets().getTop() : 0;
|
|
||||||
Bounds bounds = control.getBoundsInParent();
|
|
||||||
double width = control.getLayoutBounds().getWidth();
|
|
||||||
double height = control.getLayoutBounds().getHeight();
|
|
||||||
double diffMinX = Math.abs(control.getBoundsInLocal().getMinX() - control.getLayoutBounds().getMinX());
|
|
||||||
double diffMinY = Math.abs(control.getBoundsInLocal().getMinY() - control.getLayoutBounds().getMinY());
|
|
||||||
double diffMaxX = Math.abs(control.getBoundsInLocal().getMaxX() - control.getLayoutBounds().getMaxX());
|
|
||||||
double diffMaxY = Math.abs(control.getBoundsInLocal().getMaxY() - control.getLayoutBounds().getMaxY());
|
|
||||||
Node mask;
|
|
||||||
switch (getMaskType()) {
|
|
||||||
case RECT:
|
|
||||||
mask = new Rectangle(bounds.getMinX() + diffMinX - snappedLeftInset(),
|
|
||||||
bounds.getMinY() + diffMinY - snappedTopInset(),
|
|
||||||
width - 2 * borderWidth,
|
|
||||||
height - 2 * borderWidth); // -0.1 to prevent resizing the anchor pane
|
|
||||||
break;
|
|
||||||
case CIRCLE:
|
|
||||||
double radius = Math.min((width / 2) - 2 * borderWidth, (height / 2) - 2 * borderWidth);
|
|
||||||
mask = new Circle((bounds.getMinX() + diffMinX + bounds.getMaxX() - diffMaxX) / 2 - snappedLeftInset(),
|
|
||||||
(bounds.getMinY() + diffMinY + bounds.getMaxY() - diffMaxY) / 2 - snappedTopInset(),
|
|
||||||
radius,
|
|
||||||
Color.BLUE);
|
|
||||||
break;
|
|
||||||
case FIT:
|
|
||||||
mask = new Region();
|
|
||||||
if (control instanceof Shape) {
|
|
||||||
((Region) mask).setShape((Shape) control);
|
|
||||||
} else if (control instanceof Region) {
|
|
||||||
((Region) mask).setShape(((Region) control).getShape());
|
|
||||||
JFXNodeUtils.updateBackground(((Region) control).getBackground(), (Region) mask);
|
|
||||||
}
|
|
||||||
mask.resize(width, height);
|
|
||||||
mask.relocate(bounds.getMinX() + diffMinX, bounds.getMinY() + diffMinY);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
mask = new Rectangle(bounds.getMinX() + diffMinX - snappedLeftInset(),
|
|
||||||
bounds.getMinY() + diffMinY - snappedTopInset(),
|
|
||||||
width - 2 * borderWidth,
|
|
||||||
height - 2 * borderWidth); // -0.1 to prevent resizing the anchor pane
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return mask;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* compute the ripple radius
|
|
||||||
*
|
|
||||||
* @return the ripple radius size
|
|
||||||
*/
|
|
||||||
protected double computeRippleRadius(double centerX, double centerY) {
|
|
||||||
double dx = Math.max(centerX, control.getLayoutBounds().getWidth() - centerX);
|
|
||||||
double dy = Math.max(centerY, control.getLayoutBounds().getHeight() - centerY);
|
|
||||||
|
|
||||||
return (Math.sqrt(dx * dx + dy * dy) / 0.9) + 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void setOverLayBounds(Rectangle overlay) {
|
|
||||||
overlay.setWidth(control.getLayoutBounds().getWidth());
|
|
||||||
overlay.setHeight(control.getLayoutBounds().getHeight());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* init mouse listeners on the control
|
|
||||||
*/
|
|
||||||
protected void initControlListeners() {
|
|
||||||
// if the control got resized the overlay rect must be rest
|
|
||||||
control.layoutBoundsProperty().addListener(observable -> resetRippler());
|
|
||||||
if (getChildren().contains(control)) {
|
|
||||||
control.boundsInParentProperty().addListener(observable -> resetRippler());
|
|
||||||
}
|
|
||||||
control.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
|
|
||||||
if (event.getButton() == MouseButton.PRIMARY)
|
|
||||||
createRipple(event.getX(), event.getY());
|
|
||||||
});
|
|
||||||
// create fade out transition for the ripple
|
|
||||||
control.addEventHandler(MouseEvent.MOUSE_RELEASED, event -> {
|
|
||||||
if (event.getButton() == MouseButton.PRIMARY)
|
|
||||||
releaseRipple();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* creates Ripple effect
|
|
||||||
*/
|
|
||||||
protected void createRipple(double x, double y) {
|
|
||||||
if (!isRipplerDisabled()) {
|
|
||||||
rippler.setGeneratorCenterX(x);
|
|
||||||
rippler.setGeneratorCenterY(y);
|
|
||||||
rippler.createRipple();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void releaseRipple() {
|
|
||||||
rippler.releaseRipple();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void releaseRippleImmediately() {
|
|
||||||
rippler.releaseRippleImmediately();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* creates Ripple effect in the center of the control
|
|
||||||
*
|
|
||||||
* @return a runnable to release the ripple when needed
|
|
||||||
*/
|
|
||||||
public Runnable createManualRipple() {
|
|
||||||
if (!isRipplerDisabled()) {
|
|
||||||
rippler.setGeneratorCenterX(control.getLayoutBounds().getWidth() / 2);
|
|
||||||
rippler.setGeneratorCenterY(control.getLayoutBounds().getHeight() / 2);
|
|
||||||
rippler.createRipple();
|
|
||||||
return () -> {
|
|
||||||
// create fade out transition for the ripple
|
|
||||||
releaseRipple();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return () -> {
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// show/hide the ripple overlay
|
|
||||||
///
|
|
||||||
/// @param forceOverlay used to hold the overlay after ripple action
|
|
||||||
public void setOverlayVisible(boolean visible, boolean forceOverlay) {
|
|
||||||
this.forceOverlay = forceOverlay;
|
|
||||||
setOverlayVisible(visible);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// show/hide the ripple overlay
|
|
||||||
/// NOTE: setting overlay visibility to false will reset forceOverlay to false
|
|
||||||
public void setOverlayVisible(boolean visible) {
|
|
||||||
if (visible) {
|
|
||||||
showOverlay();
|
|
||||||
} else {
|
|
||||||
forceOverlay = false;
|
|
||||||
hideOverlay();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* this method will be set to private in future versions of JFoenix,
|
|
||||||
* user the method {@link #setOverlayVisible(boolean)}
|
|
||||||
*/
|
|
||||||
public void showOverlay() {
|
|
||||||
if (rippler.overlayRect != null) {
|
|
||||||
rippler.overlayRect.outAnimation.stop();
|
|
||||||
}
|
|
||||||
rippler.createOverlay();
|
|
||||||
rippler.overlayRect.inAnimation.play();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void hideOverlay() {
|
|
||||||
if (!forceOverlay) {
|
|
||||||
if (rippler.overlayRect != null) {
|
|
||||||
rippler.overlayRect.inAnimation.stop();
|
|
||||||
}
|
|
||||||
if (rippler.overlayRect != null) {
|
|
||||||
rippler.overlayRect.outAnimation.play();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
System.err.println("Ripple Overlay is forced!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates ripples on the screen every 0.3 seconds or whenever
|
|
||||||
* the createRipple method is called. Ripples grow and fade out
|
|
||||||
* over 0.6 seconds
|
|
||||||
*/
|
|
||||||
protected final class RippleGenerator extends Group {
|
|
||||||
|
|
||||||
private double generatorCenterX = 0;
|
|
||||||
private double generatorCenterY = 0;
|
|
||||||
private OverLayRipple overlayRect;
|
|
||||||
private final AtomicBoolean generating = new AtomicBoolean(false);
|
|
||||||
private boolean cacheRipplerClip = false;
|
|
||||||
private boolean resetClip = false;
|
|
||||||
private final Queue<Ripple> ripplesQueue = new LinkedList<>();
|
|
||||||
|
|
||||||
RippleGenerator() {
|
|
||||||
// improve in performance, by preventing
|
|
||||||
// redrawing the parent when the ripple effect is triggered
|
|
||||||
this.setManaged(false);
|
|
||||||
this.setCache(true);
|
|
||||||
this.setCacheHint(CacheHint.SPEED);
|
|
||||||
}
|
|
||||||
|
|
||||||
void createRipple() {
|
|
||||||
if (!generating.getAndSet(true)) {
|
|
||||||
// create overlay once then change its color later
|
|
||||||
createOverlay();
|
|
||||||
if (this.getClip() == null || (getChildren().size() == 1 && !cacheRipplerClip) || resetClip) {
|
|
||||||
this.setClip(getMask());
|
|
||||||
}
|
|
||||||
this.resetClip = false;
|
|
||||||
|
|
||||||
// create the ripple effect
|
|
||||||
final Ripple ripple = new Ripple(generatorCenterX, generatorCenterY);
|
|
||||||
getChildren().add(ripple);
|
|
||||||
ripplesQueue.add(ripple);
|
|
||||||
|
|
||||||
// animate the ripple
|
|
||||||
overlayRect.outAnimation.stop();
|
|
||||||
overlayRect.inAnimation.play();
|
|
||||||
ripple.inAnimation.play();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void releaseRipple() {
|
|
||||||
Ripple ripple = ripplesQueue.poll();
|
|
||||||
if (ripple != null) {
|
|
||||||
ripple.inAnimation.stop();
|
|
||||||
ripple.outAnimation = new Timeline(
|
|
||||||
new KeyFrame(Duration.millis(Math.min(800, (0.9 * 500) / ripple.getScaleX()))
|
|
||||||
, ripple.outKeyValues));
|
|
||||||
ripple.outAnimation.setOnFinished((event) -> getChildren().remove(ripple));
|
|
||||||
ripple.outAnimation.play();
|
|
||||||
if (generating.getAndSet(false)) {
|
|
||||||
if (overlayRect != null) {
|
|
||||||
overlayRect.inAnimation.stop();
|
|
||||||
if (!forceOverlay) {
|
|
||||||
overlayRect.outAnimation.play();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void releaseRippleImmediately() {
|
|
||||||
Ripple ripple = ripplesQueue.poll();
|
|
||||||
if (ripple != null) {
|
|
||||||
getChildren().remove(ripple);
|
|
||||||
if (generating.getAndSet(false)) {
|
|
||||||
if (overlayRect != null) {
|
|
||||||
overlayRect.inAnimation.stop();
|
|
||||||
if (!forceOverlay) {
|
|
||||||
overlayRect.setOpacity(0D);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void cacheRippleClip(boolean cached) {
|
|
||||||
cacheRipplerClip = cached;
|
|
||||||
}
|
|
||||||
|
|
||||||
void createOverlay() {
|
|
||||||
if (overlayRect == null) {
|
|
||||||
overlayRect = new OverLayRipple();
|
|
||||||
overlayRect.setClip(getMask());
|
|
||||||
getChildren().add(0, overlayRect);
|
|
||||||
overlayRect.fillProperty().bind(Bindings.createObjectBinding(() -> {
|
|
||||||
if (getRipplerFill() instanceof Color fill) {
|
|
||||||
return new Color(fill.getRed(),
|
|
||||||
fill.getGreen(),
|
|
||||||
fill.getBlue(),
|
|
||||||
0.2);
|
|
||||||
} else {
|
|
||||||
return Color.TRANSPARENT;
|
|
||||||
}
|
|
||||||
}, ripplerFillProperty()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void setGeneratorCenterX(double generatorCenterX) {
|
|
||||||
this.generatorCenterX = generatorCenterX;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setGeneratorCenterY(double generatorCenterY) {
|
|
||||||
this.generatorCenterY = generatorCenterY;
|
|
||||||
}
|
|
||||||
|
|
||||||
private final class OverLayRipple extends Rectangle {
|
|
||||||
// Overlay ripple animations
|
|
||||||
Animation inAnimation = new Timeline(new KeyFrame(Duration.millis(300),
|
|
||||||
new KeyValue(opacityProperty(), 1, Interpolator.EASE_IN)));
|
|
||||||
|
|
||||||
Animation outAnimation = new Timeline(new KeyFrame(Duration.millis(300),
|
|
||||||
new KeyValue(opacityProperty(), 0, Interpolator.EASE_OUT)));
|
|
||||||
|
|
||||||
OverLayRipple() {
|
|
||||||
super();
|
|
||||||
setOverLayBounds(this);
|
|
||||||
this.getStyleClass().add("jfx-rippler-overlay");
|
|
||||||
// update initial position
|
|
||||||
if (JFXRippler.this.getChildrenUnmodifiable().contains(control)) {
|
|
||||||
double diffMinX = Math.abs(control.getBoundsInLocal().getMinX() - control.getLayoutBounds().getMinX());
|
|
||||||
double diffMinY = Math.abs(control.getBoundsInLocal().getMinY() - control.getLayoutBounds().getMinY());
|
|
||||||
Bounds bounds = control.getBoundsInParent();
|
|
||||||
this.setX(bounds.getMinX() + diffMinX - snappedLeftInset());
|
|
||||||
this.setY(bounds.getMinY() + diffMinY - snappedTopInset());
|
|
||||||
}
|
|
||||||
// set initial attributes
|
|
||||||
setOpacity(0);
|
|
||||||
setCache(true);
|
|
||||||
setCacheHint(CacheHint.SPEED);
|
|
||||||
setCacheShape(true);
|
|
||||||
setManaged(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final class Ripple extends Circle {
|
|
||||||
|
|
||||||
KeyValue[] outKeyValues;
|
|
||||||
Animation outAnimation = null;
|
|
||||||
Animation inAnimation = null;
|
|
||||||
|
|
||||||
private Ripple(double centerX, double centerY) {
|
|
||||||
super(centerX,
|
|
||||||
centerY,
|
|
||||||
getRipplerRadius() == Region.USE_COMPUTED_SIZE ?
|
|
||||||
computeRippleRadius(centerX, centerY) : getRipplerRadius(), null);
|
|
||||||
setCache(true);
|
|
||||||
setCacheHint(CacheHint.SPEED);
|
|
||||||
setCacheShape(true);
|
|
||||||
setManaged(false);
|
|
||||||
setSmooth(true);
|
|
||||||
|
|
||||||
KeyValue[] inKeyValues = new KeyValue[isRipplerRecenter() ? 4 : 2];
|
|
||||||
outKeyValues = new KeyValue[isRipplerRecenter() ? 5 : 3];
|
|
||||||
|
|
||||||
inKeyValues[0] = new KeyValue(scaleXProperty(), 0.9, RIPPLE_INTERPOLATOR);
|
|
||||||
inKeyValues[1] = new KeyValue(scaleYProperty(), 0.9, RIPPLE_INTERPOLATOR);
|
|
||||||
|
|
||||||
outKeyValues[0] = new KeyValue(this.scaleXProperty(), 1, RIPPLE_INTERPOLATOR);
|
|
||||||
outKeyValues[1] = new KeyValue(this.scaleYProperty(), 1, RIPPLE_INTERPOLATOR);
|
|
||||||
outKeyValues[2] = new KeyValue(this.opacityProperty(), 0, RIPPLE_INTERPOLATOR);
|
|
||||||
|
|
||||||
if (isRipplerRecenter()) {
|
|
||||||
double dx = (control.getLayoutBounds().getWidth() / 2 - centerX) / 1.55;
|
|
||||||
double dy = (control.getLayoutBounds().getHeight() / 2 - centerY) / 1.55;
|
|
||||||
inKeyValues[2] = outKeyValues[3] = new KeyValue(translateXProperty(),
|
|
||||||
Math.signum(dx) * Math.min(Math.abs(dx),
|
|
||||||
this.getRadius() / 2),
|
|
||||||
RIPPLE_INTERPOLATOR);
|
|
||||||
inKeyValues[3] = outKeyValues[4] = new KeyValue(translateYProperty(),
|
|
||||||
Math.signum(dy) * Math.min(Math.abs(dy),
|
|
||||||
this.getRadius() / 2),
|
|
||||||
RIPPLE_INTERPOLATOR);
|
|
||||||
}
|
|
||||||
inAnimation = new Timeline(new KeyFrame(Duration.ZERO,
|
|
||||||
new KeyValue(scaleXProperty(),
|
|
||||||
0,
|
|
||||||
RIPPLE_INTERPOLATOR),
|
|
||||||
new KeyValue(scaleYProperty(),
|
|
||||||
0,
|
|
||||||
RIPPLE_INTERPOLATOR),
|
|
||||||
new KeyValue(translateXProperty(),
|
|
||||||
0,
|
|
||||||
RIPPLE_INTERPOLATOR),
|
|
||||||
new KeyValue(translateYProperty(),
|
|
||||||
0,
|
|
||||||
RIPPLE_INTERPOLATOR),
|
|
||||||
new KeyValue(opacityProperty(),
|
|
||||||
1,
|
|
||||||
RIPPLE_INTERPOLATOR)
|
|
||||||
), new KeyFrame(Duration.millis(600), inKeyValues));
|
|
||||||
|
|
||||||
setScaleX(0);
|
|
||||||
setScaleY(0);
|
|
||||||
if (getRipplerFill() instanceof Color fill) {
|
|
||||||
Color circleColor = new Color(fill.getRed(),
|
|
||||||
fill.getGreen(),
|
|
||||||
fill.getBlue(),
|
|
||||||
0.3);
|
|
||||||
setStroke(circleColor);
|
|
||||||
setFill(circleColor);
|
|
||||||
} else {
|
|
||||||
setStroke(getRipplerFill());
|
|
||||||
setFill(getRipplerFill());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void clear() {
|
|
||||||
getChildren().clear();
|
|
||||||
rippler.overlayRect = null;
|
|
||||||
generating.set(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void resetOverLay() {
|
|
||||||
if (rippler.overlayRect != null) {
|
|
||||||
rippler.overlayRect.inAnimation.stop();
|
|
||||||
final RippleGenerator.OverLayRipple oldOverlay = rippler.overlayRect;
|
|
||||||
rippler.overlayRect.outAnimation.setOnFinished((finish) -> rippler.getChildren().remove(oldOverlay));
|
|
||||||
rippler.overlayRect.outAnimation.play();
|
|
||||||
rippler.overlayRect = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void resetClip() {
|
|
||||||
this.rippler.resetClip = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void resetRippler() {
|
|
||||||
resetOverLay();
|
|
||||||
resetClip();
|
|
||||||
}
|
|
||||||
|
|
||||||
/***************************************************************************
|
|
||||||
* *
|
|
||||||
* Stylesheet Handling *
|
|
||||||
* *
|
|
||||||
**************************************************************************/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the style class to 'jfx-rippler'.
|
|
||||||
* <p>
|
|
||||||
* This is the selector class from which CSS can be used to style
|
|
||||||
* this control.
|
|
||||||
*/
|
|
||||||
private static final String DEFAULT_STYLE_CLASS = "jfx-rippler";
|
|
||||||
|
|
||||||
private void initialize() {
|
|
||||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the ripple recenter property, by default it's false.
|
|
||||||
* if true the ripple effect will show gravitational pull to the center of its control
|
|
||||||
*/
|
|
||||||
private StyleableBooleanProperty ripplerRecenter;
|
|
||||||
|
|
||||||
public boolean isRipplerRecenter() {
|
|
||||||
return ripplerRecenter != null && ripplerRecenter.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public StyleableBooleanProperty ripplerRecenterProperty() {
|
|
||||||
if (this.ripplerRecenter == null) {
|
|
||||||
this.ripplerRecenter = new SimpleStyleableBooleanProperty(
|
|
||||||
StyleableProperties.RIPPLER_RECENTER,
|
|
||||||
JFXRippler.this,
|
|
||||||
"ripplerRecenter",
|
|
||||||
false);
|
|
||||||
}
|
|
||||||
return this.ripplerRecenter;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setRipplerRecenter(boolean recenter) {
|
|
||||||
ripplerRecenterProperty().set(recenter);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the ripple radius size, by default it will be automatically computed.
|
|
||||||
*/
|
|
||||||
private StyleableDoubleProperty ripplerRadius;
|
|
||||||
|
|
||||||
public double getRipplerRadius() {
|
|
||||||
return ripplerRadius == null ? Region.USE_COMPUTED_SIZE : ripplerRadius.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public StyleableDoubleProperty ripplerRadiusProperty() {
|
|
||||||
if (this.ripplerRadius == null) {
|
|
||||||
this.ripplerRadius = new SimpleStyleableDoubleProperty(
|
|
||||||
StyleableProperties.RIPPLER_RADIUS,
|
|
||||||
JFXRippler.this,
|
|
||||||
"ripplerRadius",
|
|
||||||
Region.USE_COMPUTED_SIZE);
|
|
||||||
}
|
|
||||||
return this.ripplerRadius;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setRipplerRadius(double radius) {
|
|
||||||
ripplerRadiusProperty().set(radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final Color DEFAULT_RIPPLER_FILL = Color.rgb(0, 200, 255);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the default color of the ripple effect
|
|
||||||
*/
|
|
||||||
private StyleableObjectProperty<Paint> ripplerFill;
|
|
||||||
|
|
||||||
public Paint getRipplerFill() {
|
|
||||||
return ripplerFill == null ? DEFAULT_RIPPLER_FILL : ripplerFill.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public StyleableObjectProperty<Paint> ripplerFillProperty() {
|
|
||||||
if (this.ripplerFill == null) {
|
|
||||||
this.ripplerFill = new SimpleStyleableObjectProperty<>(StyleableProperties.RIPPLER_FILL,
|
|
||||||
JFXRippler.this,
|
|
||||||
"ripplerFill",
|
|
||||||
DEFAULT_RIPPLER_FILL);
|
|
||||||
}
|
|
||||||
return this.ripplerFill;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setRipplerFill(Paint color) {
|
|
||||||
ripplerFillProperty().set(color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// mask property used for clipping the rippler.
|
|
||||||
/// can be either CIRCLE/RECT
|
|
||||||
private StyleableObjectProperty<RipplerMask> maskType;
|
|
||||||
|
|
||||||
public RipplerMask getMaskType() {
|
|
||||||
return maskType == null ? RipplerMask.RECT : maskType.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public StyleableObjectProperty<RipplerMask> maskTypeProperty() {
|
|
||||||
if (this.maskType == null) {
|
|
||||||
this.maskType = new SimpleStyleableObjectProperty<>(
|
|
||||||
StyleableProperties.MASK_TYPE,
|
|
||||||
JFXRippler.this,
|
|
||||||
"maskType",
|
|
||||||
RipplerMask.RECT);
|
|
||||||
}
|
|
||||||
return this.maskType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMaskType(RipplerMask type) {
|
|
||||||
if (this.maskType != null || type != RipplerMask.RECT)
|
|
||||||
maskTypeProperty().set(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the ripple disable, by default it's false.
|
|
||||||
* if true the ripple effect will be hidden
|
|
||||||
*/
|
|
||||||
private StyleableBooleanProperty ripplerDisabled;
|
|
||||||
|
|
||||||
public boolean isRipplerDisabled() {
|
|
||||||
return ripplerDisabled != null && ripplerDisabled.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public StyleableBooleanProperty ripplerDisabledProperty() {
|
|
||||||
if (this.ripplerDisabled == null) {
|
|
||||||
this.ripplerDisabled = new SimpleStyleableBooleanProperty(
|
|
||||||
StyleableProperties.RIPPLER_DISABLED,
|
|
||||||
JFXRippler.this,
|
|
||||||
"ripplerDisabled",
|
|
||||||
false);
|
|
||||||
}
|
|
||||||
return this.ripplerDisabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setRipplerDisabled(boolean disabled) {
|
|
||||||
ripplerDisabledProperty().set(disabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* indicates whether the ripple effect is infront of or behind the node
|
|
||||||
*/
|
|
||||||
protected ObjectProperty<RipplerPos> position = new SimpleObjectProperty<>();
|
|
||||||
|
|
||||||
public void setPosition(RipplerPos pos) {
|
|
||||||
this.position.set(pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
public RipplerPos getPosition() {
|
|
||||||
return position == null ? RipplerPos.FRONT : position.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ObjectProperty<RipplerPos> positionProperty() {
|
|
||||||
return this.position;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class StyleableProperties {
|
|
||||||
private static final CssMetaData<JFXRippler, Boolean> RIPPLER_RECENTER =
|
|
||||||
new CssMetaData<>("-jfx-rippler-recenter",
|
|
||||||
BooleanConverter.getInstance(), false) {
|
|
||||||
@Override
|
|
||||||
public boolean isSettable(JFXRippler control) {
|
|
||||||
return control.ripplerRecenter == null || !control.ripplerRecenter.isBound();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public StyleableProperty<Boolean> getStyleableProperty(JFXRippler control) {
|
|
||||||
return control.ripplerRecenterProperty();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
private static final CssMetaData<JFXRippler, Boolean> RIPPLER_DISABLED =
|
|
||||||
new CssMetaData<>("-jfx-rippler-disabled",
|
|
||||||
BooleanConverter.getInstance(), false) {
|
|
||||||
@Override
|
|
||||||
public boolean isSettable(JFXRippler control) {
|
|
||||||
return control.ripplerDisabled == null || !control.ripplerDisabled.isBound();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public StyleableProperty<Boolean> getStyleableProperty(JFXRippler control) {
|
|
||||||
return control.ripplerDisabledProperty();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
private static final CssMetaData<JFXRippler, Paint> RIPPLER_FILL =
|
|
||||||
new CssMetaData<>("-jfx-rippler-fill",
|
|
||||||
PaintConverter.getInstance(), DEFAULT_RIPPLER_FILL) {
|
|
||||||
@Override
|
|
||||||
public boolean isSettable(JFXRippler control) {
|
|
||||||
return control.ripplerFill == null || !control.ripplerFill.isBound();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public StyleableProperty<Paint> getStyleableProperty(JFXRippler control) {
|
|
||||||
return control.ripplerFillProperty();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
private static final CssMetaData<JFXRippler, Number> RIPPLER_RADIUS =
|
|
||||||
new CssMetaData<>("-jfx-rippler-radius",
|
|
||||||
SizeConverter.getInstance(), Region.USE_COMPUTED_SIZE) {
|
|
||||||
@Override
|
|
||||||
public boolean isSettable(JFXRippler control) {
|
|
||||||
return control.ripplerRadius == null || !control.ripplerRadius.isBound();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public StyleableProperty<Number> getStyleableProperty(JFXRippler control) {
|
|
||||||
return control.ripplerRadiusProperty();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
private static final CssMetaData<JFXRippler, RipplerMask> MASK_TYPE =
|
|
||||||
new CssMetaData<>("-jfx-mask-type",
|
|
||||||
RipplerMaskTypeConverter.getInstance(), RipplerMask.RECT) {
|
|
||||||
@Override
|
|
||||||
public boolean isSettable(JFXRippler control) {
|
|
||||||
return control.maskType == null || !control.maskType.isBound();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public StyleableProperty<RipplerMask> getStyleableProperty(JFXRippler control) {
|
|
||||||
return control.maskTypeProperty();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
|
|
||||||
|
|
||||||
static {
|
|
||||||
final List<CssMetaData<? extends Styleable, ?>> styleables =
|
|
||||||
new ArrayList<>(StackPane.getClassCssMetaData());
|
|
||||||
Collections.addAll(styleables,
|
|
||||||
RIPPLER_RECENTER,
|
|
||||||
RIPPLER_RADIUS,
|
|
||||||
RIPPLER_FILL,
|
|
||||||
MASK_TYPE,
|
|
||||||
RIPPLER_DISABLED
|
|
||||||
);
|
|
||||||
STYLEABLES = Collections.unmodifiableList(styleables);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
|
|
||||||
return getClassCssMetaData();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
|
|
||||||
return StyleableProperties.STYLEABLES;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.jfoenix.controls;
|
|
||||||
|
|
||||||
import com.jfoenix.converters.IndicatorPositionConverter;
|
|
||||||
import com.jfoenix.skins.JFXSliderSkin;
|
|
||||||
import javafx.beans.binding.StringBinding;
|
|
||||||
import javafx.beans.property.ObjectProperty;
|
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
|
||||||
import javafx.css.*;
|
|
||||||
import javafx.scene.control.Skin;
|
|
||||||
import javafx.scene.control.Slider;
|
|
||||||
import javafx.util.Callback;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/// JFXSlider is the material design implementation of a slider.
|
|
||||||
///
|
|
||||||
/// @author Bashir Elias & Shadi Shaheen
|
|
||||||
/// @version 1.0
|
|
||||||
/// @since 2016-03-09
|
|
||||||
public class JFXSlider extends Slider {
|
|
||||||
|
|
||||||
public JFXSlider() {
|
|
||||||
super(0, 100, 50);
|
|
||||||
initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
public JFXSlider(double min, double max, double value) {
|
|
||||||
super(min, max, value);
|
|
||||||
initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Skin<?> createDefaultSkin() {
|
|
||||||
return new JFXSliderSkin(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initialize() {
|
|
||||||
getStyleClass().add(DEFAULT_STYLE_CLASS);
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum IndicatorPosition {
|
|
||||||
LEFT, RIGHT
|
|
||||||
}
|
|
||||||
|
|
||||||
/***************************************************************************
|
|
||||||
* *
|
|
||||||
* Properties *
|
|
||||||
* *
|
|
||||||
**************************************************************************/
|
|
||||||
|
|
||||||
/// String binding factory for the slider value.
|
|
||||||
/// Sets a custom string for the value text (by default, it shows the value rounded to the nearest whole number).
|
|
||||||
///
|
|
||||||
///
|
|
||||||
/// For example, to have the value displayed as a percentage (assuming the slider has a range of (0, 100)):
|
|
||||||
/// ```java
|
|
||||||
/// JFXSlider mySlider = ...
|
|
||||||
/// mySlider.setValueFactory(slider ->
|
|
||||||
/// Bindings.createStringBinding(
|
|
||||||
/// () -> ((int) slider.getValue()) + "%",
|
|
||||||
/// slider.valueProperty()
|
|
||||||
/// )
|
|
||||||
/// );
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// NOTE: might be replaced later with a call back to create the animated thumb node
|
|
||||||
///
|
|
||||||
/// @param callback a callback to create the string value binding
|
|
||||||
private ObjectProperty<Callback<JFXSlider, StringBinding>> valueFactory;
|
|
||||||
|
|
||||||
public final ObjectProperty<Callback<JFXSlider, StringBinding>> valueFactoryProperty() {
|
|
||||||
if (valueFactory == null) {
|
|
||||||
valueFactory = new SimpleObjectProperty<>(this, "valueFactory");
|
|
||||||
}
|
|
||||||
return valueFactory;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// @return the current slider value factory
|
|
||||||
public final Callback<JFXSlider, StringBinding> getValueFactory() {
|
|
||||||
return valueFactory == null ? null : valueFactory.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// sets custom string binding for the slider text value
|
|
||||||
///
|
|
||||||
/// @param valueFactory a callback to create the string value binding
|
|
||||||
public final void setValueFactory(final Callback<JFXSlider, StringBinding> valueFactory) {
|
|
||||||
this.valueFactoryProperty().set(valueFactory);
|
|
||||||
}
|
|
||||||
|
|
||||||
/***************************************************************************
|
|
||||||
* *
|
|
||||||
* Stylesheet Handling *
|
|
||||||
* *
|
|
||||||
**************************************************************************/
|
|
||||||
|
|
||||||
/// Initialize the style class to 'jfx-slider'.
|
|
||||||
///
|
|
||||||
/// This is the selector class from which CSS can be used to style
|
|
||||||
/// this control.
|
|
||||||
private static final String DEFAULT_STYLE_CLASS = "jfx-slider";
|
|
||||||
|
|
||||||
/// indicates the position of the slider indicator, can be
|
|
||||||
/// either LEFT or RIGHT
|
|
||||||
private StyleableObjectProperty<IndicatorPosition> indicatorPosition;
|
|
||||||
|
|
||||||
public StyleableObjectProperty<IndicatorPosition> indicatorPositionProperty() {
|
|
||||||
if (indicatorPosition == null) {
|
|
||||||
indicatorPosition = new SimpleStyleableObjectProperty<>(
|
|
||||||
StyleableProperties.INDICATOR_POSITION,
|
|
||||||
JFXSlider.this,
|
|
||||||
"indicatorPosition",
|
|
||||||
IndicatorPosition.LEFT);
|
|
||||||
}
|
|
||||||
return this.indicatorPosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IndicatorPosition getIndicatorPosition() {
|
|
||||||
return indicatorPosition == null ? IndicatorPosition.LEFT : indicatorPosition.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setIndicatorPosition(IndicatorPosition pos) {
|
|
||||||
indicatorPositionProperty().set(pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class StyleableProperties {
|
|
||||||
private static final CssMetaData<JFXSlider, IndicatorPosition> INDICATOR_POSITION = new CssMetaData<>(
|
|
||||||
"-jfx-indicator-position",
|
|
||||||
IndicatorPositionConverter.getInstance(),
|
|
||||||
IndicatorPosition.LEFT) {
|
|
||||||
@Override
|
|
||||||
public boolean isSettable(JFXSlider control) {
|
|
||||||
return control.indicatorPosition == null || !control.indicatorPosition.isBound();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public StyleableProperty<IndicatorPosition> getStyleableProperty(JFXSlider control) {
|
|
||||||
return control.indicatorPositionProperty();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
|
|
||||||
|
|
||||||
static {
|
|
||||||
final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(
|
|
||||||
Slider.getClassCssMetaData());
|
|
||||||
Collections.addAll(styleables, INDICATOR_POSITION);
|
|
||||||
CHILD_STYLEABLES = List.copyOf(styleables);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
|
|
||||||
return getClassCssMetaData();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
|
|
||||||
return StyleableProperties.CHILD_STYLEABLES;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,421 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.jfoenix.controls;
|
|
||||||
|
|
||||||
import javafx.animation.KeyFrame;
|
|
||||||
import javafx.animation.KeyValue;
|
|
||||||
import javafx.animation.PauseTransition;
|
|
||||||
import javafx.animation.Timeline;
|
|
||||||
import javafx.application.Platform;
|
|
||||||
import javafx.beans.value.ChangeListener;
|
|
||||||
import javafx.beans.value.WeakChangeListener;
|
|
||||||
import javafx.css.PseudoClass;
|
|
||||||
import javafx.event.Event;
|
|
||||||
import javafx.event.EventType;
|
|
||||||
import javafx.geometry.Bounds;
|
|
||||||
import javafx.scene.Group;
|
|
||||||
import javafx.scene.Node;
|
|
||||||
import javafx.scene.layout.Pane;
|
|
||||||
import javafx.scene.layout.StackPane;
|
|
||||||
import javafx.util.Duration;
|
|
||||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
|
||||||
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
|
|
||||||
/// "Snackbars provide brief messages about app processes at the bottom of the screen"
|
|
||||||
/// (<a href="https://material.io/design/components/snackbars.html#">Material Design Guidelines</a>).
|
|
||||||
///
|
|
||||||
/// To show a snackbar you need to
|
|
||||||
/// <ol>
|
|
||||||
/// - Have a [Pane] (snackbarContainer) to show the snackbar on top of. Register it in
|
|
||||||
/// [the JFXSnackbar constructor][#JFXSnackbar(Pane)] or using the [#registerSnackbarContainer(Pane)] method.
|
|
||||||
/// - Have or create a [JFXSnackbar]. - Having one snackbar where you pass all your
|
|
||||||
/// [SnackbarEvents][JFXSnackbar.SnackbarEvent] will ensure that the [enqueuemethod][JFXSnackbar#enqueue(SnackbarEvent)] works as intended.
|
|
||||||
///
|
|
||||||
/// - Have something to show in the snackbar. A [JFXSnackbarLayout] is nice and pretty,
|
|
||||||
/// but any arbitrary [Node] will do.
|
|
||||||
/// - Create a [SnackbarEvent][JFXSnackbar.SnackbarEvent] specifying the contents and the
|
|
||||||
/// duration.
|
|
||||||
/// </ol>
|
|
||||||
///
|
|
||||||
/// Finally, with all those things prepared, show your snackbar using
|
|
||||||
/// [snackbar.enqueue(snackbarEvent);][JFXSnackbar#enqueue(SnackbarEvent)].
|
|
||||||
///
|
|
||||||
/// It's most convenient to create functions to do most of this (creating the layout and event) with the default
|
|
||||||
/// settings; that way all you need to do to show a snackbar is specify the message or just the message and the duration.
|
|
||||||
///
|
|
||||||
/// @see <a href="https://material.io/design/components/snackbars.html#"> The Material Design Snackbar</a>
|
|
||||||
public class JFXSnackbar extends Group {
|
|
||||||
|
|
||||||
private static final String DEFAULT_STYLE_CLASS = "jfx-snackbar";
|
|
||||||
|
|
||||||
private Pane snackbarContainer;
|
|
||||||
private final ChangeListener<? super Number> sizeListener = (o, oldVal, newVal) -> refreshPopup();
|
|
||||||
private final WeakChangeListener<? super Number> weakSizeListener = new WeakChangeListener<>(sizeListener);
|
|
||||||
|
|
||||||
private final AtomicBoolean processingQueue = new AtomicBoolean(false);
|
|
||||||
private final ConcurrentLinkedQueue<SnackbarEvent> eventQueue = new ConcurrentLinkedQueue<>();
|
|
||||||
private final ConcurrentHashMap.KeySetView<Object, Boolean> eventsSet = ConcurrentHashMap.newKeySet();
|
|
||||||
|
|
||||||
private final Pane content;
|
|
||||||
private PseudoClass activePseudoClass = null;
|
|
||||||
private PauseTransition pauseTransition;
|
|
||||||
|
|
||||||
/// This constructor assumes that you will eventually call the [#registerSnackbarContainer(Pane)] method before
|
|
||||||
/// calling the [#enqueue(SnackbarEvent)] method. Otherwise, how will the snackbar know where to show itself?
|
|
||||||
///
|
|
||||||
///
|
|
||||||
/// "Snackbars provide brief messages about app processes at the bottom of the screen"
|
|
||||||
/// (<a href="https://material.io/design/components/snackbars.html#">Material Design Guidelines</a>).
|
|
||||||
///
|
|
||||||
/// To show a snackbar you need to
|
|
||||||
///
|
|
||||||
/// - Have a [Pane] (snackbarContainer) to show the snackbar on top of. Register it in
|
|
||||||
/// [the JFXSnackbar constructor][#JFXSnackbar(Pane)] or using the [#registerSnackbarContainer(Pane)] method.
|
|
||||||
/// - Have or create a [JFXSnackbar]. - Having one snackbar where you pass all your
|
|
||||||
/// [SnackbarEvents][JFXSnackbar.SnackbarEvent] will ensure that the [enqueuemethod][JFXSnackbar#enqueue(SnackbarEvent)] works as intended.
|
|
||||||
/// - Have something to show in the snackbar. A [JFXSnackbarLayout] is nice and pretty,
|
|
||||||
/// but any arbitrary [Node] will do.
|
|
||||||
/// - Create a [SnackbarEvent][JFXSnackbar.SnackbarEvent] specifying the contents and the
|
|
||||||
/// duration.
|
|
||||||
///
|
|
||||||
/// Finally, with all those things prepared, show your snackbar using
|
|
||||||
/// [snackbar.enqueue(snackbarEvent);][JFXSnackbar#enqueue(SnackbarEvent)].
|
|
||||||
///
|
|
||||||
public JFXSnackbar() {
|
|
||||||
this(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// "Snackbars provide brief messages about app processes at the bottom of the screen"
|
|
||||||
/// (<a href="https://material.io/design/components/snackbars.html#">Material Design Guidelines</a>).
|
|
||||||
///
|
|
||||||
/// To show a snackbar you need to
|
|
||||||
///
|
|
||||||
/// - Have a [Pane] (snackbarContainer) to show the snackbar on top of. Register it in
|
|
||||||
/// [the JFXSnackbar constructor][#JFXSnackbar(Pane)] or using the [#registerSnackbarContainer(Pane)] method.
|
|
||||||
/// - Have or create a [JFXSnackbar]. - Having one snackbar where you pass all your
|
|
||||||
/// [SnackbarEvents][JFXSnackbar.SnackbarEvent] will ensure that the [enqueuemethod][JFXSnackbar#enqueue(SnackbarEvent)] works as intended.
|
|
||||||
/// - Have something to show in the snackbar. A [JFXSnackbarLayout] is nice and pretty,
|
|
||||||
/// but any arbitrary [Node] will do.
|
|
||||||
/// - Create a [SnackbarEvent][JFXSnackbar.SnackbarEvent] specifying the contents and the
|
|
||||||
/// duration.
|
|
||||||
///
|
|
||||||
/// Finally, with all those things prepared, show your snackbar using
|
|
||||||
/// [snackbar.enqueue(snackbarEvent);][JFXSnackbar#enqueue(SnackbarEvent)].
|
|
||||||
///
|
|
||||||
/// @param snackbarContainer where the snackbar will appear. Using a single snackbar instead of many, will ensure that
|
|
||||||
/// the [#enqueue(SnackbarEvent)] method works correctly.
|
|
||||||
public JFXSnackbar(Pane snackbarContainer) {
|
|
||||||
initialize();
|
|
||||||
content = new StackPane();
|
|
||||||
content.getStyleClass().add("jfx-snackbar-content");
|
|
||||||
//wrap the content in a group so that the content is managed inside its own container
|
|
||||||
//but the group is not managed in the snackbarContainer so it does not affect any layout calculations
|
|
||||||
getChildren().add(content);
|
|
||||||
setManaged(false);
|
|
||||||
setVisible(false);
|
|
||||||
|
|
||||||
// register the container before resizing it
|
|
||||||
registerSnackbarContainer(snackbarContainer);
|
|
||||||
|
|
||||||
// resize the popup if its layout has been changed
|
|
||||||
layoutBoundsProperty().addListener((o, oldVal, newVal) -> refreshPopup());
|
|
||||||
|
|
||||||
addEventHandler(SnackbarEvent.SNACKBAR, this::enqueue);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initialize() {
|
|
||||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setters / Getters
|
|
||||||
|
|
||||||
public Pane getPopupContainer() {
|
|
||||||
return snackbarContainer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPrefWidth(double width) {
|
|
||||||
content.setPrefWidth(width);
|
|
||||||
}
|
|
||||||
|
|
||||||
public double getPrefWidth() {
|
|
||||||
return content.getPrefWidth();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Public API
|
|
||||||
|
|
||||||
public void registerSnackbarContainer(Pane snackbarContainer) {
|
|
||||||
if (snackbarContainer != null) {
|
|
||||||
if (this.snackbarContainer != null) {
|
|
||||||
//since listeners are added the container should be properly registered/unregistered
|
|
||||||
throw new IllegalArgumentException("Snackbar Container already set");
|
|
||||||
}
|
|
||||||
this.snackbarContainer = snackbarContainer;
|
|
||||||
this.snackbarContainer.getChildren().add(this);
|
|
||||||
this.snackbarContainer.heightProperty().addListener(weakSizeListener);
|
|
||||||
this.snackbarContainer.widthProperty().addListener(weakSizeListener);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void unregisterSnackbarContainer(Pane snackbarContainer) {
|
|
||||||
if (snackbarContainer != null) {
|
|
||||||
if (this.snackbarContainer == null) {
|
|
||||||
throw new IllegalArgumentException("Snackbar Container not set");
|
|
||||||
}
|
|
||||||
this.snackbarContainer.getChildren().remove(this);
|
|
||||||
this.snackbarContainer.heightProperty().removeListener(weakSizeListener);
|
|
||||||
this.snackbarContainer.widthProperty().removeListener(weakSizeListener);
|
|
||||||
this.snackbarContainer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void show(SnackbarEvent event) {
|
|
||||||
content.getChildren().setAll(event.getContent());
|
|
||||||
openAnimation = getTimeline(event.getTimeout());
|
|
||||||
if (event.getPseudoClass() != null) {
|
|
||||||
activePseudoClass = event.getPseudoClass();
|
|
||||||
content.pseudoClassStateChanged(activePseudoClass, true);
|
|
||||||
}
|
|
||||||
openAnimation.play();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Timeline openAnimation = null;
|
|
||||||
|
|
||||||
private Timeline getTimeline(Duration timeout) {
|
|
||||||
Timeline animation;
|
|
||||||
animation = new Timeline(
|
|
||||||
new KeyFrame(
|
|
||||||
Duration.ZERO,
|
|
||||||
e -> {
|
|
||||||
this.toBack();
|
|
||||||
this.setVisible(false);
|
|
||||||
},
|
|
||||||
new KeyValue(this.translateYProperty(), this.getLayoutBounds().getHeight(), Motion.EASE),
|
|
||||||
new KeyValue(this.opacityProperty(), 0, Motion.EASE)
|
|
||||||
),
|
|
||||||
new KeyFrame(
|
|
||||||
Duration.millis(10),
|
|
||||||
e -> {
|
|
||||||
this.toFront();
|
|
||||||
this.setVisible(true);
|
|
||||||
}
|
|
||||||
),
|
|
||||||
new KeyFrame(Duration.millis(300),
|
|
||||||
new KeyValue(this.opacityProperty(), 1, Motion.EASE),
|
|
||||||
new KeyValue(this.translateYProperty(), 0, Motion.EASE)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
animation.setCycleCount(1);
|
|
||||||
pauseTransition = Duration.INDEFINITE.equals(timeout) ? null : new PauseTransition(timeout);
|
|
||||||
if (pauseTransition != null) {
|
|
||||||
animation.setOnFinished(finish -> {
|
|
||||||
pauseTransition.setOnFinished(done -> {
|
|
||||||
pauseTransition = null;
|
|
||||||
eventsSet.remove(currentEvent);
|
|
||||||
currentEvent = eventQueue.peek();
|
|
||||||
close();
|
|
||||||
});
|
|
||||||
pauseTransition.play();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return animation;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void close() {
|
|
||||||
if (openAnimation != null) {
|
|
||||||
openAnimation.stop();
|
|
||||||
}
|
|
||||||
if (this.isVisible()) {
|
|
||||||
Timeline closeAnimation = new Timeline(
|
|
||||||
new KeyFrame(
|
|
||||||
Duration.ZERO,
|
|
||||||
e -> this.toFront(),
|
|
||||||
new KeyValue(this.opacityProperty(), 1, Motion.EASE),
|
|
||||||
new KeyValue(this.translateYProperty(), 0, Motion.EASE)
|
|
||||||
),
|
|
||||||
new KeyFrame(
|
|
||||||
Duration.millis(290),
|
|
||||||
e -> this.setVisible(true)
|
|
||||||
),
|
|
||||||
new KeyFrame(Duration.millis(300),
|
|
||||||
e -> {
|
|
||||||
this.toBack();
|
|
||||||
this.setVisible(false);
|
|
||||||
},
|
|
||||||
new KeyValue(this.translateYProperty(), this.getLayoutBounds().getHeight(), Motion.EASE),
|
|
||||||
new KeyValue(this.opacityProperty(), 0, Motion.EASE)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
closeAnimation.setCycleCount(1);
|
|
||||||
closeAnimation.setOnFinished(e -> {
|
|
||||||
resetPseudoClass();
|
|
||||||
processSnackbar();
|
|
||||||
});
|
|
||||||
closeAnimation.play();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private SnackbarEvent currentEvent = null;
|
|
||||||
|
|
||||||
public SnackbarEvent getCurrentEvent() {
|
|
||||||
return currentEvent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows {@link SnackbarEvent SnackbarEvents} one by one. The next event will be shown after the current event's duration.
|
|
||||||
*
|
|
||||||
* @param event the {@link SnackbarEvent event} to put in the queue.
|
|
||||||
*/
|
|
||||||
public void enqueue(SnackbarEvent event) {
|
|
||||||
synchronized (this) {
|
|
||||||
if (!eventsSet.contains(event)) {
|
|
||||||
eventsSet.add(event);
|
|
||||||
eventQueue.offer(event);
|
|
||||||
} else if (currentEvent == event && pauseTransition != null) {
|
|
||||||
pauseTransition.playFromStart();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (processingQueue.compareAndSet(false, true)) {
|
|
||||||
Platform.runLater(() -> {
|
|
||||||
currentEvent = eventQueue.poll();
|
|
||||||
if (currentEvent != null) {
|
|
||||||
show(currentEvent);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void resetPseudoClass() {
|
|
||||||
if (activePseudoClass != null) {
|
|
||||||
content.pseudoClassStateChanged(activePseudoClass, false);
|
|
||||||
activePseudoClass = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void processSnackbar() {
|
|
||||||
currentEvent = eventQueue.poll();
|
|
||||||
if (currentEvent != null) {
|
|
||||||
eventsSet.remove(currentEvent);
|
|
||||||
show(currentEvent);
|
|
||||||
} else {
|
|
||||||
//The enqueue method and this listener should be executed sequentially on the FX Thread so there
|
|
||||||
//should not be a race condition
|
|
||||||
processingQueue.getAndSet(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void refreshPopup() {
|
|
||||||
if (snackbarContainer == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Bounds contentBound = this.getLayoutBounds();
|
|
||||||
double offsetX = Math.ceil(snackbarContainer.getWidth() / 2) - Math.ceil(contentBound.getWidth() / 2);
|
|
||||||
double offsetY = snackbarContainer.getHeight() - contentBound.getHeight();
|
|
||||||
this.setLayoutX(offsetX);
|
|
||||||
this.setLayoutY(offsetY);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////
|
|
||||||
// Event API
|
|
||||||
///////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
/// Specifies _what_ and _how long_ to show a [JFXSnackbar].
|
|
||||||
///
|
|
||||||
/// The _what_ can be any arbitrary [Node]; the [JFXSnackbarLayout] is a great choice.
|
|
||||||
///
|
|
||||||
/// The _how long_ is specified in the form of a [javafx.util.Duration][javafx.util.Duration], not to be
|
|
||||||
/// confused with the [java.time.Duration].
|
|
||||||
public static class SnackbarEvent extends Event {
|
|
||||||
|
|
||||||
public static final EventType<SnackbarEvent> SNACKBAR = new EventType<>(Event.ANY, "SNACKBAR");
|
|
||||||
|
|
||||||
/// The amount of time the snackbar will show for, if not otherwise specified.
|
|
||||||
///
|
|
||||||
/// It's 1.5 seconds.
|
|
||||||
public static Duration DEFAULT_DURATION = Duration.seconds(1.5);
|
|
||||||
|
|
||||||
private final Node content;
|
|
||||||
private final PseudoClass pseudoClass;
|
|
||||||
private final Duration timeout;
|
|
||||||
|
|
||||||
/// Creates a [SnackbarEvent] with the [default duration][#DEFAULT_DURATION] and no pseudoClass.
|
|
||||||
///
|
|
||||||
/// @param content what you want shown in the snackbar; a [JFXSnackbarLayout] is a great choice.
|
|
||||||
public SnackbarEvent(Node content) {
|
|
||||||
this(content, DEFAULT_DURATION, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a [SnackbarEvent] with the [default duration][#DEFAULT_DURATION]; you specify the contents and
|
|
||||||
/// pseudoClass.
|
|
||||||
///
|
|
||||||
/// @param content what you want shown in the snackbar; a [JFXSnackbarLayout] is a great choice.
|
|
||||||
public SnackbarEvent(Node content, PseudoClass pseudoClass) {
|
|
||||||
this(content, DEFAULT_DURATION, pseudoClass);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a SnackbarEvent with no pseudoClass; you specify the contents and duration.
|
|
||||||
/// pseudoClass.
|
|
||||||
///
|
|
||||||
/// @param content what you want shown in the snackbar; a [JFXSnackbarLayout] is a great choice.
|
|
||||||
/// @param timeout the amount of time you want the snackbar to show for.
|
|
||||||
public SnackbarEvent(Node content, Duration timeout) {
|
|
||||||
this(content, timeout, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a SnackbarEvent; you specify the contents, duration and pseudoClass.
|
|
||||||
///
|
|
||||||
/// If you don't need so much customization, try one of the other constructors.
|
|
||||||
///
|
|
||||||
/// @param content what you want shown in the snackbar; a [JFXSnackbarLayout] is a great choice.
|
|
||||||
/// @param timeout the amount of time you want the snackbar to show for.
|
|
||||||
public SnackbarEvent(Node content, Duration timeout, PseudoClass pseudoClass) {
|
|
||||||
super(SNACKBAR);
|
|
||||||
this.content = content;
|
|
||||||
this.pseudoClass = pseudoClass;
|
|
||||||
this.timeout = timeout;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Node getContent() {
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
public PseudoClass getPseudoClass() {
|
|
||||||
return pseudoClass;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Duration getTimeout() {
|
|
||||||
return timeout;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public EventType<? extends SnackbarEvent> getEventType() {
|
|
||||||
return (EventType<? extends SnackbarEvent>) super.getEventType();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isPersistent() {
|
|
||||||
return Duration.INDEFINITE.equals(getTimeout());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.jfoenix.controls;
|
|
||||||
|
|
||||||
import javafx.beans.binding.Bindings;
|
|
||||||
import javafx.event.ActionEvent;
|
|
||||||
import javafx.event.EventHandler;
|
|
||||||
import javafx.geometry.Insets;
|
|
||||||
import javafx.scene.control.Control;
|
|
||||||
import javafx.scene.control.Label;
|
|
||||||
import javafx.scene.layout.BorderPane;
|
|
||||||
import javafx.scene.layout.StackPane;
|
|
||||||
|
|
||||||
/// JFXSnackbarLayout default layout for snackbar content
|
|
||||||
///
|
|
||||||
/// @author Shadi Shaheen
|
|
||||||
/// @version 1.0
|
|
||||||
/// @since 2018-11-16
|
|
||||||
public class JFXSnackbarLayout extends BorderPane {
|
|
||||||
|
|
||||||
private final Label toast;
|
|
||||||
private JFXButton action;
|
|
||||||
private final StackPane actionContainer;
|
|
||||||
|
|
||||||
public JFXSnackbarLayout(String message) {
|
|
||||||
this(message, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public JFXSnackbarLayout(String message, String actionText, EventHandler<ActionEvent> actionHandler) {
|
|
||||||
initialize();
|
|
||||||
|
|
||||||
toast = new Label();
|
|
||||||
toast.setMinWidth(Control.USE_PREF_SIZE);
|
|
||||||
toast.getStyleClass().add("jfx-snackbar-toast");
|
|
||||||
toast.setWrapText(true);
|
|
||||||
toast.setText(message);
|
|
||||||
StackPane toastContainer = new StackPane(toast);
|
|
||||||
toastContainer.setPadding(new Insets(20));
|
|
||||||
actionContainer = new StackPane();
|
|
||||||
actionContainer.setVisible(false);
|
|
||||||
actionContainer.setManaged(false);
|
|
||||||
|
|
||||||
toast.prefWidthProperty().bind(Bindings.createDoubleBinding(() -> {
|
|
||||||
if (getPrefWidth() == -1) {
|
|
||||||
return getPrefWidth();
|
|
||||||
}
|
|
||||||
double actionWidth = actionContainer.isVisible() ? actionContainer.getWidth() : 0.0;
|
|
||||||
return prefWidthProperty().get() - actionWidth;
|
|
||||||
}, prefWidthProperty(), actionContainer.widthProperty(), actionContainer.visibleProperty()));
|
|
||||||
|
|
||||||
setLeft(toastContainer);
|
|
||||||
setRight(actionContainer);
|
|
||||||
|
|
||||||
if (actionText != null) {
|
|
||||||
action = new JFXButton();
|
|
||||||
action.setText(actionText);
|
|
||||||
action.setOnAction(actionHandler);
|
|
||||||
action.setMinWidth(Control.USE_PREF_SIZE);
|
|
||||||
action.setButtonType(JFXButton.ButtonType.FLAT);
|
|
||||||
action.getStyleClass().add("jfx-snackbar-action");
|
|
||||||
// actions will be added upon showing the snackbar if needed
|
|
||||||
actionContainer.getChildren().add(action);
|
|
||||||
|
|
||||||
if (!actionText.isEmpty()) {
|
|
||||||
actionContainer.setPadding(new Insets(0, 10, 0, 0));
|
|
||||||
action.setVisible(true);
|
|
||||||
actionContainer.setVisible(true);
|
|
||||||
actionContainer.setManaged(true);
|
|
||||||
// to force updating the layout bounds
|
|
||||||
action.setText("");
|
|
||||||
action.setText(actionText);
|
|
||||||
action.setOnAction(actionHandler);
|
|
||||||
} else {
|
|
||||||
actionContainer.setVisible(false);
|
|
||||||
actionContainer.setManaged(false);
|
|
||||||
action.setVisible(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final String DEFAULT_STYLE_CLASS = "jfx-snackbar-layout";
|
|
||||||
|
|
||||||
public String getToast() {
|
|
||||||
return toast.getText();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setToast(String toast) {
|
|
||||||
this.toast.setText(toast);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initialize() {
|
|
||||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.jfoenix.controls;
|
|
||||||
|
|
||||||
import com.jfoenix.skins.JFXSpinnerSkin;
|
|
||||||
import javafx.css.CssMetaData;
|
|
||||||
import javafx.css.SimpleStyleableDoubleProperty;
|
|
||||||
import javafx.css.Styleable;
|
|
||||||
import javafx.css.StyleableDoubleProperty;
|
|
||||||
import javafx.css.converter.SizeConverter;
|
|
||||||
import javafx.scene.control.ProgressIndicator;
|
|
||||||
import javafx.scene.control.Skin;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.ThreadLocalRandom;
|
|
||||||
|
|
||||||
/// JFXSpinner is the material design implementation of a loading spinner.
|
|
||||||
///
|
|
||||||
/// @author Bashir Elias & Shadi Shaheen
|
|
||||||
/// @version 1.0
|
|
||||||
/// @since 2016-03-09
|
|
||||||
public class JFXSpinner extends ProgressIndicator {
|
|
||||||
|
|
||||||
public static final double INDETERMINATE_PROGRESS = ProgressIndicator.INDETERMINATE_PROGRESS;
|
|
||||||
|
|
||||||
public JFXSpinner() {
|
|
||||||
this(INDETERMINATE_PROGRESS);
|
|
||||||
}
|
|
||||||
|
|
||||||
public JFXSpinner(double progress) {
|
|
||||||
super(progress);
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void init() {
|
|
||||||
getStyleClass().add(DEFAULT_STYLE_CLASS);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Skin<?> createDefaultSkin() {
|
|
||||||
return new JFXSpinnerSkin(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/***************************************************************************
|
|
||||||
* *
|
|
||||||
* Stylesheet Handling *
|
|
||||||
* *
|
|
||||||
**************************************************************************/
|
|
||||||
|
|
||||||
/// Initialize the style class to 'jfx-spinner'.
|
|
||||||
///
|
|
||||||
/// This is the selector class from which CSS can be used to style
|
|
||||||
/// this control.
|
|
||||||
private static final String DEFAULT_STYLE_CLASS = "jfx-spinner";
|
|
||||||
|
|
||||||
private static final double DEFAULT_RADIUS = 16.0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* specifies the radius of the spinner node, by default it's set to `16.0`
|
|
||||||
*/
|
|
||||||
private StyleableDoubleProperty radius;
|
|
||||||
|
|
||||||
public final StyleableDoubleProperty radiusProperty() {
|
|
||||||
if (this.radius == null) {
|
|
||||||
this.radius = new SimpleStyleableDoubleProperty(StyleableProperties.RADIUS,
|
|
||||||
JFXSpinner.this,
|
|
||||||
"radius",
|
|
||||||
DEFAULT_RADIUS);
|
|
||||||
}
|
|
||||||
return this.radius;
|
|
||||||
}
|
|
||||||
|
|
||||||
public final double getRadius() {
|
|
||||||
return radius != null ? radius.get() : DEFAULT_RADIUS;
|
|
||||||
}
|
|
||||||
|
|
||||||
public final void setRadius(final double radius) {
|
|
||||||
this.radiusProperty().set(radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
public double getStartingAngle() {
|
|
||||||
return 360 - ThreadLocalRandom.current().nextDouble() * 720;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class StyleableProperties {
|
|
||||||
private static final CssMetaData<JFXSpinner, Number> RADIUS =
|
|
||||||
new CssMetaData<>("-jfx-radius",
|
|
||||||
SizeConverter.getInstance(), DEFAULT_RADIUS) {
|
|
||||||
@Override
|
|
||||||
public boolean isSettable(JFXSpinner control) {
|
|
||||||
return control.radius == null || !control.radius.isBound();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public StyleableDoubleProperty getStyleableProperty(JFXSpinner control) {
|
|
||||||
return control.radiusProperty();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
|
|
||||||
|
|
||||||
static {
|
|
||||||
final List<CssMetaData<? extends Styleable, ?>> styleables =
|
|
||||||
new ArrayList<>(ProgressIndicator.getClassCssMetaData());
|
|
||||||
Collections.addAll(styleables, RADIUS);
|
|
||||||
CHILD_STYLEABLES = List.copyOf(styleables);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
|
|
||||||
return getClassCssMetaData();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
|
|
||||||
return StyleableProperties.CHILD_STYLEABLES;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,288 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
|
||||||
package com.jfoenix.controls;
|
|
||||||
|
|
||||||
import com.jfoenix.skins.JFXSwitchSkin;
|
|
||||||
import javafx.beans.property.BooleanProperty;
|
|
||||||
import javafx.beans.property.ObjectProperty;
|
|
||||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
|
||||||
import javafx.beans.property.ReadOnlyBooleanWrapper;
|
|
||||||
import javafx.beans.property.SimpleBooleanProperty;
|
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
|
||||||
import javafx.css.CssMetaData;
|
|
||||||
import javafx.css.PseudoClass;
|
|
||||||
import javafx.css.SimpleStyleableDoubleProperty;
|
|
||||||
import javafx.css.Styleable;
|
|
||||||
import javafx.css.StyleableDoubleProperty;
|
|
||||||
import javafx.css.StyleablePropertyFactory;
|
|
||||||
import javafx.event.ActionEvent;
|
|
||||||
import javafx.event.EventHandler;
|
|
||||||
import javafx.scene.AccessibleAction;
|
|
||||||
import javafx.scene.AccessibleAttribute;
|
|
||||||
import javafx.scene.AccessibleRole;
|
|
||||||
import javafx.scene.control.Control;
|
|
||||||
import javafx.scene.control.Skin;
|
|
||||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
|
||||||
import org.jetbrains.annotations.NotNullByDefault;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
import org.jetbrains.annotations.Unmodifiable;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/// A Material Design 3 switch for a boolean setting.
|
|
||||||
///
|
|
||||||
/// Supports clicking, dragging, and Space-key activation. Labels belong to the surrounding layout;
|
|
||||||
/// supply an accessible text description. Size and colors are configurable through CSS.
|
|
||||||
@NotNullByDefault
|
|
||||||
public class JFXSwitch extends Control {
|
|
||||||
|
|
||||||
/// Creates an unselected switch.
|
|
||||||
public JFXSwitch() {
|
|
||||||
getStyleClass().setAll("jfx-switch");
|
|
||||||
setFocusTraversable(true);
|
|
||||||
setAccessibleRole(AccessibleRole.TOGGLE_BUTTON);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Selection state.
|
|
||||||
private final BooleanProperty selected = new SimpleBooleanProperty(this, "selected") {
|
|
||||||
/// Updates visual and accessible selection without firing an action.
|
|
||||||
@Override
|
|
||||||
protected void invalidated() {
|
|
||||||
pseudoClassStateChanged(SELECTED, get());
|
|
||||||
notifyAccessibleAttributeChanged(AccessibleAttribute.SELECTED);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Changes selection without firing an action event.
|
|
||||||
public final void setSelected(boolean value) {
|
|
||||||
selected.set(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns whether the switch is selected.
|
|
||||||
public final boolean isSelected() {
|
|
||||||
return selected.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the bindable selection state; the default is false.
|
|
||||||
public final BooleanProperty selectedProperty() {
|
|
||||||
return selected;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read-only activation state.
|
|
||||||
private final ReadOnlyBooleanWrapper armed = new ReadOnlyBooleanWrapper(this, "armed") {
|
|
||||||
/// Reflects input feedback in CSS.
|
|
||||||
@Override
|
|
||||||
protected void invalidated() {
|
|
||||||
pseudoClassStateChanged(ARMED, get());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Returns whether the switch is armed.
|
|
||||||
public final boolean isArmed() {
|
|
||||||
return armed.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the read-only state indicating a pending activation.
|
|
||||||
public final ReadOnlyBooleanProperty armedProperty() {
|
|
||||||
return armed.getReadOnlyProperty();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Optional action handler.
|
|
||||||
private @Nullable ObjectProperty<@Nullable EventHandler<ActionEvent>> onAction;
|
|
||||||
|
|
||||||
/// Sets or clears the action handler.
|
|
||||||
public final void setOnAction(@Nullable EventHandler<ActionEvent> handler) {
|
|
||||||
if (onAction != null || handler != null)
|
|
||||||
onActionProperty().set(handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the action handler, or null when none is installed.
|
|
||||||
public final @Nullable EventHandler<ActionEvent> getOnAction() {
|
|
||||||
return onAction == null ? null : onAction.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the handler property for activation events.
|
|
||||||
public final ObjectProperty<@Nullable EventHandler<ActionEvent>> onActionProperty() {
|
|
||||||
if (onAction == null) {
|
|
||||||
onAction = new SimpleObjectProperty<>(this, "onAction") {
|
|
||||||
/// Updates the action handler.
|
|
||||||
@Override
|
|
||||||
protected void invalidated() {
|
|
||||||
setEventHandler(ActionEvent.ACTION, get());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return onAction;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The requested track height in logical pixels.
|
|
||||||
private final StyleableDoubleProperty size = new SimpleStyleableDoubleProperty(SIZE, this, "size", 32.0) {
|
|
||||||
/// Validates the size and requests layout.
|
|
||||||
@Override
|
|
||||||
protected void invalidated() {
|
|
||||||
get();
|
|
||||||
requestLayout();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Sets the track height; see [#sizeProperty()].
|
|
||||||
public final void setSize(double value) {
|
|
||||||
size.set(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the requested track height in logical pixels.
|
|
||||||
public final double getSize() {
|
|
||||||
return size.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Track height in logical pixels, styled by `-jfx-size`; the default is 32.
|
|
||||||
/// Scales the artwork and interaction target proportionally; nonpositive or nonfinite sizes render at the default size.
|
|
||||||
public final StyleableDoubleProperty sizeProperty() {
|
|
||||||
return size;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Optional selected-icon property.
|
|
||||||
private @Nullable BooleanProperty showSelectedIcon;
|
|
||||||
|
|
||||||
/// Sets the value of [#showSelectedIconProperty()].
|
|
||||||
public final void setShowSelectedIcon(boolean value) {
|
|
||||||
if (showSelectedIcon != null || value)
|
|
||||||
showSelectedIconProperty().set(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns whether the selected icon is shown.
|
|
||||||
public final boolean isShowSelectedIcon() {
|
|
||||||
return showSelectedIcon != null && showSelectedIcon.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether to show a check mark when selected; defaults to false.
|
|
||||||
public final BooleanProperty showSelectedIconProperty() {
|
|
||||||
if (showSelectedIcon == null)
|
|
||||||
showSelectedIcon = createOptionProperty("showSelectedIcon", false, false);
|
|
||||||
return showSelectedIcon;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Optional unselected-icon property.
|
|
||||||
private @Nullable BooleanProperty showUnselectedIcon;
|
|
||||||
|
|
||||||
/// Sets the value of [#showUnselectedIconProperty()].
|
|
||||||
public final void setShowUnselectedIcon(boolean value) {
|
|
||||||
if (showUnselectedIcon != null || value)
|
|
||||||
showUnselectedIconProperty().set(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns whether the unselected icon is shown.
|
|
||||||
public final boolean isShowUnselectedIcon() {
|
|
||||||
return showUnselectedIcon != null && showUnselectedIcon.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether to show a cross when unselected; defaults to false.
|
|
||||||
public final BooleanProperty showUnselectedIconProperty() {
|
|
||||||
if (showUnselectedIcon == null)
|
|
||||||
showUnselectedIcon = createOptionProperty("showUnselectedIcon", false, false);
|
|
||||||
return showUnselectedIcon;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Optional animation policy override.
|
|
||||||
private @Nullable BooleanProperty disableAnimation;
|
|
||||||
|
|
||||||
/// Sets the value of [#disableAnimationProperty()].
|
|
||||||
public final void setDisableAnimation(boolean value) {
|
|
||||||
if (disableAnimation != null || value == AnimationUtils.isAnimationEnabled())
|
|
||||||
disableAnimationProperty().set(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns whether animation is disabled.
|
|
||||||
public final boolean isDisableAnimation() {
|
|
||||||
return disableAnimation == null ? !AnimationUtils.isAnimationEnabled() : disableAnimation.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Disables animation, defaulting to the inverse of the global animation-enabled setting.
|
|
||||||
public final BooleanProperty disableAnimationProperty() {
|
|
||||||
if (disableAnimation == null)
|
|
||||||
disableAnimation = createOptionProperty("disableAnimation", !AnimationUtils.isAnimationEnabled(), true);
|
|
||||||
return disableAnimation;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Arms input feedback unless the switch is disabled or its selection is bound.
|
|
||||||
public final void arm() {
|
|
||||||
if (!isDisabled() && !selected.isBound())
|
|
||||||
armed.set(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clears input feedback without firing an action.
|
|
||||||
public final void disarm() {
|
|
||||||
armed.set(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Toggles selection and fires one action; disabled or bound switches ignore activation.
|
|
||||||
public void fire() {
|
|
||||||
if (!isDisabled() && !selected.isBound()) {
|
|
||||||
setSelected(!isSelected());
|
|
||||||
fireEvent(new ActionEvent(this, this));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates the default switch skin.
|
|
||||||
@Override
|
|
||||||
protected Skin<?> createDefaultSkin() {
|
|
||||||
return new JFXSwitchSkin(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a property that updates the skin, optionally settling active animations.
|
|
||||||
private BooleanProperty createOptionProperty(String name, boolean initialValue, boolean finishAnimation) {
|
|
||||||
return new SimpleBooleanProperty(this, name, initialValue) {
|
|
||||||
/// Validates the value even without a skin, so subsequent changes also reach this callback.
|
|
||||||
@Override
|
|
||||||
protected void invalidated() {
|
|
||||||
get();
|
|
||||||
if (getSkin() instanceof JFXSwitchSkin skin)
|
|
||||||
skin.updateOptions(finishAnimation);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Indicates that the switch is selected.
|
|
||||||
private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
|
|
||||||
|
|
||||||
/// Indicates a pending activation.
|
|
||||||
private static final PseudoClass ARMED = PseudoClass.getPseudoClass("armed");
|
|
||||||
|
|
||||||
/// Factory for switch CSS metadata.
|
|
||||||
private static final StyleablePropertyFactory<JFXSwitch> STYLEABLES =
|
|
||||||
new StyleablePropertyFactory<>(Control.getClassCssMetaData());
|
|
||||||
|
|
||||||
/// CSS metadata for track height.
|
|
||||||
private static final CssMetaData<JFXSwitch, Number> SIZE =
|
|
||||||
STYLEABLES.createSizeCssMetaData("-jfx-size", control -> control.size, 32.0);
|
|
||||||
|
|
||||||
/// Switch and inherited CSS metadata.
|
|
||||||
private static final @Unmodifiable List<CssMetaData<? extends Styleable, ?>> CSS_META_DATA =
|
|
||||||
List.copyOf(STYLEABLES.getCssMetaData());
|
|
||||||
|
|
||||||
/// Returns immutable CSS metadata, including inherited control properties.
|
|
||||||
public static @Unmodifiable List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
|
|
||||||
return CSS_META_DATA;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the metadata used to style this switch.
|
|
||||||
@Override
|
|
||||||
public @Unmodifiable List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
|
|
||||||
return getClassCssMetaData();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Exposes selection to assistive technology.
|
|
||||||
@Override
|
|
||||||
public @Nullable Object queryAccessibleAttribute(AccessibleAttribute attribute, @Nullable Object... parameters) {
|
|
||||||
return attribute == AccessibleAttribute.SELECTED ? isSelected() : super.queryAccessibleAttribute(attribute, parameters);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handles accessible activation.
|
|
||||||
@Override
|
|
||||||
public void executeAccessibleAction(AccessibleAction action, @Nullable Object... parameters) {
|
|
||||||
if (action == AccessibleAction.FIRE)
|
|
||||||
fire();
|
|
||||||
else
|
|
||||||
super.executeAccessibleAction(action, parameters);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,297 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.jfoenix.controls;
|
|
||||||
|
|
||||||
import com.jfoenix.skins.JFXTextAreaSkin;
|
|
||||||
import com.jfoenix.validation.base.ValidatorBase;
|
|
||||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
|
||||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
|
||||||
import javafx.collections.FXCollections;
|
|
||||||
import javafx.collections.ObservableList;
|
|
||||||
import javafx.css.*;
|
|
||||||
import javafx.css.converter.BooleanConverter;
|
|
||||||
import javafx.css.converter.PaintConverter;
|
|
||||||
import javafx.scene.control.Control;
|
|
||||||
import javafx.scene.control.Skin;
|
|
||||||
import javafx.scene.control.TextArea;
|
|
||||||
import javafx.scene.paint.Color;
|
|
||||||
import javafx.scene.paint.Paint;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.ui.FXUtils.useJFXContextMenu;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JFXTextArea is the material design implementation of a text area.
|
|
||||||
*
|
|
||||||
* @author Shadi Shaheen
|
|
||||||
* @version 1.0
|
|
||||||
* @since 2016-03-09
|
|
||||||
*/
|
|
||||||
public class JFXTextArea extends TextArea {
|
|
||||||
/**
|
|
||||||
* Initialize the style class to 'jfx-text-field'.
|
|
||||||
* <p>
|
|
||||||
* This is the selector class from which CSS can be used to style
|
|
||||||
* this control.
|
|
||||||
*/
|
|
||||||
private static final String DEFAULT_STYLE_CLASS = "jfx-text-area";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
public JFXTextArea() {
|
|
||||||
initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
public JFXTextArea(String text) {
|
|
||||||
super(text);
|
|
||||||
initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected Skin<?> createDefaultSkin() {
|
|
||||||
return new JFXTextAreaSkin(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initialize() {
|
|
||||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
|
||||||
if ("dalvik".equalsIgnoreCase(System.getProperty("java.vm.name"))) {
|
|
||||||
this.setStyle("-fx-skin: \"com.jfoenix.android.skins.JFXTextAreaSkinAndroid\";");
|
|
||||||
}
|
|
||||||
|
|
||||||
useJFXContextMenu(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/***************************************************************************
|
|
||||||
* *
|
|
||||||
* Properties *
|
|
||||||
* *
|
|
||||||
**************************************************************************/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* holds the current active validator on the text area in case of validation error
|
|
||||||
*/
|
|
||||||
private ReadOnlyObjectWrapper<ValidatorBase> activeValidator = new ReadOnlyObjectWrapper<>();
|
|
||||||
|
|
||||||
public ValidatorBase getActiveValidator() {
|
|
||||||
return activeValidator == null ? null : activeValidator.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ReadOnlyObjectProperty<ValidatorBase> activeValidatorProperty() {
|
|
||||||
return this.activeValidator.getReadOnlyProperty();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* list of validators that will validate the text value upon calling
|
|
||||||
* {{@link #validate()}
|
|
||||||
*/
|
|
||||||
private ObservableList<ValidatorBase> validators = FXCollections.observableArrayList();
|
|
||||||
|
|
||||||
public ObservableList<ValidatorBase> getValidators() {
|
|
||||||
return validators;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setValidators(ValidatorBase... validators) {
|
|
||||||
this.validators.addAll(validators);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* validates the text value using the list of validators provided by the user
|
|
||||||
* {{@link #setValidators(ValidatorBase...)}
|
|
||||||
*
|
|
||||||
* @return true if the value is valid else false
|
|
||||||
*/
|
|
||||||
public boolean validate() {
|
|
||||||
for (ValidatorBase validator : validators) {
|
|
||||||
if (validator.getSrcControl() == null) {
|
|
||||||
validator.setSrcControl(this);
|
|
||||||
}
|
|
||||||
validator.validate();
|
|
||||||
if (validator.getHasErrors()) {
|
|
||||||
activeValidator.set(validator);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
activeValidator.set(null);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void resetValidation() {
|
|
||||||
pseudoClassStateChanged(ValidatorBase.PSEUDO_CLASS_ERROR, false);
|
|
||||||
activeValidator.set(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/***************************************************************************
|
|
||||||
* *
|
|
||||||
* styleable Properties *
|
|
||||||
* *
|
|
||||||
**************************************************************************/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* set true to show a float the prompt text when focusing the field
|
|
||||||
*/
|
|
||||||
private StyleableBooleanProperty labelFloat = new SimpleStyleableBooleanProperty(StyleableProperties.LABEL_FLOAT, JFXTextArea.this, "lableFloat", false);
|
|
||||||
|
|
||||||
public final StyleableBooleanProperty labelFloatProperty() {
|
|
||||||
return this.labelFloat;
|
|
||||||
}
|
|
||||||
|
|
||||||
public final boolean isLabelFloat() {
|
|
||||||
return this.labelFloatProperty().get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public final void setLabelFloat(final boolean labelFloat) {
|
|
||||||
this.labelFloatProperty().set(labelFloat);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* default color used when the text area is unfocused
|
|
||||||
*/
|
|
||||||
private StyleableObjectProperty<Paint> unFocusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.UNFOCUS_COLOR, JFXTextArea.this, "unFocusColor", Color.rgb(77, 77, 77));
|
|
||||||
|
|
||||||
public Paint getUnFocusColor() {
|
|
||||||
return unFocusColor == null ? Color.rgb(77, 77, 77) : unFocusColor.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public StyleableObjectProperty<Paint> unFocusColorProperty() {
|
|
||||||
return this.unFocusColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setUnFocusColor(Paint color) {
|
|
||||||
this.unFocusColor.set(color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* default color used when the text area is focused
|
|
||||||
*/
|
|
||||||
private StyleableObjectProperty<Paint> focusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.FOCUS_COLOR, JFXTextArea.this, "focusColor", Color.valueOf("#4059A9"));
|
|
||||||
|
|
||||||
public Paint getFocusColor() {
|
|
||||||
return focusColor == null ? Color.valueOf("#4059A9") : focusColor.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public StyleableObjectProperty<Paint> focusColorProperty() {
|
|
||||||
return this.focusColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setFocusColor(Paint color) {
|
|
||||||
this.focusColor.set(color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* disable animation on validation
|
|
||||||
*/
|
|
||||||
private StyleableBooleanProperty disableAnimation = new SimpleStyleableBooleanProperty(StyleableProperties.DISABLE_ANIMATION, JFXTextArea.this, "disableAnimation", false);
|
|
||||||
|
|
||||||
public final StyleableBooleanProperty disableAnimationProperty() {
|
|
||||||
return this.disableAnimation;
|
|
||||||
}
|
|
||||||
|
|
||||||
public final Boolean isDisableAnimation() {
|
|
||||||
return disableAnimation != null && this.disableAnimationProperty().get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public final void setDisableAnimation(final Boolean disabled) {
|
|
||||||
this.disableAnimationProperty().set(disabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
private final static class StyleableProperties {
|
|
||||||
private static final CssMetaData<JFXTextArea, Paint> UNFOCUS_COLOR = new CssMetaData<JFXTextArea, Paint>("-jfx-unfocus-color", PaintConverter.getInstance(), Color.rgb(77, 77, 77)) {
|
|
||||||
@Override
|
|
||||||
public boolean isSettable(JFXTextArea control) {
|
|
||||||
return control.unFocusColor == null || !control.unFocusColor.isBound();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public StyleableProperty<Paint> getStyleableProperty(JFXTextArea control) {
|
|
||||||
return control.unFocusColorProperty();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
private static final CssMetaData<JFXTextArea, Paint> FOCUS_COLOR = new CssMetaData<JFXTextArea, Paint>("-jfx-focus-color", PaintConverter.getInstance(), Color.valueOf("#4059A9")) {
|
|
||||||
@Override
|
|
||||||
public boolean isSettable(JFXTextArea control) {
|
|
||||||
return control.focusColor == null || !control.focusColor.isBound();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public StyleableProperty<Paint> getStyleableProperty(JFXTextArea control) {
|
|
||||||
return control.focusColorProperty();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
private static final CssMetaData<JFXTextArea, Boolean> LABEL_FLOAT = new CssMetaData<JFXTextArea, Boolean>("-jfx-label-float", BooleanConverter.getInstance(), false) {
|
|
||||||
@Override
|
|
||||||
public boolean isSettable(JFXTextArea control) {
|
|
||||||
return control.labelFloat == null || !control.labelFloat.isBound();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public StyleableBooleanProperty getStyleableProperty(JFXTextArea control) {
|
|
||||||
return control.labelFloatProperty();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private static final CssMetaData<JFXTextArea, Boolean> DISABLE_ANIMATION = new CssMetaData<JFXTextArea, Boolean>("-jfx-disable-animation", BooleanConverter.getInstance(), false) {
|
|
||||||
@Override
|
|
||||||
public boolean isSettable(JFXTextArea control) {
|
|
||||||
return control.disableAnimation == null || !control.disableAnimation.isBound();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public StyleableBooleanProperty getStyleableProperty(JFXTextArea control) {
|
|
||||||
return control.disableAnimationProperty();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
|
|
||||||
|
|
||||||
static {
|
|
||||||
final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Control.getClassCssMetaData());
|
|
||||||
Collections.addAll(styleables, UNFOCUS_COLOR, FOCUS_COLOR, LABEL_FLOAT, DISABLE_ANIMATION);
|
|
||||||
CHILD_STYLEABLES = Collections.unmodifiableList(styleables);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// inherit the styleable properties from parent
|
|
||||||
private List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
|
|
||||||
if (STYLEABLES == null) {
|
|
||||||
final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Control.getClassCssMetaData());
|
|
||||||
styleables.addAll(getClassCssMetaData());
|
|
||||||
styleables.addAll(TextArea.getClassCssMetaData());
|
|
||||||
STYLEABLES = Collections.unmodifiableList(styleables);
|
|
||||||
}
|
|
||||||
return STYLEABLES;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
|
|
||||||
return StyleableProperties.CHILD_STYLEABLES;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,307 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.jfoenix.controls;
|
|
||||||
|
|
||||||
import com.jfoenix.skins.JFXTextFieldSkin;
|
|
||||||
import com.jfoenix.validation.base.ValidatorBase;
|
|
||||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
|
||||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
|
||||||
import javafx.collections.FXCollections;
|
|
||||||
import javafx.collections.ObservableList;
|
|
||||||
import javafx.css.*;
|
|
||||||
import javafx.css.converter.BooleanConverter;
|
|
||||||
import javafx.css.converter.PaintConverter;
|
|
||||||
import javafx.scene.control.Control;
|
|
||||||
import javafx.scene.control.Skin;
|
|
||||||
import javafx.scene.control.TextField;
|
|
||||||
import javafx.scene.paint.Color;
|
|
||||||
import javafx.scene.paint.Paint;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.ui.FXUtils.useJFXContextMenu;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JFXTextField is the material design implementation of a text Field.
|
|
||||||
*
|
|
||||||
* @author Shadi Shaheen
|
|
||||||
* @version 1.0
|
|
||||||
* @since 2016-03-09
|
|
||||||
*/
|
|
||||||
public class JFXTextField extends TextField {
|
|
||||||
/**
|
|
||||||
* Initialize the style class to 'jfx-text-field'.
|
|
||||||
* <p>
|
|
||||||
* This is the selector class from which CSS can be used to style
|
|
||||||
* this control.
|
|
||||||
*/
|
|
||||||
private static final String DEFAULT_STYLE_CLASS = "jfx-text-field";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
public JFXTextField() {
|
|
||||||
initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
public JFXTextField(String text) {
|
|
||||||
super(text);
|
|
||||||
initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected Skin<?> createDefaultSkin() {
|
|
||||||
return new JFXTextFieldSkin(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initialize() {
|
|
||||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
|
||||||
if ("dalvik".equalsIgnoreCase(System.getProperty("java.vm.name"))) {
|
|
||||||
this.setStyle("-fx-skin: \"com.jfoenix.android.skins.JFXTextFieldSkinAndroid\";");
|
|
||||||
}
|
|
||||||
|
|
||||||
useJFXContextMenu(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://github.com/HMCL-dev/HMCL/issues/5822
|
|
||||||
// TODO: This method may no longer be needed after we update JFXTextFieldSkin
|
|
||||||
@Override
|
|
||||||
protected void layoutChildren() {
|
|
||||||
super.layoutChildren();
|
|
||||||
this.setNeedsLayout(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/***************************************************************************
|
|
||||||
* *
|
|
||||||
* Properties *
|
|
||||||
* *
|
|
||||||
**************************************************************************/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* holds the current active validator on the text field in case of validation error
|
|
||||||
*/
|
|
||||||
private ReadOnlyObjectWrapper<ValidatorBase> activeValidator = new ReadOnlyObjectWrapper<>();
|
|
||||||
|
|
||||||
public ValidatorBase getActiveValidator() {
|
|
||||||
return activeValidator == null ? null : activeValidator.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ReadOnlyObjectProperty<ValidatorBase> activeValidatorProperty() {
|
|
||||||
return this.activeValidator.getReadOnlyProperty();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* list of validators that will validate the text value upon calling
|
|
||||||
* {{@link #validate()}
|
|
||||||
*/
|
|
||||||
private ObservableList<ValidatorBase> validators = FXCollections.observableArrayList();
|
|
||||||
|
|
||||||
public ObservableList<ValidatorBase> getValidators() {
|
|
||||||
return validators;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setValidators(ValidatorBase... validators) {
|
|
||||||
this.validators.addAll(validators);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* validates the text value using the list of validators provided by the user
|
|
||||||
* {{@link #setValidators(ValidatorBase...)}
|
|
||||||
*
|
|
||||||
* @return true if the value is valid else false
|
|
||||||
*/
|
|
||||||
public boolean validate() {
|
|
||||||
for (ValidatorBase validator : validators) {
|
|
||||||
if (validator.getSrcControl() == null) {
|
|
||||||
validator.setSrcControl(this);
|
|
||||||
}
|
|
||||||
validator.validate();
|
|
||||||
if (validator.getHasErrors()) {
|
|
||||||
activeValidator.set(validator);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
activeValidator.set(null);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void resetValidation() {
|
|
||||||
pseudoClassStateChanged(ValidatorBase.PSEUDO_CLASS_ERROR, false);
|
|
||||||
activeValidator.set(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/***************************************************************************
|
|
||||||
* *
|
|
||||||
* styleable Properties *
|
|
||||||
* *
|
|
||||||
**************************************************************************/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* set true to show a float the prompt text when focusing the field
|
|
||||||
*/
|
|
||||||
private StyleableBooleanProperty labelFloat = new SimpleStyleableBooleanProperty(StyleableProperties.LABEL_FLOAT, JFXTextField.this, "lableFloat", false);
|
|
||||||
|
|
||||||
public final StyleableBooleanProperty labelFloatProperty() {
|
|
||||||
return this.labelFloat;
|
|
||||||
}
|
|
||||||
|
|
||||||
public final boolean isLabelFloat() {
|
|
||||||
return this.labelFloatProperty().get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public final void setLabelFloat(final boolean labelFloat) {
|
|
||||||
this.labelFloatProperty().set(labelFloat);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* default color used when the field is unfocused
|
|
||||||
*/
|
|
||||||
private StyleableObjectProperty<Paint> unFocusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.UNFOCUS_COLOR, JFXTextField.this, "unFocusColor", Color.rgb(77, 77, 77));
|
|
||||||
|
|
||||||
public Paint getUnFocusColor() {
|
|
||||||
return unFocusColor == null ? Color.rgb(77, 77, 77) : unFocusColor.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public StyleableObjectProperty<Paint> unFocusColorProperty() {
|
|
||||||
return this.unFocusColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setUnFocusColor(Paint color) {
|
|
||||||
this.unFocusColor.set(color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* default color used when the field is focused
|
|
||||||
*/
|
|
||||||
private StyleableObjectProperty<Paint> focusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.FOCUS_COLOR, JFXTextField.this, "focusColor", Color.valueOf("#4059A9"));
|
|
||||||
|
|
||||||
public Paint getFocusColor() {
|
|
||||||
return focusColor == null ? Color.valueOf("#4059A9") : focusColor.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public StyleableObjectProperty<Paint> focusColorProperty() {
|
|
||||||
return this.focusColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setFocusColor(Paint color) {
|
|
||||||
this.focusColor.set(color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* disable animation on validation
|
|
||||||
*/
|
|
||||||
private StyleableBooleanProperty disableAnimation = new SimpleStyleableBooleanProperty(StyleableProperties.DISABLE_ANIMATION, JFXTextField.this, "disableAnimation", false);
|
|
||||||
|
|
||||||
public final StyleableBooleanProperty disableAnimationProperty() {
|
|
||||||
return this.disableAnimation;
|
|
||||||
}
|
|
||||||
|
|
||||||
public final Boolean isDisableAnimation() {
|
|
||||||
return disableAnimation != null && this.disableAnimationProperty().get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public final void setDisableAnimation(final Boolean disabled) {
|
|
||||||
this.disableAnimationProperty().set(disabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
private final static class StyleableProperties {
|
|
||||||
private static final CssMetaData<JFXTextField, Paint> UNFOCUS_COLOR = new CssMetaData<JFXTextField, Paint>("-jfx-unfocus-color", PaintConverter.getInstance(), Color.valueOf("#A6A6A6")) {
|
|
||||||
@Override
|
|
||||||
public boolean isSettable(JFXTextField control) {
|
|
||||||
return control.unFocusColor == null || !control.unFocusColor.isBound();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public StyleableProperty<Paint> getStyleableProperty(JFXTextField control) {
|
|
||||||
return control.unFocusColorProperty();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
private static final CssMetaData<JFXTextField, Paint> FOCUS_COLOR = new CssMetaData<JFXTextField, Paint>("-jfx-focus-color", PaintConverter.getInstance(), Color.valueOf("#3f51b5")) {
|
|
||||||
@Override
|
|
||||||
public boolean isSettable(JFXTextField control) {
|
|
||||||
return control.focusColor == null || !control.focusColor.isBound();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public StyleableProperty<Paint> getStyleableProperty(JFXTextField control) {
|
|
||||||
return control.focusColorProperty();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
private static final CssMetaData<JFXTextField, Boolean> LABEL_FLOAT = new CssMetaData<JFXTextField, Boolean>("-jfx-label-float", BooleanConverter.getInstance(), false) {
|
|
||||||
@Override
|
|
||||||
public boolean isSettable(JFXTextField control) {
|
|
||||||
return control.labelFloat == null || !control.labelFloat.isBound();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public StyleableBooleanProperty getStyleableProperty(JFXTextField control) {
|
|
||||||
return control.labelFloatProperty();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private static final CssMetaData<JFXTextField, Boolean> DISABLE_ANIMATION = new CssMetaData<JFXTextField, Boolean>("-jfx-disable-animation", BooleanConverter.getInstance(), false) {
|
|
||||||
@Override
|
|
||||||
public boolean isSettable(JFXTextField control) {
|
|
||||||
return control.disableAnimation == null || !control.disableAnimation.isBound();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public StyleableBooleanProperty getStyleableProperty(JFXTextField control) {
|
|
||||||
return control.disableAnimationProperty();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
|
|
||||||
|
|
||||||
static {
|
|
||||||
final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Control.getClassCssMetaData());
|
|
||||||
Collections.addAll(styleables, UNFOCUS_COLOR, FOCUS_COLOR, LABEL_FLOAT, DISABLE_ANIMATION);
|
|
||||||
CHILD_STYLEABLES = Collections.unmodifiableList(styleables);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// inherit the styleable properties from parent
|
|
||||||
private List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
|
|
||||||
if (STYLEABLES == null) {
|
|
||||||
final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Control.getClassCssMetaData());
|
|
||||||
styleables.addAll(getClassCssMetaData());
|
|
||||||
styleables.addAll(TextField.getClassCssMetaData());
|
|
||||||
STYLEABLES = Collections.unmodifiableList(styleables);
|
|
||||||
}
|
|
||||||
return STYLEABLES;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
|
|
||||||
return StyleableProperties.CHILD_STYLEABLES;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.jfoenix.controls;
|
|
||||||
|
|
||||||
import com.jfoenix.utils.JFXNodeUtils;
|
|
||||||
import javafx.beans.InvalidationListener;
|
|
||||||
import javafx.beans.WeakInvalidationListener;
|
|
||||||
import javafx.geometry.Insets;
|
|
||||||
import javafx.scene.Node;
|
|
||||||
import javafx.scene.control.TreeCell;
|
|
||||||
import javafx.scene.control.TreeItem;
|
|
||||||
import javafx.scene.layout.*;
|
|
||||||
import javafx.scene.paint.Color;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
import java.lang.ref.WeakReference;
|
|
||||||
|
|
||||||
/// JFXTreeCell is simple material design implementation of a tree cell.
|
|
||||||
///
|
|
||||||
/// @author Shadi Shaheen
|
|
||||||
/// @version 1.0
|
|
||||||
/// @since 2017-02-15
|
|
||||||
public class JFXTreeCell<T> extends TreeCell<T> {
|
|
||||||
|
|
||||||
protected JFXRippler cellRippler = new JFXRippler(this) {
|
|
||||||
@Override
|
|
||||||
protected Node getMask() {
|
|
||||||
Region clip = new Region();
|
|
||||||
JFXNodeUtils.updateBackground(JFXTreeCell.this.getBackground(), clip);
|
|
||||||
double width = control.getLayoutBounds().getWidth();
|
|
||||||
double height = control.getLayoutBounds().getHeight();
|
|
||||||
clip.resize(width, height);
|
|
||||||
return clip;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void positionControl(Node control) {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
};
|
|
||||||
private HBox hbox;
|
|
||||||
private final StackPane selectedPane = new StackPane();
|
|
||||||
|
|
||||||
private final InvalidationListener treeItemGraphicInvalidationListener = observable -> updateDisplay(getItem(),
|
|
||||||
isEmpty());
|
|
||||||
private final WeakInvalidationListener weakTreeItemGraphicListener = new WeakInvalidationListener(
|
|
||||||
treeItemGraphicInvalidationListener);
|
|
||||||
|
|
||||||
private WeakReference<TreeItem<T>> treeItemRef;
|
|
||||||
|
|
||||||
public JFXTreeCell() {
|
|
||||||
selectedPane.getStyleClass().add("selection-bar");
|
|
||||||
selectedPane.setBackground(new Background(new BackgroundFill(Color.RED, CornerRadii.EMPTY, Insets.EMPTY)));
|
|
||||||
selectedPane.setPrefWidth(3);
|
|
||||||
selectedPane.setMouseTransparent(true);
|
|
||||||
selectedProperty().addListener((o, oldVal, newVal) -> selectedPane.setVisible(newVal ? true : false));
|
|
||||||
|
|
||||||
final InvalidationListener treeItemInvalidationListener = observable -> {
|
|
||||||
TreeItem<T> oldTreeItem = treeItemRef == null ? null : treeItemRef.get();
|
|
||||||
if (oldTreeItem != null) {
|
|
||||||
oldTreeItem.graphicProperty().removeListener(weakTreeItemGraphicListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
TreeItem<T> newTreeItem = getTreeItem();
|
|
||||||
if (newTreeItem != null) {
|
|
||||||
newTreeItem.graphicProperty().addListener(weakTreeItemGraphicListener);
|
|
||||||
treeItemRef = new WeakReference<>(newTreeItem);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
treeItemProperty().addListener(treeItemInvalidationListener);
|
|
||||||
if (getTreeItem() != null) {
|
|
||||||
getTreeItem().graphicProperty().addListener(weakTreeItemGraphicListener);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void layoutChildren() {
|
|
||||||
super.layoutChildren();
|
|
||||||
if (!getChildren().contains(selectedPane)) {
|
|
||||||
getChildren().add(0, cellRippler);
|
|
||||||
cellRippler.rippler.clear();
|
|
||||||
getChildren().add(0, selectedPane);
|
|
||||||
}
|
|
||||||
cellRippler.resizeRelocate(0, 0, getWidth(), getHeight());
|
|
||||||
cellRippler.releaseRipple();
|
|
||||||
selectedPane.resizeRelocate(0, 0, selectedPane.prefWidth(-1), getHeight());
|
|
||||||
selectedPane.setVisible(isSelected());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Updates the text and graphic for the current value and tree item.
|
|
||||||
///
|
|
||||||
/// @implSpec Called when the cell value or the current tree item's graphic changes.
|
|
||||||
/// Subclasses may override this method to decorate the default display.
|
|
||||||
///
|
|
||||||
/// @param item the cell value, or `null` to clear the display
|
|
||||||
/// @param empty whether the cell has no item
|
|
||||||
protected void updateDisplay(@Nullable T item, boolean empty) {
|
|
||||||
if (item == null || empty) {
|
|
||||||
hbox = null;
|
|
||||||
setText(null);
|
|
||||||
setGraphic(null);
|
|
||||||
} else {
|
|
||||||
@Nullable TreeItem<T> treeItem = getTreeItem();
|
|
||||||
if (treeItem != null && treeItem.getGraphic() != null) {
|
|
||||||
if (item instanceof Node) {
|
|
||||||
setText(null);
|
|
||||||
if (hbox == null) {
|
|
||||||
hbox = new HBox(3);
|
|
||||||
}
|
|
||||||
hbox.getChildren().setAll(treeItem.getGraphic(), (Node) item);
|
|
||||||
setGraphic(hbox);
|
|
||||||
} else {
|
|
||||||
hbox = null;
|
|
||||||
setText(item.toString());
|
|
||||||
setGraphic(treeItem.getGraphic());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
hbox = null;
|
|
||||||
if (item instanceof Node) {
|
|
||||||
setText(null);
|
|
||||||
setGraphic((Node) item);
|
|
||||||
} else {
|
|
||||||
setText(item.toString());
|
|
||||||
setGraphic(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void updateItem(T item, boolean empty) {
|
|
||||||
super.updateItem(item, empty);
|
|
||||||
updateDisplay(item, empty);
|
|
||||||
setMouseTransparent(item == null || empty);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.jfoenix.controls;
|
|
||||||
|
|
||||||
import javafx.scene.control.TreeItem;
|
|
||||||
import javafx.scene.control.TreeView;
|
|
||||||
|
|
||||||
/// JFXTreeView is the material design implementation of a TreeView
|
|
||||||
/// with expand/collapse animation and selection indicator.
|
|
||||||
///
|
|
||||||
/// @author Shadi Shaheen
|
|
||||||
/// @version 1.0
|
|
||||||
/// @since 2017-02-15
|
|
||||||
public class JFXTreeView<T> extends TreeView<T> {
|
|
||||||
|
|
||||||
private static final String DEFAULT_STYLE_CLASS = "jfx-tree-view";
|
|
||||||
|
|
||||||
public JFXTreeView() {
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
|
|
||||||
public JFXTreeView(TreeItem<T> root) {
|
|
||||||
super(root);
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void init() {
|
|
||||||
this.setCellFactory((view) -> new JFXTreeCell<>());
|
|
||||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.jfoenix.controls.behavior;
|
|
||||||
|
|
||||||
import com.sun.javafx.scene.control.behavior.ComboBoxBaseBehavior;
|
|
||||||
import javafx.scene.control.ComboBoxBase;
|
|
||||||
import javafx.scene.control.PopupControl;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Shadi Shaheen
|
|
||||||
* @version 2.0
|
|
||||||
* @since 2017-10-05
|
|
||||||
*/
|
|
||||||
public class JFXGenericPickerBehavior<T> extends ComboBoxBaseBehavior<T> {
|
|
||||||
|
|
||||||
public JFXGenericPickerBehavior(ComboBoxBase<T> var1) {
|
|
||||||
super(var1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onAutoHide(PopupControl var1) {
|
|
||||||
if (!var1.isShowing() && this.getNode().isShowing()) {
|
|
||||||
this.getNode().hide();
|
|
||||||
}
|
|
||||||
if (!this.getNode().isShowing()) {
|
|
||||||
super.onAutoHide(var1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.jfoenix.controls.datamodels.treetable;
|
|
||||||
|
|
||||||
import javafx.beans.property.ObjectProperty;
|
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
|
||||||
import javafx.collections.FXCollections;
|
|
||||||
import javafx.collections.ObservableList;
|
|
||||||
import javafx.scene.control.TreeTableColumn;
|
|
||||||
|
|
||||||
/// data model that is used in JFXTreeTableView, it's used to implement
|
|
||||||
/// the grouping feature.
|
|
||||||
///
|
|
||||||
/// **Note:** the data object used in JFXTreeTableView **must** extends this class
|
|
||||||
///
|
|
||||||
/// @param <T> is the concrete object of the Tree table
|
|
||||||
/// @author Shadi Shaheen
|
|
||||||
/// @version 1.0
|
|
||||||
/// @since 2016-03-09
|
|
||||||
public class RecursiveTreeObject<T> {
|
|
||||||
|
|
||||||
/// grouped children objects
|
|
||||||
private ObservableList<T> children = FXCollections.observableArrayList();
|
|
||||||
|
|
||||||
public ObservableList<T> getChildren() {
|
|
||||||
return children;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setChildren(ObservableList<T> children) {
|
|
||||||
this.children = children;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether or not the object is grouped by a specified tree table column
|
|
||||||
ObjectProperty<TreeTableColumn<T, ?>> groupedColumn = new SimpleObjectProperty<>();
|
|
||||||
|
|
||||||
public final ObjectProperty<TreeTableColumn<T, ?>> groupedColumnProperty() {
|
|
||||||
return this.groupedColumn;
|
|
||||||
}
|
|
||||||
|
|
||||||
public final TreeTableColumn<T, ?> getGroupedColumn() {
|
|
||||||
return this.groupedColumnProperty().get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public final void setGroupedColumn(final TreeTableColumn<T, ?> groupedColumn) {
|
|
||||||
this.groupedColumnProperty().set(groupedColumn);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// the value that must be shown when grouped
|
|
||||||
ObjectProperty<Object> groupedValue = new SimpleObjectProperty<>();
|
|
||||||
|
|
||||||
public final ObjectProperty<Object> groupedValueProperty() {
|
|
||||||
return this.groupedValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
public final Object getGroupedValue() {
|
|
||||||
return this.groupedValueProperty().get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public final void setGroupedValue(final Object groupedValue) {
|
|
||||||
this.groupedValueProperty().set(groupedValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.jfoenix.controls.events;
|
|
||||||
|
|
||||||
import javafx.event.Event;
|
|
||||||
import javafx.event.EventType;
|
|
||||||
|
|
||||||
import java.io.Serial;
|
|
||||||
|
|
||||||
/// JFXDialog events, used exclusively by the following methods:
|
|
||||||
///
|
|
||||||
/// - [com.jfoenix.controls.JFXDialog#show()]
|
|
||||||
/// - [com.jfoenix.controls.JFXDialog#close()]
|
|
||||||
///
|
|
||||||
/// @author Shadi Shaheen
|
|
||||||
/// @version 1.0
|
|
||||||
/// @since 2016-03-09
|
|
||||||
public class JFXDialogEvent extends Event {
|
|
||||||
|
|
||||||
@Serial
|
|
||||||
private static final long serialVersionUID = 1L;
|
|
||||||
|
|
||||||
/// This event occurs when a JFXDialog is closed, no longer visible to the user
|
|
||||||
/// ( after the exit animation ends )
|
|
||||||
public static final EventType<JFXDialogEvent> CLOSED = new EventType<>(Event.ANY, "JFX_DIALOG_CLOSED");
|
|
||||||
|
|
||||||
/// This event occurs when a JFXDialog is opened, visible to the user
|
|
||||||
/// ( after the entrance animation ends )
|
|
||||||
public static final EventType<JFXDialogEvent> OPENED = new EventType<>(Event.ANY, "JFX_DIALOG_OPENED");
|
|
||||||
|
|
||||||
/// Construct a new JFXDialog `Event` with the specified event type
|
|
||||||
///
|
|
||||||
/// @param eventType the event type
|
|
||||||
public JFXDialogEvent(EventType<? extends Event> eventType) {
|
|
||||||
super(eventType);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2016 JFoenix
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
* this software and associated documentation files (the "Software"), to deal in
|
|
||||||
* the Software without restriction, including without limitation the rights to
|
|
||||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
||||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
|
||||||
* subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
||||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
||||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
||||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.jfoenix.effects;
|
|
||||||
|
|
||||||
import javafx.scene.Node;
|
|
||||||
import javafx.scene.effect.BlurType;
|
|
||||||
import javafx.scene.effect.DropShadow;
|
|
||||||
import javafx.scene.layout.Pane;
|
|
||||||
import javafx.scene.paint.Color;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* it will create a shadow effect for a given node and a specified depth level.
|
|
||||||
* depth levels are {0,1,2,3,4,5}
|
|
||||||
*
|
|
||||||
* @author Shadi Shaheen
|
|
||||||
* @version 1.0
|
|
||||||
* @since 2016-03-09
|
|
||||||
*/
|
|
||||||
public final class JFXDepthManager {
|
|
||||||
private JFXDepthManager() {
|
|
||||||
throw new AssertionError();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final DropShadow[] depth = new DropShadow[] {
|
|
||||||
new DropShadow(BlurType.GAUSSIAN, Color.TRANSPARENT, 0, 0, 0, 0),
|
|
||||||
new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.12), 4, 0, 0, 0),
|
|
||||||
new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.16), 8, 0, 0, 4),
|
|
||||||
new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.20), 12, 0, 0, 4),
|
|
||||||
new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.24), 16, 0, 0, 4),
|
|
||||||
new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.28), 20, 0, 0, 8)
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* this method is used to add shadow effect to the node,
|
|
||||||
* however the shadow is not real ( gets affected with node transformations)
|
|
||||||
* <p>
|
|
||||||
* use {@link #createMaterialNode(Node, int)} instead to generate a real shadow
|
|
||||||
*/
|
|
||||||
public static void setDepth(Node control, int level) {
|
|
||||||
level = level < 0 ? 0 : level;
|
|
||||||
level = level > 5 ? 5 : level;
|
|
||||||
control.setEffect(new DropShadow(BlurType.GAUSSIAN,
|
|
||||||
depth[level].getColor(),
|
|
||||||
depth[level].getRadius(),
|
|
||||||
depth[level].getSpread(),
|
|
||||||
depth[level].getOffsetX(),
|
|
||||||
depth[level].getOffsetY()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int getLevels() {
|
|
||||||
return depth.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static DropShadow getShadowAt(int level) {
|
|
||||||
return depth[level];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* this method will generate a new container node that prevent
|
|
||||||
* control transformation to be applied to the shadow effect
|
|
||||||
* (which makes it looks as a real shadow)
|
|
||||||
*/
|
|
||||||
public static Node createMaterialNode(Node control, int level) {
|
|
||||||
Node container = new Pane(control) {
|
|
||||||
@Override
|
|
||||||
protected double computeMaxWidth(double height) {
|
|
||||||
return computePrefWidth(height);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected double computeMaxHeight(double width) {
|
|
||||||
return computePrefHeight(width);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected double computePrefWidth(double height) {
|
|
||||||
return control.prefWidth(height);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected double computePrefHeight(double width) {
|
|
||||||
return control.prefHeight(width);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
container.getStyleClass().add("depth-container");
|
|
||||||
container.setPickOnBounds(false);
|
|
||||||
level = level < 0 ? 0 : level;
|
|
||||||
level = level > 5 ? 5 : level;
|
|
||||||
container.setEffect(new DropShadow(BlurType.GAUSSIAN,
|
|
||||||
depth[level].getColor(),
|
|
||||||
depth[level].getRadius(),
|
|
||||||
depth[level].getSpread(),
|
|
||||||
depth[level].getOffsetX(),
|
|
||||||
depth[level].getOffsetY()));
|
|
||||||
return container;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void pop(Node control) {
|
|
||||||
control.setEffect(new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.26), 5, 0.05, 0, 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,222 +0,0 @@
|
|||||||
//
|
|
||||||
// Source code recreated from a .class file by IntelliJ IDEA
|
|
||||||
// (powered by Fernflower decompiler)
|
|
||||||
//
|
|
||||||
|
|
||||||
package com.jfoenix.skins;
|
|
||||||
|
|
||||||
import com.jfoenix.adapters.skins.ButtonSkin;
|
|
||||||
import com.jfoenix.controls.JFXButton;
|
|
||||||
import com.jfoenix.controls.JFXRippler;
|
|
||||||
import com.jfoenix.effects.JFXDepthManager;
|
|
||||||
import com.jfoenix.transitions.CachedTransition;
|
|
||||||
import javafx.animation.Interpolator;
|
|
||||||
import javafx.animation.KeyFrame;
|
|
||||||
import javafx.animation.KeyValue;
|
|
||||||
import javafx.animation.Timeline;
|
|
||||||
import javafx.animation.Transition;
|
|
||||||
import javafx.beans.binding.Bindings;
|
|
||||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
|
||||||
import javafx.geometry.Insets;
|
|
||||||
import javafx.scene.Node;
|
|
||||||
import javafx.scene.control.Label;
|
|
||||||
import javafx.scene.effect.DropShadow;
|
|
||||||
import javafx.scene.layout.Background;
|
|
||||||
import javafx.scene.layout.BackgroundFill;
|
|
||||||
import javafx.scene.layout.CornerRadii;
|
|
||||||
import javafx.scene.layout.StackPane;
|
|
||||||
import javafx.scene.paint.Color;
|
|
||||||
import javafx.scene.shape.Shape;
|
|
||||||
import javafx.util.Duration;
|
|
||||||
import org.jackhuang.hmcl.ui.FXUtils;
|
|
||||||
|
|
||||||
public class JFXButtonSkin extends ButtonSkin {
|
|
||||||
private final StackPane buttonContainer = new StackPane();
|
|
||||||
private final JFXRippler buttonRippler = new JFXRippler(new StackPane()) {
|
|
||||||
protected Node getMask() {
|
|
||||||
StackPane mask = new StackPane();
|
|
||||||
mask.shapeProperty().bind(JFXButtonSkin.this.buttonContainer.shapeProperty());
|
|
||||||
mask.backgroundProperty().bind(Bindings.createObjectBinding(() -> new Background(
|
|
||||||
new BackgroundFill(Color.WHITE,
|
|
||||||
JFXButtonSkin.this.buttonContainer.backgroundProperty().get() != null && !JFXButtonSkin.this.buttonContainer.getBackground().getFills().isEmpty()
|
|
||||||
? JFXButtonSkin.this.buttonContainer.getBackground().getFills().get(0).getRadii()
|
|
||||||
: JFXButtonSkin.this.defaultRadii, JFXButtonSkin.this.buttonContainer.backgroundProperty().get() != null && !JFXButtonSkin.this.buttonContainer.getBackground().getFills().isEmpty() ? JFXButtonSkin.this.buttonContainer.getBackground().getFills().get(0).getInsets() : Insets.EMPTY)),
|
|
||||||
JFXButtonSkin.this.buttonContainer.backgroundProperty()));
|
|
||||||
mask.resize(JFXButtonSkin.this.buttonContainer.getWidth() - JFXButtonSkin.this.buttonContainer.snappedRightInset() - JFXButtonSkin.this.buttonContainer.snappedLeftInset(), JFXButtonSkin.this.buttonContainer.getHeight() - JFXButtonSkin.this.buttonContainer.snappedBottomInset() - JFXButtonSkin.this.buttonContainer.snappedTopInset());
|
|
||||||
return mask;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initListeners() {
|
|
||||||
this.ripplerPane.setOnMousePressed((event) -> {
|
|
||||||
if (JFXButtonSkin.this.releaseManualRippler != null) {
|
|
||||||
JFXButtonSkin.this.releaseManualRippler.run();
|
|
||||||
}
|
|
||||||
|
|
||||||
JFXButtonSkin.this.releaseManualRippler = null;
|
|
||||||
this.createRipple(event.getX(), event.getY());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
private Transition clickedAnimation;
|
|
||||||
private final CornerRadii defaultRadii = new CornerRadii(3.0);
|
|
||||||
private boolean invalid = true;
|
|
||||||
private Runnable releaseManualRippler = null;
|
|
||||||
|
|
||||||
public JFXButtonSkin(JFXButton button) {
|
|
||||||
super(button);
|
|
||||||
this.getSkinnable().armedProperty().addListener((o, oldVal, newVal) -> {
|
|
||||||
if (newVal) {
|
|
||||||
this.releaseManualRippler = this.buttonRippler.createManualRipple();
|
|
||||||
if (this.clickedAnimation != null) {
|
|
||||||
this.clickedAnimation.setRate(1.0);
|
|
||||||
this.clickedAnimation.play();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (this.releaseManualRippler != null) {
|
|
||||||
this.releaseManualRippler.run();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.clickedAnimation != null) {
|
|
||||||
this.clickedAnimation.setRate(-1.0);
|
|
||||||
this.clickedAnimation.play();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
this.buttonContainer.getChildren().add(this.buttonRippler);
|
|
||||||
button.buttonTypeProperty().addListener((o, oldVal, newVal) -> this.updateButtonType(newVal));
|
|
||||||
button.setOnMousePressed((e) -> {
|
|
||||||
if (this.clickedAnimation != null) {
|
|
||||||
this.clickedAnimation.setRate(1.0F);
|
|
||||||
this.clickedAnimation.play();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
button.setOnMouseReleased((e) -> {
|
|
||||||
if (this.clickedAnimation != null) {
|
|
||||||
this.clickedAnimation.setRate(-1.0F);
|
|
||||||
this.clickedAnimation.play();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ReadOnlyBooleanProperty focusVisibleProperty = FXUtils.focusVisibleProperty(button);
|
|
||||||
if (focusVisibleProperty == null) {
|
|
||||||
focusVisibleProperty = button.focusedProperty();
|
|
||||||
}
|
|
||||||
focusVisibleProperty.addListener((o, oldVal, newVal) -> {
|
|
||||||
if (newVal) {
|
|
||||||
if (!this.getSkinnable().isPressed()) {
|
|
||||||
this.buttonRippler.showOverlay();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.buttonRippler.hideOverlay();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
button.pressedProperty().addListener((o, oldVal, newVal) -> this.buttonRippler.hideOverlay());
|
|
||||||
button.setPickOnBounds(false);
|
|
||||||
this.buttonContainer.setPickOnBounds(false);
|
|
||||||
this.buttonContainer.shapeProperty().bind(this.getSkinnable().shapeProperty());
|
|
||||||
this.buttonContainer.borderProperty().bind(this.getSkinnable().borderProperty());
|
|
||||||
this.buttonContainer.backgroundProperty().bind(Bindings.createObjectBinding(() -> {
|
|
||||||
if (button.getBackground() == null || this.isJavaDefaultBackground(button.getBackground()) || this.isJavaDefaultClickedBackground(button.getBackground())) {
|
|
||||||
button.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, this.defaultRadii, null)));
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return new Background(new BackgroundFill(this.getSkinnable().getBackground() != null ? this.getSkinnable().getBackground().getFills().get(0).getFill() : Color.TRANSPARENT, this.getSkinnable().getBackground() != null ? this.getSkinnable().getBackground().getFills().get(0).getRadii() : this.defaultRadii, Insets.EMPTY));
|
|
||||||
} catch (Exception var3) {
|
|
||||||
return this.getSkinnable().getBackground();
|
|
||||||
}
|
|
||||||
}, this.getSkinnable().backgroundProperty()));
|
|
||||||
button.ripplerFillProperty().addListener((o, oldVal, newVal) -> this.buttonRippler.setRipplerFill(newVal));
|
|
||||||
if (button.getBackground() == null || this.isJavaDefaultBackground(button.getBackground())) {
|
|
||||||
button.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, this.defaultRadii, null)));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateButtonType(button.getButtonType());
|
|
||||||
this.updateChildren();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void updateChildren() {
|
|
||||||
super.updateChildren();
|
|
||||||
if (this.buttonContainer != null) {
|
|
||||||
this.getChildren().add(0, this.buttonContainer);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 1; i < this.getChildren().size(); ++i) {
|
|
||||||
this.getChildren().get(i).setMouseTransparent(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void layoutChildren(double x, double y, double w, double h) {
|
|
||||||
if (this.invalid) {
|
|
||||||
if (((JFXButton) this.getSkinnable()).getRipplerFill() == null) {
|
|
||||||
for (int i = this.getChildren().size() - 1; i >= 1; --i) {
|
|
||||||
if (this.getChildren().get(i) instanceof Shape shape) {
|
|
||||||
this.buttonRippler.setRipplerFill(shape.getFill());
|
|
||||||
shape.fillProperty().addListener((o, oldVal, newVal) -> this.buttonRippler.setRipplerFill(newVal));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.getChildren().get(i) instanceof Label label) {
|
|
||||||
this.buttonRippler.setRipplerFill(label.getTextFill());
|
|
||||||
label.textFillProperty().addListener((o, oldVal, newVal) -> this.buttonRippler.setRipplerFill(newVal));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.buttonRippler.setRipplerFill(((JFXButton) this.getSkinnable()).getRipplerFill());
|
|
||||||
}
|
|
||||||
|
|
||||||
this.invalid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
double shift = 1.0F;
|
|
||||||
this.buttonContainer.resizeRelocate(this.getSkinnable().getLayoutBounds().getMinX() - shift, this.getSkinnable().getLayoutBounds().getMinY() - shift, this.getSkinnable().getWidth() + (double) 2.0F * shift, this.getSkinnable().getHeight() + (double) 2.0F * shift);
|
|
||||||
this.layoutLabelInArea(x, y, w, h);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isJavaDefaultBackground(Background background) {
|
|
||||||
try {
|
|
||||||
String firstFill = background.getFills().get(0).getFill().toString();
|
|
||||||
return "0xffffffba".equals(firstFill) || "0xffffffbf".equals(firstFill) || "0xffffff12".equals(firstFill) || "0xffffffbd".equals(firstFill);
|
|
||||||
} catch (Exception var3) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isJavaDefaultClickedBackground(Background background) {
|
|
||||||
try {
|
|
||||||
return "0x039ed3ff".equals(background.getFills().get(0).getFill().toString());
|
|
||||||
} catch (Exception var3) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateButtonType(JFXButton.ButtonType type) {
|
|
||||||
switch (type) {
|
|
||||||
case RAISED -> {
|
|
||||||
JFXDepthManager.setDepth(this.buttonContainer, 2);
|
|
||||||
this.clickedAnimation = new ButtonClickTransition();
|
|
||||||
}
|
|
||||||
case FLAT -> this.buttonContainer.setEffect(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ButtonClickTransition extends CachedTransition {
|
|
||||||
public ButtonClickTransition() {
|
|
||||||
super(JFXButtonSkin.this.buttonContainer, new Timeline(
|
|
||||||
new KeyFrame(Duration.ZERO,
|
|
||||||
new KeyValue(((DropShadow) JFXButtonSkin.this.buttonContainer.getEffect()).radiusProperty(), JFXDepthManager.getShadowAt(2).radiusProperty().get(), Interpolator.EASE_BOTH),
|
|
||||||
new KeyValue(((DropShadow) JFXButtonSkin.this.buttonContainer.getEffect()).spreadProperty(), JFXDepthManager.getShadowAt(2).spreadProperty().get(), Interpolator.EASE_BOTH),
|
|
||||||
new KeyValue(((DropShadow) JFXButtonSkin.this.buttonContainer.getEffect()).offsetXProperty(), JFXDepthManager.getShadowAt(2).offsetXProperty().get(), Interpolator.EASE_BOTH),
|
|
||||||
new KeyValue(((DropShadow) JFXButtonSkin.this.buttonContainer.getEffect()).offsetYProperty(), JFXDepthManager.getShadowAt(2).offsetYProperty().get(), Interpolator.EASE_BOTH)),
|
|
||||||
new KeyFrame(Duration.millis(1000.0F),
|
|
||||||
new KeyValue(((DropShadow) JFXButtonSkin.this.buttonContainer.getEffect()).radiusProperty(), JFXDepthManager.getShadowAt(5).radiusProperty().get(), Interpolator.EASE_BOTH),
|
|
||||||
new KeyValue(((DropShadow) JFXButtonSkin.this.buttonContainer.getEffect()).spreadProperty(), JFXDepthManager.getShadowAt(5).spreadProperty().get(), Interpolator.EASE_BOTH),
|
|
||||||
new KeyValue(((DropShadow) JFXButtonSkin.this.buttonContainer.getEffect()).offsetXProperty(), JFXDepthManager.getShadowAt(5).offsetXProperty().get(), Interpolator.EASE_BOTH),
|
|
||||||
new KeyValue(((DropShadow) JFXButtonSkin.this.buttonContainer.getEffect()).offsetYProperty(), JFXDepthManager.getShadowAt(5).offsetYProperty().get(), Interpolator.EASE_BOTH))));
|
|
||||||
this.setCycleDuration(Duration.seconds(0.2));
|
|
||||||
this.setDelay(Duration.seconds(0.0F));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,302 +0,0 @@
|
|||||||
//
|
|
||||||
// Source code recreated from a .class file by IntelliJ IDEA
|
|
||||||
// (powered by FernFlower decompiler)
|
|
||||||
//
|
|
||||||
|
|
||||||
package com.jfoenix.skins;
|
|
||||||
|
|
||||||
import com.jfoenix.controls.JFXCheckBox;
|
|
||||||
import com.jfoenix.controls.JFXRippler;
|
|
||||||
import com.jfoenix.controls.JFXRippler.RipplerMask;
|
|
||||||
import com.jfoenix.controls.JFXRippler.RipplerPos;
|
|
||||||
import com.jfoenix.transitions.CachedTransition;
|
|
||||||
import com.jfoenix.transitions.JFXFillTransition;
|
|
||||||
import com.jfoenix.utils.JFXNodeUtils;
|
|
||||||
import javafx.animation.*;
|
|
||||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
|
||||||
import javafx.geometry.HPos;
|
|
||||||
import javafx.geometry.Insets;
|
|
||||||
import javafx.geometry.VPos;
|
|
||||||
import javafx.scene.control.CheckBox;
|
|
||||||
import javafx.scene.control.skin.CheckBoxSkin;
|
|
||||||
import javafx.scene.layout.*;
|
|
||||||
import javafx.scene.paint.Color;
|
|
||||||
import javafx.scene.shape.SVGPath;
|
|
||||||
import javafx.util.Duration;
|
|
||||||
import org.jackhuang.hmcl.theme.Themes;
|
|
||||||
import org.jackhuang.hmcl.ui.FXUtils;
|
|
||||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
|
||||||
|
|
||||||
public class JFXCheckBoxSkin extends CheckBoxSkin {
|
|
||||||
private final StackPane box = new StackPane();
|
|
||||||
private final StackPane mark = new StackPane();
|
|
||||||
private final SVGPath markShape = new SVGPath();
|
|
||||||
private final double lineThick = 2.0;
|
|
||||||
private final double padding = 10.0;
|
|
||||||
private final JFXRippler rippler;
|
|
||||||
private final AnchorPane container = new AnchorPane();
|
|
||||||
private final double labelOffset = -8.0;
|
|
||||||
private final Transition transition;
|
|
||||||
private boolean invalid = true;
|
|
||||||
private boolean selected;
|
|
||||||
private boolean indeterminate;
|
|
||||||
|
|
||||||
/// The checkbox fill animation, initialized with the configured checked color.
|
|
||||||
private JFXFillTransition fillTransition;
|
|
||||||
|
|
||||||
public JFXCheckBoxSkin(JFXCheckBox control) {
|
|
||||||
super(control);
|
|
||||||
this.box.setMinSize(18.0, 18.0);
|
|
||||||
this.box.setPrefSize(18.0, 18.0);
|
|
||||||
this.box.setMaxSize(18.0, 18.0);
|
|
||||||
this.box.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, new CornerRadii(2.0), Insets.EMPTY)));
|
|
||||||
this.box.setBorder(new Border(new BorderStroke(Themes.getColorScheme().getOnSurfaceVariant(), BorderStrokeStyle.SOLID, new CornerRadii(2.0), new BorderWidths(this.lineThick))));
|
|
||||||
StackPane boxContainer = new StackPane();
|
|
||||||
boxContainer.getChildren().add(this.box);
|
|
||||||
boxContainer.setPadding(new Insets(this.padding));
|
|
||||||
this.rippler = new JFXRippler(boxContainer, RipplerMask.CIRCLE, RipplerPos.BACK);
|
|
||||||
this.updateRippleColor(control.isSelected(), control.isIndeterminate());
|
|
||||||
this.mark.setShape(this.markShape);
|
|
||||||
this.updateMarkShape(true, false);
|
|
||||||
this.mark.setVisible(false);
|
|
||||||
this.mark.setScaleX(0.0);
|
|
||||||
this.mark.setScaleY(0.0);
|
|
||||||
boxContainer.getChildren().add(this.mark);
|
|
||||||
this.container.getChildren().add(this.rippler);
|
|
||||||
AnchorPane.setRightAnchor(this.rippler, this.labelOffset);
|
|
||||||
this.registerChangeListener(control.selectedProperty(), ignored -> requestSelectionUpdate());
|
|
||||||
this.registerChangeListener(control.indeterminateProperty(), ignored -> requestSelectionUpdate());
|
|
||||||
|
|
||||||
ReadOnlyBooleanProperty focusVisibleProperty = FXUtils.focusVisibleProperty(control);
|
|
||||||
if (focusVisibleProperty == null)
|
|
||||||
focusVisibleProperty = control.focusedProperty();
|
|
||||||
focusVisibleProperty.addListener((o, oldVal, newVal) -> {
|
|
||||||
if (newVal) {
|
|
||||||
if (!this.getSkinnable().isPressed()) {
|
|
||||||
this.rippler.showOverlay();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.rippler.hideOverlay();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
control.pressedProperty().addListener((o, oldVal, newVal) -> this.rippler.hideOverlay());
|
|
||||||
this.updateChildren();
|
|
||||||
this.registerChangeListener(control.checkedColorProperty(), ignored -> {
|
|
||||||
if (fillTransition != null) {
|
|
||||||
fillTransition.stop();
|
|
||||||
}
|
|
||||||
boolean selected = control.isSelected();
|
|
||||||
boolean indeterminate = control.isIndeterminate();
|
|
||||||
JFXFillTransition fillTransition = this.createFillTransition();
|
|
||||||
this.fillTransition = fillTransition;
|
|
||||||
fillTransition.jumpTo(selected || indeterminate ? fillTransition.getCycleDuration() : Duration.ZERO);
|
|
||||||
updateColors(selected, indeterminate);
|
|
||||||
});
|
|
||||||
this.registerChangeListener(control.unCheckedColorProperty(), ignored -> updateColors(control.isSelected(), control.isIndeterminate()));
|
|
||||||
this.transition = new CheckBoxTransition();
|
|
||||||
this.fillTransition = this.createFillTransition();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void requestSelectionUpdate() {
|
|
||||||
this.invalid = true;
|
|
||||||
this.getSkinnable().requestLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateRippleColor(boolean selected, boolean indeterminate) {
|
|
||||||
JFXCheckBox control = (JFXCheckBox) this.getSkinnable();
|
|
||||||
this.rippler.setRipplerFill(selected || indeterminate
|
|
||||||
? control.getCheckedColor()
|
|
||||||
: control.getUnCheckedColor());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateColors(boolean selected, boolean indeterminate) {
|
|
||||||
var control = (JFXCheckBox) getSkinnable();
|
|
||||||
boolean marked = selected || indeterminate;
|
|
||||||
JFXNodeUtils.updateBackground(box.getBackground(), box, marked ? control.getCheckedColor() : Color.TRANSPARENT);
|
|
||||||
rippler.setRipplerFill(marked ? control.getCheckedColor() : control.getUnCheckedColor());
|
|
||||||
this.updateBorder(selected, indeterminate);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateBorder(boolean selected, boolean indeterminate) {
|
|
||||||
JFXCheckBox control = (JFXCheckBox) this.getSkinnable();
|
|
||||||
final BorderStroke borderStroke = box.getBorder().getStrokes().get(0);
|
|
||||||
box.setBorder(new Border(new BorderStroke(
|
|
||||||
selected || indeterminate ? control.getCheckedColor() : Themes.getColorScheme().getOnSurfaceVariant(),
|
|
||||||
borderStroke.getTopStyle(),
|
|
||||||
borderStroke.getRadii(),
|
|
||||||
borderStroke.getWidths())));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateMarkShape(boolean selected, boolean indeterminate) {
|
|
||||||
if (indeterminate) {
|
|
||||||
this.markShape.setContent("M1 0 H9 A1 1 0 0 1 10 1 A1 1 0 0 1 9 2 H1 A1 1 0 0 1 0 1 A1 1 0 0 1 1 0 Z");
|
|
||||||
this.mark.setMaxSize(10.0, 2.0);
|
|
||||||
this.mark.setStyle("-fx-background-color:-monet-on-primary;");
|
|
||||||
} else if (selected) {
|
|
||||||
this.markShape.setContent("M384 690l452-452 60 60-512 512-238-238 60-60z");
|
|
||||||
this.mark.setMaxSize(15.0, 12.0);
|
|
||||||
this.mark.setStyle("-fx-background-color:-monet-on-primary; -fx-border-color:-monet-on-primary; -fx-border-width:2px;");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void updateChildren() {
|
|
||||||
super.updateChildren();
|
|
||||||
if (this.container != null) {
|
|
||||||
this.getChildren().remove(1);
|
|
||||||
this.getChildren().add(this.container);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
|
|
||||||
return super.computePrefWidth(height, topInset, rightInset, bottomInset, leftInset) + this.snapSizeX(this.box.minWidth(-1.0)) + this.labelOffset + 2.0 * this.padding;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
|
|
||||||
return super.computePrefWidth(height, topInset, rightInset, bottomInset, leftInset) + this.snapSizeY(this.box.prefWidth(-1.0)) + this.labelOffset + 2.0 * this.padding;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void layoutChildren(double x, double y, double w, double h) {
|
|
||||||
CheckBox checkBox = this.getSkinnable();
|
|
||||||
double boxWidth = this.snapSizeX(this.container.prefWidth(-1.0));
|
|
||||||
double boxHeight = this.snapSizeY(this.container.prefHeight(-1.0));
|
|
||||||
double computeWidth = Math.min(checkBox.prefWidth(-1.0), checkBox.minWidth(-1.0)) + this.labelOffset + 2.0 * this.padding;
|
|
||||||
double labelWidth = Math.min(computeWidth - boxWidth, w - this.snapSizeX(boxWidth)) + this.labelOffset + 2.0 * this.padding;
|
|
||||||
double labelHeight = Math.min(checkBox.prefHeight(labelWidth), h);
|
|
||||||
double maxHeight = Math.max(boxHeight, labelHeight);
|
|
||||||
double xOffset = computeXOffset(w, labelWidth + boxWidth, checkBox.getAlignment().getHpos()) + x;
|
|
||||||
double yOffset = computeYOffset(h, maxHeight, checkBox.getAlignment().getVpos()) + x;
|
|
||||||
if (this.invalid) {
|
|
||||||
boolean selected = checkBox.isSelected();
|
|
||||||
boolean indeterminate = checkBox.isIndeterminate();
|
|
||||||
this.updateRippleColor(selected, indeterminate);
|
|
||||||
this.playSelectAnimation(selected, indeterminate);
|
|
||||||
|
|
||||||
this.invalid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.layoutLabelInArea(xOffset + boxWidth, yOffset, labelWidth, maxHeight, checkBox.getAlignment());
|
|
||||||
this.container.resize(boxWidth, boxHeight);
|
|
||||||
this.positionInArea(this.container, xOffset, yOffset, boxWidth, maxHeight, 0.0, checkBox.getAlignment().getHpos(), checkBox.getAlignment().getVpos());
|
|
||||||
}
|
|
||||||
|
|
||||||
static double computeXOffset(double width, double contentWidth, HPos hpos) {
|
|
||||||
return switch (hpos) {
|
|
||||||
case LEFT -> 0.0;
|
|
||||||
case CENTER -> (width - contentWidth) / 2.0;
|
|
||||||
case RIGHT -> width - contentWidth;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static double computeYOffset(double height, double contentHeight, VPos vpos) {
|
|
||||||
return switch (vpos) {
|
|
||||||
case TOP -> 0.0;
|
|
||||||
case CENTER -> (height - contentHeight) / 2.0;
|
|
||||||
case BOTTOM -> height - contentHeight;
|
|
||||||
default -> 0.0;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private void playSelectAnimation(boolean selected, boolean indeterminate) {
|
|
||||||
boolean previousSelected = this.selected;
|
|
||||||
boolean previousIndeterminate = this.indeterminate;
|
|
||||||
this.selected = selected;
|
|
||||||
this.indeterminate = indeterminate;
|
|
||||||
boolean previousMarked = previousSelected || previousIndeterminate;
|
|
||||||
boolean marked = selected || indeterminate;
|
|
||||||
boolean sameVisualState = previousIndeterminate == indeterminate
|
|
||||||
&& (indeterminate || previousSelected == selected);
|
|
||||||
this.updateBorder(selected, indeterminate);
|
|
||||||
|
|
||||||
if (sameVisualState) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.invalid = false;
|
|
||||||
|
|
||||||
if (!AnimationUtils.isAnimationEnabled()) {
|
|
||||||
finishSelectionAnimation(selected, indeterminate);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (previousMarked && marked) {
|
|
||||||
this.transition.stop();
|
|
||||||
this.transition.setOnFinished(null);
|
|
||||||
this.transition.jumpTo(this.transition.getCycleDuration());
|
|
||||||
this.transition.setRate(-1.0);
|
|
||||||
this.transition.setOnFinished(event -> {
|
|
||||||
this.transition.setOnFinished(null);
|
|
||||||
this.updateMarkShape(selected, indeterminate);
|
|
||||||
this.transition.jumpTo(Duration.ZERO);
|
|
||||||
this.transition.setRate(1.0);
|
|
||||||
this.transition.play();
|
|
||||||
});
|
|
||||||
this.transition.play();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.transition.setOnFinished(null);
|
|
||||||
if (marked) {
|
|
||||||
this.updateMarkShape(selected, indeterminate);
|
|
||||||
}
|
|
||||||
this.transition.setRate(marked ? 1.0 : -1.0);
|
|
||||||
this.fillTransition.setRate(marked ? 1.0 : -1.0);
|
|
||||||
this.fillTransition.play();
|
|
||||||
this.transition.play();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stops selection animations and immediately displays the checkbox's current state.
|
|
||||||
///
|
|
||||||
/// The state indicator, fill, and border are updated even if no animation is running.
|
|
||||||
/// This also suppresses pending selection updates and the initial selection animation.
|
|
||||||
/// The skin must not have been disposed.
|
|
||||||
public void finishSelectionAnimation() {
|
|
||||||
CheckBox checkBox = this.getSkinnable();
|
|
||||||
this.finishSelectionAnimation(checkBox.isSelected(), checkBox.isIndeterminate());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void finishSelectionAnimation(boolean selected, boolean indeterminate) {
|
|
||||||
this.selected = selected;
|
|
||||||
this.indeterminate = indeterminate;
|
|
||||||
boolean marked = selected || indeterminate;
|
|
||||||
transition.stop();
|
|
||||||
transition.setOnFinished(null);
|
|
||||||
transition.jumpTo(marked ? transition.getCycleDuration() : Duration.ZERO);
|
|
||||||
fillTransition.stop();
|
|
||||||
fillTransition.jumpTo(marked ? fillTransition.getCycleDuration() : Duration.ZERO);
|
|
||||||
invalid = false;
|
|
||||||
updateMarkShape(selected, indeterminate);
|
|
||||||
mark.setVisible(marked);
|
|
||||||
mark.setScaleX(marked ? 1.0 : 0.0);
|
|
||||||
mark.setScaleY(marked ? 1.0 : 0.0);
|
|
||||||
updateColors(selected, indeterminate);
|
|
||||||
}
|
|
||||||
|
|
||||||
private JFXFillTransition createFillTransition() {
|
|
||||||
JFXFillTransition fillTransition = new JFXFillTransition(Duration.millis(120.0), this.box, Color.TRANSPARENT,
|
|
||||||
(Color) ((JFXCheckBox) this.getSkinnable()).getCheckedColor());
|
|
||||||
fillTransition.setInterpolator(Interpolator.EASE_OUT);
|
|
||||||
return fillTransition;
|
|
||||||
}
|
|
||||||
|
|
||||||
private final class CheckBoxTransition extends CachedTransition {
|
|
||||||
CheckBoxTransition() {
|
|
||||||
super(JFXCheckBoxSkin.this.mark,
|
|
||||||
new Timeline(
|
|
||||||
new KeyFrame(Duration.ZERO,
|
|
||||||
new KeyValue(JFXCheckBoxSkin.this.mark.visibleProperty(), false, Interpolator.EASE_OUT),
|
|
||||||
new KeyValue(JFXCheckBoxSkin.this.mark.scaleXProperty(), (double) 0.5F, Interpolator.EASE_OUT),
|
|
||||||
new KeyValue(JFXCheckBoxSkin.this.mark.scaleYProperty(), (double) 0.5F, Interpolator.EASE_OUT)),
|
|
||||||
new KeyFrame(Duration.millis(400.0),
|
|
||||||
new KeyValue(JFXCheckBoxSkin.this.mark.visibleProperty(), true, Interpolator.EASE_OUT),
|
|
||||||
new KeyValue(JFXCheckBoxSkin.this.mark.scaleXProperty(), (double) 0.5F, Interpolator.EASE_OUT),
|
|
||||||
new KeyValue(JFXCheckBoxSkin.this.mark.scaleYProperty(), (double) 0.5F, Interpolator.EASE_OUT)),
|
|
||||||
new KeyFrame(Duration.millis(1000.0),
|
|
||||||
new KeyValue(JFXCheckBoxSkin.this.mark.visibleProperty(), true, Interpolator.EASE_OUT),
|
|
||||||
new KeyValue(JFXCheckBoxSkin.this.mark.scaleXProperty(), 1, Interpolator.EASE_OUT),
|
|
||||||
new KeyValue(JFXCheckBoxSkin.this.mark.scaleYProperty(), 1, Interpolator.EASE_OUT))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
this.setCycleDuration(Duration.seconds(0.12));
|
|
||||||
this.setDelay(Duration.seconds(0.05));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,623 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.jfoenix.skins;
|
|
||||||
|
|
||||||
import com.jfoenix.controls.JFXButton;
|
|
||||||
import com.jfoenix.controls.JFXColorPicker;
|
|
||||||
import com.jfoenix.utils.JFXNodeUtils;
|
|
||||||
import javafx.collections.FXCollections;
|
|
||||||
import javafx.collections.ListChangeListener.Change;
|
|
||||||
import javafx.collections.ObservableList;
|
|
||||||
import javafx.event.ActionEvent;
|
|
||||||
import javafx.event.Event;
|
|
||||||
import javafx.geometry.Bounds;
|
|
||||||
import javafx.geometry.NodeOrientation;
|
|
||||||
import javafx.geometry.Pos;
|
|
||||||
import javafx.scene.Node;
|
|
||||||
import javafx.scene.control.Label;
|
|
||||||
import javafx.scene.control.PopupControl;
|
|
||||||
import javafx.scene.control.Tooltip;
|
|
||||||
import javafx.scene.input.KeyEvent;
|
|
||||||
import javafx.scene.input.MouseButton;
|
|
||||||
import javafx.scene.input.MouseEvent;
|
|
||||||
import javafx.scene.layout.*;
|
|
||||||
import javafx.scene.paint.Color;
|
|
||||||
import javafx.scene.shape.Rectangle;
|
|
||||||
import javafx.scene.shape.StrokeType;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Shadi Shaheen FUTURE WORK: this UI will get re-designed to match material design guidlines
|
|
||||||
*/
|
|
||||||
final class JFXColorPalette extends Region {
|
|
||||||
|
|
||||||
private static final int SQUARE_SIZE = 15;
|
|
||||||
|
|
||||||
// package protected for testing purposes
|
|
||||||
JFXColorGrid colorPickerGrid;
|
|
||||||
final JFXButton customColorLink = new JFXButton(i18n("color.custom"));
|
|
||||||
JFXCustomColorPickerDialog customColorDialog = null;
|
|
||||||
|
|
||||||
private final JFXColorPicker colorPicker;
|
|
||||||
private final GridPane customColorGrid = new GridPane();
|
|
||||||
private final Label customColorLabel = new Label(i18n("color.recent"));
|
|
||||||
|
|
||||||
private PopupControl popupControl;
|
|
||||||
private ColorSquare focusedSquare;
|
|
||||||
|
|
||||||
private Color mouseDragColor = null;
|
|
||||||
private boolean dragDetected = false;
|
|
||||||
|
|
||||||
private final ColorSquare hoverSquare = new ColorSquare();
|
|
||||||
|
|
||||||
public JFXColorPalette(final JFXColorPicker colorPicker) {
|
|
||||||
getStyleClass().add("color-palette-region");
|
|
||||||
this.colorPicker = colorPicker;
|
|
||||||
colorPickerGrid = new JFXColorGrid();
|
|
||||||
colorPickerGrid.getChildren().get(0).requestFocus();
|
|
||||||
customColorLabel.setAlignment(Pos.CENTER_LEFT);
|
|
||||||
customColorLink.setPrefWidth(colorPickerGrid.prefWidth(-1));
|
|
||||||
customColorLink.setAlignment(Pos.CENTER);
|
|
||||||
customColorLink.setFocusTraversable(true);
|
|
||||||
customColorLink.setOnAction(ev -> {
|
|
||||||
if (customColorDialog == null) {
|
|
||||||
customColorDialog = new JFXCustomColorPickerDialog(popupControl);
|
|
||||||
customColorDialog.customColorProperty().addListener((ov, t1, t2) -> {
|
|
||||||
colorPicker.setValue(customColorDialog.customColorProperty().get());
|
|
||||||
});
|
|
||||||
customColorDialog.setOnSave(() -> {
|
|
||||||
Color customColor = customColorDialog.customColorProperty().get();
|
|
||||||
buildCustomColors();
|
|
||||||
colorPicker.getCustomColors().add(customColor);
|
|
||||||
updateSelection(customColor);
|
|
||||||
Event.fireEvent(colorPicker, new ActionEvent());
|
|
||||||
colorPicker.hide();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
customColorDialog.setCurrentColor(colorPicker.valueProperty().get());
|
|
||||||
if (popupControl != null) {
|
|
||||||
popupControl.setAutoHide(false);
|
|
||||||
}
|
|
||||||
customColorDialog.show();
|
|
||||||
customColorDialog.setOnHidden(event -> {
|
|
||||||
if (popupControl != null) {
|
|
||||||
popupControl.setAutoHide(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
initNavigation();
|
|
||||||
customColorGrid.getStyleClass().add("color-picker-grid");
|
|
||||||
customColorGrid.setVisible(false);
|
|
||||||
|
|
||||||
buildCustomColors();
|
|
||||||
|
|
||||||
colorPicker.getCustomColors().addListener((Change<? extends Color> change) -> buildCustomColors());
|
|
||||||
VBox paletteBox = new VBox();
|
|
||||||
paletteBox.getStyleClass().add("color-palette");
|
|
||||||
paletteBox.getChildren().addAll(colorPickerGrid);
|
|
||||||
if (colorPicker.getPreDefinedColors() == null) {
|
|
||||||
paletteBox.getChildren().addAll(customColorLabel, customColorGrid, customColorLink);
|
|
||||||
}
|
|
||||||
|
|
||||||
hoverSquare.setMouseTransparent(true);
|
|
||||||
hoverSquare.getStyleClass().addAll("hover-square");
|
|
||||||
setFocusedSquare(null);
|
|
||||||
|
|
||||||
getChildren().addAll(paletteBox, hoverSquare);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setFocusedSquare(ColorSquare square) {
|
|
||||||
hoverSquare.setVisible(square != null);
|
|
||||||
|
|
||||||
if (square == focusedSquare) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
focusedSquare = square;
|
|
||||||
|
|
||||||
hoverSquare.setVisible(focusedSquare != null);
|
|
||||||
if (focusedSquare == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!focusedSquare.isFocused()) {
|
|
||||||
focusedSquare.requestFocus();
|
|
||||||
}
|
|
||||||
|
|
||||||
hoverSquare.rectangle.setFill(focusedSquare.rectangle.getFill());
|
|
||||||
|
|
||||||
Bounds b = square.localToScene(square.getLayoutBounds());
|
|
||||||
|
|
||||||
double x = b.getMinX();
|
|
||||||
double y = b.getMinY();
|
|
||||||
|
|
||||||
double xAdjust;
|
|
||||||
double scaleAdjust = hoverSquare.getScaleX() == 1.0 ? 0 : hoverSquare.getWidth() / 4.0;
|
|
||||||
|
|
||||||
if (colorPicker.getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT) {
|
|
||||||
x = focusedSquare.getLayoutX();
|
|
||||||
xAdjust = -focusedSquare.getWidth() + scaleAdjust;
|
|
||||||
} else {
|
|
||||||
xAdjust = focusedSquare.getWidth() / 2.0 + scaleAdjust;
|
|
||||||
}
|
|
||||||
|
|
||||||
hoverSquare.setLayoutX(snapPositionX(x) - xAdjust);
|
|
||||||
hoverSquare.setLayoutY(snapPositionY(y) - focusedSquare.getHeight() / 2.0 + (hoverSquare.getScaleY() == 1.0 ? 0 : focusedSquare.getHeight() / 4.0));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void buildCustomColors() {
|
|
||||||
final ObservableList<Color> customColors = colorPicker.getCustomColors();
|
|
||||||
customColorGrid.getChildren().clear();
|
|
||||||
if (customColors.isEmpty()) {
|
|
||||||
customColorLabel.setVisible(false);
|
|
||||||
customColorLabel.setManaged(false);
|
|
||||||
customColorGrid.setVisible(false);
|
|
||||||
customColorGrid.setManaged(false);
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
customColorLabel.setVisible(true);
|
|
||||||
customColorLabel.setManaged(true);
|
|
||||||
customColorGrid.setVisible(true);
|
|
||||||
customColorGrid.setManaged(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
int customColumnIndex = 0;
|
|
||||||
int customRowIndex = 0;
|
|
||||||
int remainingSquares = customColors.size() % NUM_OF_COLUMNS;
|
|
||||||
int numEmpty = (remainingSquares == 0) ? 0 : NUM_OF_COLUMNS - remainingSquares;
|
|
||||||
|
|
||||||
for (int i = 0; i < customColors.size(); i++) {
|
|
||||||
Color c = customColors.get(i);
|
|
||||||
ColorSquare square = new ColorSquare(c, i, true);
|
|
||||||
customColorGrid.add(square, customColumnIndex, customRowIndex);
|
|
||||||
customColumnIndex++;
|
|
||||||
if (customColumnIndex == NUM_OF_COLUMNS) {
|
|
||||||
customColumnIndex = 0;
|
|
||||||
customRowIndex++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (int i = 0; i < numEmpty; i++) {
|
|
||||||
ColorSquare emptySquare = new ColorSquare();
|
|
||||||
customColorGrid.add(emptySquare, customColumnIndex, customRowIndex);
|
|
||||||
customColumnIndex++;
|
|
||||||
}
|
|
||||||
requestLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initNavigation() {
|
|
||||||
setOnKeyPressed(ke -> {
|
|
||||||
switch (ke.getCode()) {
|
|
||||||
case SPACE:
|
|
||||||
case ENTER:
|
|
||||||
// select the focused color
|
|
||||||
if (focusedSquare != null) {
|
|
||||||
focusedSquare.selectColor(ke);
|
|
||||||
}
|
|
||||||
ke.consume();
|
|
||||||
break;
|
|
||||||
default: // no-op
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPopupControl(PopupControl pc) {
|
|
||||||
this.popupControl = pc;
|
|
||||||
}
|
|
||||||
|
|
||||||
public JFXColorGrid getColorGrid() {
|
|
||||||
return colorPickerGrid;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isCustomColorDialogShowing() {
|
|
||||||
return customColorDialog != null && customColorDialog.isVisible();
|
|
||||||
}
|
|
||||||
|
|
||||||
class ColorSquare extends StackPane {
|
|
||||||
Rectangle rectangle;
|
|
||||||
boolean isEmpty;
|
|
||||||
|
|
||||||
public ColorSquare() {
|
|
||||||
this(null, -1, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ColorSquare(Color color, int index) {
|
|
||||||
this(color, index, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ColorSquare(Color color, int index, boolean isCustom) {
|
|
||||||
// Add style class to handle selected color square
|
|
||||||
getStyleClass().add("color-square");
|
|
||||||
if (color != null) {
|
|
||||||
setFocusTraversable(true);
|
|
||||||
focusedProperty().addListener((s, ov, nv) -> setFocusedSquare(nv ? this : null));
|
|
||||||
addEventHandler(MouseEvent.MOUSE_ENTERED, event -> setFocusedSquare(ColorSquare.this));
|
|
||||||
addEventHandler(MouseEvent.MOUSE_EXITED, event -> setFocusedSquare(null));
|
|
||||||
addEventHandler(MouseEvent.MOUSE_RELEASED, event -> {
|
|
||||||
if (!dragDetected && event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 1) {
|
|
||||||
if (!isEmpty) {
|
|
||||||
Color fill = (Color) rectangle.getFill();
|
|
||||||
colorPicker.setValue(fill);
|
|
||||||
colorPicker.fireEvent(new ActionEvent());
|
|
||||||
updateSelection(fill);
|
|
||||||
event.consume();
|
|
||||||
}
|
|
||||||
colorPicker.hide();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
rectangle = new Rectangle(SQUARE_SIZE, SQUARE_SIZE);
|
|
||||||
if (color == null) {
|
|
||||||
rectangle.setFill(Color.WHITE);
|
|
||||||
isEmpty = true;
|
|
||||||
} else {
|
|
||||||
rectangle.setFill(color);
|
|
||||||
}
|
|
||||||
|
|
||||||
rectangle.setStrokeType(StrokeType.INSIDE);
|
|
||||||
|
|
||||||
String tooltipStr = JFXNodeUtils.colorToHex(color);
|
|
||||||
Tooltip.install(this, new Tooltip((tooltipStr == null) ? "" : tooltipStr));
|
|
||||||
|
|
||||||
rectangle.getStyleClass().add("color-rect");
|
|
||||||
getChildren().add(rectangle);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void selectColor(KeyEvent event) {
|
|
||||||
if (rectangle.getFill() != null) {
|
|
||||||
if (rectangle.getFill() instanceof Color) {
|
|
||||||
colorPicker.setValue((Color) rectangle.getFill());
|
|
||||||
colorPicker.fireEvent(new ActionEvent());
|
|
||||||
}
|
|
||||||
event.consume();
|
|
||||||
}
|
|
||||||
colorPicker.hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The skin can update selection if colorpicker value changes..
|
|
||||||
public void updateSelection(Color color) {
|
|
||||||
setFocusedSquare(null);
|
|
||||||
|
|
||||||
for (ColorSquare c : colorPickerGrid.getSquares()) {
|
|
||||||
if (c.rectangle.getFill().equals(color)) {
|
|
||||||
setFocusedSquare(c);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// check custom colors
|
|
||||||
for (Node n : customColorGrid.getChildren()) {
|
|
||||||
ColorSquare c = (ColorSquare) n;
|
|
||||||
if (c.rectangle.getFill().equals(color)) {
|
|
||||||
setFocusedSquare(c);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class JFXColorGrid extends GridPane {
|
|
||||||
|
|
||||||
private final List<ColorSquare> squares;
|
|
||||||
final int NUM_OF_COLORS;
|
|
||||||
final int NUM_OF_ROWS;
|
|
||||||
|
|
||||||
public JFXColorGrid() {
|
|
||||||
getStyleClass().add("color-picker-grid");
|
|
||||||
setId("ColorCustomizerColorGrid");
|
|
||||||
int columnIndex = 0;
|
|
||||||
int rowIndex = 0;
|
|
||||||
squares = FXCollections.observableArrayList();
|
|
||||||
double[] limitedColors = colorPicker.getPreDefinedColors();
|
|
||||||
limitedColors = limitedColors == null ? RAW_VALUES : limitedColors;
|
|
||||||
NUM_OF_COLORS = limitedColors.length / 3;
|
|
||||||
NUM_OF_ROWS = (int) Math.ceil((double) NUM_OF_COLORS / (double) NUM_OF_COLUMNS);
|
|
||||||
final int numColors = limitedColors.length / 3;
|
|
||||||
Color[] colors = new Color[numColors];
|
|
||||||
for (int i = 0; i < numColors; i++) {
|
|
||||||
colors[i] = new Color(limitedColors[i * 3] / 255,
|
|
||||||
limitedColors[(i * 3) + 1] / 255, limitedColors[(i * 3) + 2] / 255,
|
|
||||||
1.0);
|
|
||||||
ColorSquare cs = new ColorSquare(colors[i], i);
|
|
||||||
squares.add(cs);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (ColorSquare square : squares) {
|
|
||||||
add(square, columnIndex, rowIndex);
|
|
||||||
columnIndex++;
|
|
||||||
if (columnIndex == NUM_OF_COLUMNS) {
|
|
||||||
columnIndex = 0;
|
|
||||||
rowIndex++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setOnMouseDragged(t -> {
|
|
||||||
if (!dragDetected) {
|
|
||||||
dragDetected = true;
|
|
||||||
mouseDragColor = colorPicker.getValue();
|
|
||||||
}
|
|
||||||
int xIndex = clamp(0,
|
|
||||||
(int) t.getX() / (SQUARE_SIZE + 1), NUM_OF_COLUMNS - 1);
|
|
||||||
int yIndex = clamp(0,
|
|
||||||
(int) t.getY() / (SQUARE_SIZE + 1), NUM_OF_ROWS - 1);
|
|
||||||
int index = xIndex + yIndex * NUM_OF_COLUMNS;
|
|
||||||
colorPicker.setValue((Color) squares.get(index).rectangle.getFill());
|
|
||||||
updateSelection(colorPicker.getValue());
|
|
||||||
});
|
|
||||||
addEventHandler(MouseEvent.MOUSE_RELEASED, t -> {
|
|
||||||
if (colorPickerGrid.getBoundsInLocal().contains(t.getX(), t.getY())) {
|
|
||||||
updateSelection(colorPicker.getValue());
|
|
||||||
colorPicker.fireEvent(new ActionEvent());
|
|
||||||
colorPicker.hide();
|
|
||||||
} else {
|
|
||||||
// restore color as mouse release happened outside the grid.
|
|
||||||
if (mouseDragColor != null) {
|
|
||||||
colorPicker.setValue(mouseDragColor);
|
|
||||||
updateSelection(mouseDragColor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dragDetected = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<ColorSquare> getSquares() {
|
|
||||||
return squares;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected double computePrefWidth(double height) {
|
|
||||||
return (SQUARE_SIZE + 1) * NUM_OF_COLUMNS;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected double computePrefHeight(double width) {
|
|
||||||
return (SQUARE_SIZE + 1) * NUM_OF_ROWS;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final int NUM_OF_COLUMNS = 10;
|
|
||||||
private static final double[] RAW_VALUES = {
|
|
||||||
// WARNING: always make sure the number of colors is a divisable by NUM_OF_COLUMNS
|
|
||||||
250, 250, 250, // first row
|
|
||||||
245, 245, 245,
|
|
||||||
238, 238, 238,
|
|
||||||
224, 224, 224,
|
|
||||||
189, 189, 189,
|
|
||||||
158, 158, 158,
|
|
||||||
117, 117, 117,
|
|
||||||
97, 97, 97,
|
|
||||||
66, 66, 66,
|
|
||||||
33, 33, 33,
|
|
||||||
// second row
|
|
||||||
236, 239, 241,
|
|
||||||
207, 216, 220,
|
|
||||||
176, 190, 197,
|
|
||||||
144, 164, 174,
|
|
||||||
120, 144, 156,
|
|
||||||
96, 125, 139,
|
|
||||||
84, 110, 122,
|
|
||||||
69, 90, 100,
|
|
||||||
55, 71, 79,
|
|
||||||
38, 50, 56,
|
|
||||||
// third row
|
|
||||||
255, 235, 238,
|
|
||||||
255, 205, 210,
|
|
||||||
239, 154, 154,
|
|
||||||
229, 115, 115,
|
|
||||||
239, 83, 80,
|
|
||||||
244, 67, 54,
|
|
||||||
229, 57, 53,
|
|
||||||
211, 47, 47,
|
|
||||||
198, 40, 40,
|
|
||||||
183, 28, 28,
|
|
||||||
// forth row
|
|
||||||
252, 228, 236,
|
|
||||||
248, 187, 208,
|
|
||||||
244, 143, 177,
|
|
||||||
240, 98, 146,
|
|
||||||
236, 64, 122,
|
|
||||||
233, 30, 99,
|
|
||||||
216, 27, 96,
|
|
||||||
194, 24, 91,
|
|
||||||
173, 20, 87,
|
|
||||||
136, 14, 79,
|
|
||||||
// fifth row
|
|
||||||
243, 229, 245,
|
|
||||||
225, 190, 231,
|
|
||||||
206, 147, 216,
|
|
||||||
186, 104, 200,
|
|
||||||
171, 71, 188,
|
|
||||||
156, 39, 176,
|
|
||||||
142, 36, 170,
|
|
||||||
123, 31, 162,
|
|
||||||
106, 27, 154,
|
|
||||||
74, 20, 140,
|
|
||||||
// sixth row
|
|
||||||
237, 231, 246,
|
|
||||||
209, 196, 233,
|
|
||||||
179, 157, 219,
|
|
||||||
149, 117, 205,
|
|
||||||
126, 87, 194,
|
|
||||||
103, 58, 183,
|
|
||||||
94, 53, 177,
|
|
||||||
81, 45, 168,
|
|
||||||
69, 39, 160,
|
|
||||||
49, 27, 146,
|
|
||||||
// seventh row
|
|
||||||
232, 234, 246,
|
|
||||||
197, 202, 233,
|
|
||||||
159, 168, 218,
|
|
||||||
121, 134, 203,
|
|
||||||
92, 107, 192,
|
|
||||||
63, 81, 181,
|
|
||||||
57, 73, 171,
|
|
||||||
48, 63, 159,
|
|
||||||
40, 53, 147,
|
|
||||||
26, 35, 126,
|
|
||||||
// eigth row
|
|
||||||
227, 242, 253,
|
|
||||||
187, 222, 251,
|
|
||||||
144, 202, 249,
|
|
||||||
100, 181, 246,
|
|
||||||
66, 165, 245,
|
|
||||||
33, 150, 243,
|
|
||||||
30, 136, 229,
|
|
||||||
25, 118, 210,
|
|
||||||
21, 101, 192,
|
|
||||||
13, 71, 161,
|
|
||||||
// ninth row
|
|
||||||
225, 245, 254,
|
|
||||||
179, 229, 252,
|
|
||||||
129, 212, 250,
|
|
||||||
79, 195, 247,
|
|
||||||
41, 182, 246,
|
|
||||||
3, 169, 244,
|
|
||||||
3, 155, 229,
|
|
||||||
2, 136, 209,
|
|
||||||
2, 119, 189,
|
|
||||||
1, 87, 155,
|
|
||||||
// tenth row
|
|
||||||
224, 247, 250,
|
|
||||||
178, 235, 242,
|
|
||||||
128, 222, 234,
|
|
||||||
77, 208, 225,
|
|
||||||
38, 198, 218,
|
|
||||||
0, 188, 212,
|
|
||||||
0, 172, 193,
|
|
||||||
0, 151, 167,
|
|
||||||
0, 131, 143,
|
|
||||||
0, 96, 100,
|
|
||||||
// eleventh row
|
|
||||||
224, 242, 241,
|
|
||||||
178, 223, 219,
|
|
||||||
128, 203, 196,
|
|
||||||
77, 182, 172,
|
|
||||||
38, 166, 154,
|
|
||||||
0, 150, 136,
|
|
||||||
0, 137, 123,
|
|
||||||
0, 121, 107,
|
|
||||||
0, 105, 92,
|
|
||||||
0, 77, 64,
|
|
||||||
// twelfth row
|
|
||||||
232, 245, 233,
|
|
||||||
200, 230, 201,
|
|
||||||
165, 214, 167,
|
|
||||||
129, 199, 132,
|
|
||||||
102, 187, 106,
|
|
||||||
76, 175, 80,
|
|
||||||
67, 160, 71,
|
|
||||||
56, 142, 60,
|
|
||||||
46, 125, 50,
|
|
||||||
27, 94, 32,
|
|
||||||
|
|
||||||
// thirteenth row
|
|
||||||
241, 248, 233,
|
|
||||||
220, 237, 200,
|
|
||||||
197, 225, 165,
|
|
||||||
174, 213, 129,
|
|
||||||
156, 204, 101,
|
|
||||||
139, 195, 74,
|
|
||||||
124, 179, 66,
|
|
||||||
104, 159, 56,
|
|
||||||
85, 139, 47,
|
|
||||||
51, 105, 30,
|
|
||||||
// fourteenth row
|
|
||||||
249, 251, 231,
|
|
||||||
240, 244, 195,
|
|
||||||
230, 238, 156,
|
|
||||||
220, 231, 117,
|
|
||||||
212, 225, 87,
|
|
||||||
205, 220, 57,
|
|
||||||
192, 202, 51,
|
|
||||||
175, 180, 43,
|
|
||||||
158, 157, 36,
|
|
||||||
130, 119, 23,
|
|
||||||
|
|
||||||
// fifteenth row
|
|
||||||
255, 253, 231,
|
|
||||||
255, 249, 196,
|
|
||||||
255, 245, 157,
|
|
||||||
255, 241, 118,
|
|
||||||
255, 238, 88,
|
|
||||||
255, 235, 59,
|
|
||||||
253, 216, 53,
|
|
||||||
251, 192, 45,
|
|
||||||
249, 168, 37,
|
|
||||||
245, 127, 23,
|
|
||||||
|
|
||||||
// sixteenth row
|
|
||||||
255, 248, 225,
|
|
||||||
255, 236, 179,
|
|
||||||
255, 224, 130,
|
|
||||||
255, 213, 79,
|
|
||||||
255, 202, 40,
|
|
||||||
255, 193, 7,
|
|
||||||
255, 179, 0,
|
|
||||||
255, 160, 0,
|
|
||||||
255, 143, 0,
|
|
||||||
255, 111, 0,
|
|
||||||
|
|
||||||
// seventeenth row
|
|
||||||
255, 243, 224,
|
|
||||||
255, 224, 178,
|
|
||||||
255, 204, 128,
|
|
||||||
255, 183, 77,
|
|
||||||
255, 167, 38,
|
|
||||||
255, 152, 0,
|
|
||||||
251, 140, 0,
|
|
||||||
245, 124, 0,
|
|
||||||
239, 108, 0,
|
|
||||||
230, 81, 0,
|
|
||||||
|
|
||||||
// eighteenth row
|
|
||||||
251, 233, 231,
|
|
||||||
255, 204, 188,
|
|
||||||
255, 171, 145,
|
|
||||||
255, 138, 101,
|
|
||||||
255, 112, 67,
|
|
||||||
255, 87, 34,
|
|
||||||
244, 81, 30,
|
|
||||||
230, 74, 25,
|
|
||||||
216, 67, 21,
|
|
||||||
191, 54, 12,
|
|
||||||
|
|
||||||
// nineteenth row
|
|
||||||
239, 235, 233,
|
|
||||||
215, 204, 200,
|
|
||||||
188, 170, 164,
|
|
||||||
161, 136, 127,
|
|
||||||
141, 110, 99,
|
|
||||||
121, 85, 72,
|
|
||||||
109, 76, 65,
|
|
||||||
93, 64, 55,
|
|
||||||
78, 52, 46,
|
|
||||||
62, 39, 35,
|
|
||||||
};
|
|
||||||
|
|
||||||
private static int clamp(int min, int value, int max) {
|
|
||||||
if (value < min) {
|
|
||||||
return min;
|
|
||||||
}
|
|
||||||
if (value > max) {
|
|
||||||
return max;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,258 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.jfoenix.skins;
|
|
||||||
|
|
||||||
import com.jfoenix.controls.JFXClippedPane;
|
|
||||||
import com.jfoenix.controls.JFXColorPicker;
|
|
||||||
import com.jfoenix.controls.JFXRippler;
|
|
||||||
import com.jfoenix.effects.JFXDepthManager;
|
|
||||||
import com.jfoenix.utils.JFXNodeUtils;
|
|
||||||
import javafx.animation.Interpolator;
|
|
||||||
import javafx.animation.KeyFrame;
|
|
||||||
import javafx.animation.KeyValue;
|
|
||||||
import javafx.animation.Timeline;
|
|
||||||
import javafx.beans.property.ObjectProperty;
|
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
|
||||||
import javafx.css.*;
|
|
||||||
import javafx.css.converter.BooleanConverter;
|
|
||||||
import javafx.geometry.Insets;
|
|
||||||
import javafx.scene.Node;
|
|
||||||
import javafx.scene.control.ColorPicker;
|
|
||||||
import javafx.scene.control.Label;
|
|
||||||
import javafx.scene.control.TextField;
|
|
||||||
import javafx.scene.control.skin.ComboBoxPopupControl;
|
|
||||||
import javafx.scene.layout.Background;
|
|
||||||
import javafx.scene.layout.BackgroundFill;
|
|
||||||
import javafx.scene.layout.CornerRadii;
|
|
||||||
import javafx.scene.paint.Color;
|
|
||||||
import javafx.scene.shape.Circle;
|
|
||||||
import javafx.util.Duration;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Shadi Shaheen
|
|
||||||
*/
|
|
||||||
public final class JFXColorPickerSkin extends JFXGenericPickerSkin<Color> {
|
|
||||||
|
|
||||||
private final Label displayNode;
|
|
||||||
private final JFXClippedPane colorBox;
|
|
||||||
private JFXColorPalette popupContent;
|
|
||||||
StyleableBooleanProperty colorLabelVisible = new SimpleStyleableBooleanProperty(StyleableProperties.COLOR_LABEL_VISIBLE,
|
|
||||||
JFXColorPickerSkin.this,
|
|
||||||
"colorLabelVisible",
|
|
||||||
true);
|
|
||||||
|
|
||||||
private final ObjectProperty<Color> textFill = new SimpleObjectProperty<>(Color.BLACK);
|
|
||||||
private final ObjectProperty<Background> colorBoxBackground = new SimpleObjectProperty<>(null);
|
|
||||||
|
|
||||||
public JFXColorPickerSkin(final ColorPicker colorPicker) {
|
|
||||||
super(colorPicker);
|
|
||||||
|
|
||||||
// create displayNode
|
|
||||||
displayNode = new Label("");
|
|
||||||
displayNode.getStyleClass().add("color-label");
|
|
||||||
displayNode.setMouseTransparent(true);
|
|
||||||
displayNode.textFillProperty().bind(textFill);
|
|
||||||
|
|
||||||
// label graphic
|
|
||||||
colorBox = new JFXClippedPane(displayNode);
|
|
||||||
colorBox.getStyleClass().add("color-box");
|
|
||||||
colorBox.setManaged(false);
|
|
||||||
colorBox.backgroundProperty().bind(colorBoxBackground);
|
|
||||||
initColor();
|
|
||||||
final JFXRippler rippler = new JFXRippler(colorBox, JFXRippler.RipplerMask.FIT);
|
|
||||||
rippler.ripplerFillProperty().bind(textFill);
|
|
||||||
getChildren().setAll(rippler);
|
|
||||||
JFXDepthManager.setDepth(getSkinnable(), 1);
|
|
||||||
getSkinnable().setPickOnBounds(false);
|
|
||||||
|
|
||||||
colorPicker.focusedProperty().addListener(observable -> {
|
|
||||||
if (colorPicker.isFocused()) {
|
|
||||||
if (!getSkinnable().isPressed()) {
|
|
||||||
rippler.setOverlayVisible(true);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
rippler.setOverlayVisible(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// add listeners
|
|
||||||
registerChangeListener(colorPicker.valueProperty(), obs -> updateColor());
|
|
||||||
|
|
||||||
colorLabelVisible.addListener(invalidate -> {
|
|
||||||
if (colorLabelVisible.get()) {
|
|
||||||
displayNode.setText(JFXNodeUtils.colorToHex(getSkinnable().getValue()));
|
|
||||||
} else {
|
|
||||||
displayNode.setText("");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
|
|
||||||
double width = 100;
|
|
||||||
String displayNodeText = displayNode.getText();
|
|
||||||
displayNode.setText("#DDDDDD");
|
|
||||||
width = Math.max(width, super.computePrefWidth(height, topInset, rightInset, bottomInset, leftInset));
|
|
||||||
displayNode.setText(displayNodeText);
|
|
||||||
return width + rightInset + leftInset;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
|
|
||||||
if (colorBox == null) {
|
|
||||||
reflectUpdateDisplayArea();
|
|
||||||
}
|
|
||||||
return topInset + colorBox.prefHeight(width) + bottomInset;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void layoutChildren(double x, double y, double w, double h) {
|
|
||||||
super.layoutChildren(x, y, w, h);
|
|
||||||
double hInsets = snappedLeftInset() + snappedRightInset();
|
|
||||||
double vInsets = snappedTopInset() + snappedBottomInset();
|
|
||||||
double width = w + hInsets;
|
|
||||||
double height = h + vInsets;
|
|
||||||
colorBox.resizeRelocate(0, 0, width, height);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Node getPopupContent() {
|
|
||||||
if (popupContent == null) {
|
|
||||||
popupContent = new JFXColorPalette((JFXColorPicker) getSkinnable());
|
|
||||||
}
|
|
||||||
return popupContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void show() {
|
|
||||||
super.show();
|
|
||||||
final ColorPicker colorPicker = (ColorPicker) getSkinnable();
|
|
||||||
popupContent.updateSelection(colorPicker.getValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Node getDisplayNode() {
|
|
||||||
return displayNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateColor() {
|
|
||||||
final ColorPicker colorPicker = (ColorPicker) getSkinnable();
|
|
||||||
Color color = colorPicker.getValue();
|
|
||||||
Color circleColor = color == null ? Color.WHITE : color;
|
|
||||||
// update picker box color
|
|
||||||
if (((JFXColorPicker) getSkinnable()).isDisableAnimation()) {
|
|
||||||
JFXNodeUtils.updateBackground(colorBox.getBackground(), colorBox, circleColor);
|
|
||||||
} else {
|
|
||||||
Circle colorCircle = new Circle();
|
|
||||||
colorCircle.setFill(circleColor);
|
|
||||||
colorCircle.setManaged(false);
|
|
||||||
colorCircle.setLayoutX(colorBox.getWidth() / 4);
|
|
||||||
colorCircle.setLayoutY(colorBox.getHeight() / 2);
|
|
||||||
colorBox.getChildren().add(colorCircle);
|
|
||||||
Timeline animateColor = new Timeline(new KeyFrame(Duration.millis(240),
|
|
||||||
new KeyValue(colorCircle.radiusProperty(),
|
|
||||||
200,
|
|
||||||
Interpolator.EASE_BOTH)));
|
|
||||||
animateColor.setOnFinished((finish) -> {
|
|
||||||
colorBoxBackground.set(new Background(new BackgroundFill(colorCircle.getFill(), new CornerRadii(3), Insets.EMPTY)));
|
|
||||||
colorBox.getChildren().remove(colorCircle);
|
|
||||||
});
|
|
||||||
animateColor.play();
|
|
||||||
}
|
|
||||||
// update label color
|
|
||||||
textFill.set(circleColor.grayscale().getRed() < 0.5 ? Color.valueOf(
|
|
||||||
"rgba(255, 255, 255, 0.87)") : Color.valueOf("rgba(0, 0, 0, 0.87)"));
|
|
||||||
if (colorLabelVisible.get()) {
|
|
||||||
displayNode.setText(JFXNodeUtils.colorToHex(circleColor));
|
|
||||||
} else {
|
|
||||||
displayNode.setText("");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initColor() {
|
|
||||||
final ColorPicker colorPicker = (ColorPicker) getSkinnable();
|
|
||||||
Color color = colorPicker.getValue();
|
|
||||||
Color circleColor = color == null ? Color.WHITE : color;
|
|
||||||
// update picker box color
|
|
||||||
colorBoxBackground.set(new Background(new BackgroundFill(circleColor, new CornerRadii(3), Insets.EMPTY)));
|
|
||||||
// update label color
|
|
||||||
textFill.set(circleColor.grayscale().getRed() < 0.5 ? Color.valueOf(
|
|
||||||
"rgba(255, 255, 255, 0.87)") : Color.valueOf("rgba(0, 0, 0, 0.87)"));
|
|
||||||
if (colorLabelVisible.get()) {
|
|
||||||
displayNode.setText(JFXNodeUtils.colorToHex(circleColor));
|
|
||||||
} else {
|
|
||||||
displayNode.setText("");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/***************************************************************************
|
|
||||||
* *
|
|
||||||
* Stylesheet Handling *
|
|
||||||
* *
|
|
||||||
**************************************************************************/
|
|
||||||
|
|
||||||
private static final class StyleableProperties {
|
|
||||||
private static final CssMetaData<ColorPicker, Boolean> COLOR_LABEL_VISIBLE =
|
|
||||||
new CssMetaData<ColorPicker, Boolean>("-fx-color-label-visible",
|
|
||||||
BooleanConverter.getInstance(), Boolean.TRUE) {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isSettable(ColorPicker n) {
|
|
||||||
final JFXColorPickerSkin skin = (JFXColorPickerSkin) n.getSkin();
|
|
||||||
return skin.colorLabelVisible == null || !skin.colorLabelVisible.isBound();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public StyleableProperty<Boolean> getStyleableProperty(ColorPicker n) {
|
|
||||||
final JFXColorPickerSkin skin = (JFXColorPickerSkin) n.getSkin();
|
|
||||||
return skin.colorLabelVisible;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
|
|
||||||
|
|
||||||
static {
|
|
||||||
final List<CssMetaData<? extends Styleable, ?>> styleables =
|
|
||||||
new ArrayList<>(ComboBoxPopupControl.getClassCssMetaData());
|
|
||||||
styleables.add(COLOR_LABEL_VISIBLE);
|
|
||||||
STYLEABLES = Collections.unmodifiableList(styleables);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
|
|
||||||
return StyleableProperties.STYLEABLES;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
|
|
||||||
return getClassCssMetaData();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected TextField getEditor() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected javafx.util.StringConverter<Color> getConverter() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,629 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.jfoenix.skins;
|
|
||||||
|
|
||||||
import com.jfoenix.effects.JFXDepthManager;
|
|
||||||
import com.jfoenix.transitions.CachedTransition;
|
|
||||||
import javafx.animation.Animation.Status;
|
|
||||||
import javafx.animation.*;
|
|
||||||
import javafx.collections.FXCollections;
|
|
||||||
import javafx.collections.ObservableList;
|
|
||||||
import javafx.geometry.Point2D;
|
|
||||||
import javafx.scene.Node;
|
|
||||||
import javafx.scene.effect.ColorAdjust;
|
|
||||||
import javafx.scene.image.Image;
|
|
||||||
import javafx.scene.image.ImageView;
|
|
||||||
import javafx.scene.image.PixelWriter;
|
|
||||||
import javafx.scene.image.WritableImage;
|
|
||||||
import javafx.scene.input.MouseEvent;
|
|
||||||
import javafx.scene.layout.Pane;
|
|
||||||
import javafx.scene.paint.Color;
|
|
||||||
import javafx.scene.shape.Circle;
|
|
||||||
import javafx.scene.shape.Path;
|
|
||||||
import javafx.util.Duration;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Shadi Shaheen & Bassel El Mabsout this UI allows the user to pick a color using HSL color system
|
|
||||||
*/
|
|
||||||
final class JFXColorPickerUI extends Pane {
|
|
||||||
|
|
||||||
private CachedTransition selectorTransition;
|
|
||||||
private int pickerSize = 400;
|
|
||||||
// sl circle selector size
|
|
||||||
private final int selectorSize = 20;
|
|
||||||
private final double centerX;
|
|
||||||
private final double centerY;
|
|
||||||
private final double huesRadius;
|
|
||||||
private final double slRadius;
|
|
||||||
private double currentHue = 0;
|
|
||||||
|
|
||||||
private final ImageView huesCircleView;
|
|
||||||
private final ImageView slCircleView;
|
|
||||||
private final Pane colorSelector;
|
|
||||||
private final Pane selector;
|
|
||||||
private CurveTransition colorsTransition;
|
|
||||||
|
|
||||||
public JFXColorPickerUI(int pickerSize) {
|
|
||||||
JFXDepthManager.setDepth(this, 1);
|
|
||||||
|
|
||||||
this.pickerSize = pickerSize;
|
|
||||||
this.centerX = (double) pickerSize / 2;
|
|
||||||
this.centerY = (double) pickerSize / 2;
|
|
||||||
final double pickerRadius = (double) pickerSize / 2;
|
|
||||||
this.huesRadius = pickerRadius * 0.9;
|
|
||||||
final double huesSmallR = pickerRadius * 0.8;
|
|
||||||
final double huesLargeR = pickerRadius;
|
|
||||||
this.slRadius = pickerRadius * 0.7;
|
|
||||||
|
|
||||||
// Create Hues Circle
|
|
||||||
huesCircleView = new ImageView(getHuesCircle(pickerSize, pickerSize));
|
|
||||||
// clip to smooth the edges
|
|
||||||
Circle outterCircle = new Circle(centerX, centerY, huesLargeR - 2);
|
|
||||||
Circle innterCircle = new Circle(centerX, centerY, huesSmallR + 2);
|
|
||||||
huesCircleView.setClip(Path.subtract(outterCircle, innterCircle));
|
|
||||||
this.getChildren().add(huesCircleView);
|
|
||||||
|
|
||||||
// create Hues Circle Selector
|
|
||||||
Circle r1 = new Circle(pickerRadius - huesSmallR);
|
|
||||||
Circle r2 = new Circle(pickerRadius - huesRadius);
|
|
||||||
colorSelector = new Pane();
|
|
||||||
colorSelector.setStyle("-fx-border-color:#424242; -fx-border-width:1px; -fx-background-color:rgba(255, 255, 255, 0.87);");
|
|
||||||
colorSelector.setPrefSize(pickerRadius - huesSmallR, pickerRadius - huesSmallR);
|
|
||||||
colorSelector.setShape(Path.subtract(r1, r2));
|
|
||||||
colorSelector.setCache(true);
|
|
||||||
colorSelector.setMouseTransparent(true);
|
|
||||||
colorSelector.setPickOnBounds(false);
|
|
||||||
this.getChildren().add(colorSelector);
|
|
||||||
|
|
||||||
// add Hues Selection Listeners
|
|
||||||
huesCircleView.addEventHandler(MouseEvent.MOUSE_DRAGGED, (event) -> {
|
|
||||||
if (colorsTransition != null) {
|
|
||||||
colorsTransition.stop();
|
|
||||||
}
|
|
||||||
double dx = event.getX() - centerX;
|
|
||||||
double dy = event.getY() - centerY;
|
|
||||||
double theta = Math.atan2(dy, dx);
|
|
||||||
double x = centerX + huesRadius * Math.cos(theta);
|
|
||||||
double y = centerY + huesRadius * Math.sin(theta);
|
|
||||||
colorSelector.setRotate(90 + Math.toDegrees(Math.atan2(dy, dx)));
|
|
||||||
colorSelector.setTranslateX(x - colorSelector.getPrefWidth() / 2);
|
|
||||||
colorSelector.setTranslateY(y - colorSelector.getPrefHeight() / 2);
|
|
||||||
});
|
|
||||||
huesCircleView.addEventHandler(MouseEvent.MOUSE_PRESSED, (event) -> {
|
|
||||||
double dx = event.getX() - centerX;
|
|
||||||
double dy = event.getY() - centerY;
|
|
||||||
double theta = Math.atan2(dy, dx);
|
|
||||||
double x = centerX + huesRadius * Math.cos(theta);
|
|
||||||
double y = centerY + huesRadius * Math.sin(theta);
|
|
||||||
colorsTransition = new CurveTransition(new Point2D(colorSelector.getTranslateX() + colorSelector.getPrefWidth() / 2,
|
|
||||||
colorSelector.getTranslateY() + colorSelector.getPrefHeight() / 2),
|
|
||||||
new Point2D(x, y));
|
|
||||||
colorsTransition.play();
|
|
||||||
});
|
|
||||||
colorSelector.translateXProperty()
|
|
||||||
.addListener((o, oldVal, newVal) -> updateHSLCircleColor((int) (newVal.intValue() + colorSelector.getPrefWidth() / 2),
|
|
||||||
(int) (colorSelector.getTranslateY() + colorSelector
|
|
||||||
.getPrefHeight() / 2)));
|
|
||||||
colorSelector.translateYProperty()
|
|
||||||
.addListener((o, oldVal, newVal) -> updateHSLCircleColor((int) (colorSelector.getTranslateX() + colorSelector
|
|
||||||
.getPrefWidth() / 2), (int) (newVal.intValue() + colorSelector.getPrefHeight() / 2)));
|
|
||||||
|
|
||||||
|
|
||||||
// Create SL Circle
|
|
||||||
slCircleView = new ImageView(getSLCricle(pickerSize, pickerSize));
|
|
||||||
slCircleView.setClip(new Circle(centerX, centerY, slRadius - 2));
|
|
||||||
slCircleView.setPickOnBounds(false);
|
|
||||||
this.getChildren().add(slCircleView);
|
|
||||||
|
|
||||||
// create SL Circle Selector
|
|
||||||
selector = new Pane();
|
|
||||||
Circle c1 = new Circle(selectorSize / 2);
|
|
||||||
Circle c2 = new Circle((selectorSize / 2) * 0.5);
|
|
||||||
selector.setShape(Path.subtract(c1, c2));
|
|
||||||
selector.setStyle(
|
|
||||||
"-fx-border-color:#424242; -fx-border-width:1px;-fx-background-color:rgba(255, 255, 255, 0.87);");
|
|
||||||
selector.setPrefSize(selectorSize, selectorSize);
|
|
||||||
selector.setMinSize(selectorSize, selectorSize);
|
|
||||||
selector.setMaxSize(selectorSize, selectorSize);
|
|
||||||
selector.setCache(true);
|
|
||||||
selector.setMouseTransparent(true);
|
|
||||||
this.getChildren().add(selector);
|
|
||||||
|
|
||||||
|
|
||||||
// add SL selection Listeners
|
|
||||||
slCircleView.addEventHandler(MouseEvent.MOUSE_DRAGGED, (event) -> {
|
|
||||||
if (selectorTransition != null) {
|
|
||||||
selectorTransition.stop();
|
|
||||||
}
|
|
||||||
if (Math.pow(event.getX() - centerX, 2) + Math.pow(event.getY() - centerY, 2) < Math.pow(slRadius - 2, 2)) {
|
|
||||||
selector.setTranslateX(event.getX() - selector.getPrefWidth() / 2);
|
|
||||||
selector.setTranslateY(event.getY() - selector.getPrefHeight() / 2);
|
|
||||||
} else {
|
|
||||||
double dx = event.getX() - centerX;
|
|
||||||
double dy = event.getY() - centerY;
|
|
||||||
double theta = Math.atan2(dy, dx);
|
|
||||||
double x = centerX + (slRadius - 2) * Math.cos(theta);
|
|
||||||
double y = centerY + (slRadius - 2) * Math.sin(theta);
|
|
||||||
selector.setTranslateX(x - selector.getPrefWidth() / 2);
|
|
||||||
selector.setTranslateY(y - selector.getPrefHeight() / 2);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
slCircleView.addEventHandler(MouseEvent.MOUSE_PRESSED, (event) -> {
|
|
||||||
selectorTransition = new CachedTransition(selector, new Timeline(new KeyFrame(Duration.millis(1000),
|
|
||||||
new KeyValue(selector.translateXProperty(),
|
|
||||||
event.getX() - selector.getPrefWidth() / 2,
|
|
||||||
Interpolator.EASE_BOTH),
|
|
||||||
new KeyValue(selector.translateYProperty(),
|
|
||||||
event.getY() - selector.getPrefHeight() / 2,
|
|
||||||
Interpolator.EASE_BOTH)))) {
|
|
||||||
{
|
|
||||||
setCycleDuration(Duration.millis(160));
|
|
||||||
setDelay(Duration.seconds(0));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
selectorTransition.play();
|
|
||||||
});
|
|
||||||
// add slCircleView listener
|
|
||||||
selector.translateXProperty()
|
|
||||||
.addListener((o, oldVal, newVal) -> setColorAtLocation(newVal.intValue() + selectorSize / 2,
|
|
||||||
(int) selector.getTranslateY() + selectorSize / 2));
|
|
||||||
selector.translateYProperty()
|
|
||||||
.addListener((o, oldVal, newVal) -> setColorAtLocation((int) selector.getTranslateX() + selectorSize / 2,
|
|
||||||
newVal.intValue() + selectorSize / 2));
|
|
||||||
|
|
||||||
|
|
||||||
// initial color selection
|
|
||||||
double dx = 20 - centerX;
|
|
||||||
double dy = 20 - centerY;
|
|
||||||
double theta = Math.atan2(dy, dx);
|
|
||||||
double x = centerX + huesRadius * Math.cos(theta);
|
|
||||||
double y = centerY + huesRadius * Math.sin(theta);
|
|
||||||
colorSelector.setRotate(90 + Math.toDegrees(Math.atan2(dy, dx)));
|
|
||||||
colorSelector.setTranslateX(x - colorSelector.getPrefWidth() / 2);
|
|
||||||
colorSelector.setTranslateY(y - colorSelector.getPrefHeight() / 2);
|
|
||||||
selector.setTranslateX(centerX - selector.getPrefWidth() / 2);
|
|
||||||
selector.setTranslateY(centerY - selector.getPrefHeight() / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List of Color Nodes that needs to be updated when picking a color
|
|
||||||
*/
|
|
||||||
private final ObservableList<Node> colorNodes = FXCollections.observableArrayList();
|
|
||||||
|
|
||||||
public void addColorSelectionNode(Node... nodes) {
|
|
||||||
colorNodes.addAll(nodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void removeColorSelectionNode(Node... nodes) {
|
|
||||||
colorNodes.removeAll(nodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateHSLCircleColor(int x, int y) {
|
|
||||||
// transform color to HSL space
|
|
||||||
Color color = huesCircleView.getImage().getPixelReader().getColor(x, y);
|
|
||||||
double max = Math.max(color.getRed(), Math.max(color.getGreen(), color.getBlue()));
|
|
||||||
double min = Math.min(color.getRed(), Math.min(color.getGreen(), color.getBlue()));
|
|
||||||
double hue = 0;
|
|
||||||
if (max != min) {
|
|
||||||
double d = max - min;
|
|
||||||
if (max == color.getRed()) {
|
|
||||||
hue = (color.getGreen() - color.getBlue()) / d + (color.getGreen() < color.getBlue() ? 6 : 0);
|
|
||||||
} else if (max == color.getGreen()) {
|
|
||||||
hue = (color.getBlue() - color.getRed()) / d + 2;
|
|
||||||
} else if (max == color.getBlue()) {
|
|
||||||
hue = (color.getRed() - color.getGreen()) / d + 4;
|
|
||||||
}
|
|
||||||
hue /= 6;
|
|
||||||
}
|
|
||||||
currentHue = map(hue, 0, 1, 0, 255);
|
|
||||||
|
|
||||||
// refresh the HSL circle
|
|
||||||
refreshHSLCircle();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void refreshHSLCircle() {
|
|
||||||
ColorAdjust colorAdjust = new ColorAdjust();
|
|
||||||
colorAdjust.setHue(map(currentHue + (currentHue < 127.5 ? 1 : -1) * 127.5, 0, 255, -1, 1));
|
|
||||||
slCircleView.setEffect(colorAdjust);
|
|
||||||
setColorAtLocation((int) selector.getTranslateX() + selectorSize / 2,
|
|
||||||
(int) selector.getTranslateY() + selectorSize / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* this method is used to move selectors to a certain color
|
|
||||||
*/
|
|
||||||
private boolean allowColorChange = true;
|
|
||||||
private ParallelTransition pTrans;
|
|
||||||
|
|
||||||
public void moveToColor(Color color) {
|
|
||||||
allowColorChange = false;
|
|
||||||
double max = Math.max(color.getRed(),
|
|
||||||
Math.max(color.getGreen(), color.getBlue())), min = Math.min(color.getRed(),
|
|
||||||
Math.min(color.getGreen(),
|
|
||||||
color.getBlue()));
|
|
||||||
double hue = 0;
|
|
||||||
double l = (max + min) / 2;
|
|
||||||
double s = 0;
|
|
||||||
if (max == min) {
|
|
||||||
hue = s = 0; // achromatic
|
|
||||||
} else {
|
|
||||||
double d = max - min;
|
|
||||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
||||||
if (max == color.getRed()) {
|
|
||||||
hue = (color.getGreen() - color.getBlue()) / d + (color.getGreen() < color.getBlue() ? 6 : 0);
|
|
||||||
} else if (max == color.getGreen()) {
|
|
||||||
hue = (color.getBlue() - color.getRed()) / d + 2;
|
|
||||||
} else if (max == color.getBlue()) {
|
|
||||||
hue = (color.getRed() - color.getGreen()) / d + 4;
|
|
||||||
}
|
|
||||||
hue /= 6;
|
|
||||||
}
|
|
||||||
currentHue = map(hue, 0, 1, 0, 255);
|
|
||||||
|
|
||||||
// Animate Hue
|
|
||||||
double theta = map(currentHue, 0, 255, -Math.PI, Math.PI);
|
|
||||||
double x = centerX + huesRadius * Math.cos(theta);
|
|
||||||
double y = centerY + huesRadius * Math.sin(theta);
|
|
||||||
colorsTransition = new CurveTransition(
|
|
||||||
new Point2D(
|
|
||||||
colorSelector.getTranslateX() + colorSelector.getPrefWidth() / 2,
|
|
||||||
colorSelector.getTranslateY() + colorSelector.getPrefHeight() / 2
|
|
||||||
),
|
|
||||||
new Point2D(x, y));
|
|
||||||
|
|
||||||
// Animate SL
|
|
||||||
s = map(s, 0, 1, 0, 255);
|
|
||||||
l = map(l, 0, 1, 0, 255);
|
|
||||||
Point2D point = getPointFromSL((int) s, (int) l, slRadius);
|
|
||||||
double pX = centerX - point.getX();
|
|
||||||
double pY = centerY - point.getY();
|
|
||||||
|
|
||||||
double endPointX;
|
|
||||||
double endPointY;
|
|
||||||
if (Math.pow(pX - centerX, 2) + Math.pow(pY - centerY, 2) < Math.pow(slRadius - 2, 2)) {
|
|
||||||
endPointX = pX - selector.getPrefWidth() / 2;
|
|
||||||
endPointY = pY - selector.getPrefHeight() / 2;
|
|
||||||
} else {
|
|
||||||
double dx = pX - centerX;
|
|
||||||
double dy = pY - centerY;
|
|
||||||
theta = Math.atan2(dy, dx);
|
|
||||||
x = centerX + (slRadius - 2) * Math.cos(theta);
|
|
||||||
y = centerY + (slRadius - 2) * Math.sin(theta);
|
|
||||||
endPointX = x - selector.getPrefWidth() / 2;
|
|
||||||
endPointY = y - selector.getPrefHeight() / 2;
|
|
||||||
}
|
|
||||||
selectorTransition = new CachedTransition(selector, new Timeline(new KeyFrame(Duration.millis(1000),
|
|
||||||
new KeyValue(selector.translateXProperty(),
|
|
||||||
endPointX,
|
|
||||||
Interpolator.EASE_BOTH),
|
|
||||||
new KeyValue(selector.translateYProperty(),
|
|
||||||
endPointY,
|
|
||||||
Interpolator.EASE_BOTH)))) {
|
|
||||||
{
|
|
||||||
setCycleDuration(Duration.millis(160));
|
|
||||||
setDelay(Duration.seconds(0));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (pTrans != null) {
|
|
||||||
pTrans.stop();
|
|
||||||
}
|
|
||||||
pTrans = new ParallelTransition(colorsTransition, selectorTransition);
|
|
||||||
pTrans.setOnFinished((finish) -> {
|
|
||||||
if (pTrans.getStatus() == Status.STOPPED) {
|
|
||||||
allowColorChange = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
pTrans.play();
|
|
||||||
|
|
||||||
refreshHSLCircle();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setColorAtLocation(int x, int y) {
|
|
||||||
if (allowColorChange) {
|
|
||||||
Color color = getColorAtLocation(x, y);
|
|
||||||
String colorString = "rgb(" + color.getRed() * 255 + "," + color.getGreen() * 255 + "," + color.getBlue() * 255 + ");";
|
|
||||||
for (Node node : colorNodes)
|
|
||||||
node.setStyle("-fx-background-color:" + colorString + "; -fx-fill:" + colorString + ";");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Color getColorAtLocation(double x, double y) {
|
|
||||||
double dy = x - centerX;
|
|
||||||
double dx = y - centerY;
|
|
||||||
return getColor(dx, dy);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Image getHuesCircle(int width, int height) {
|
|
||||||
WritableImage raster = new WritableImage(width, height);
|
|
||||||
PixelWriter pixelWriter = raster.getPixelWriter();
|
|
||||||
Point2D center = new Point2D((double) width / 2, (double) height / 2);
|
|
||||||
double rsmall = 0.8 * width / 2;
|
|
||||||
double rbig = (double) width / 2;
|
|
||||||
for (int y = 0; y < height; y++) {
|
|
||||||
for (int x = 0; x < width; x++) {
|
|
||||||
double dx = x - center.getX();
|
|
||||||
double dy = y - center.getY();
|
|
||||||
double distance = Math.sqrt((dx * dx) + (dy * dy));
|
|
||||||
double o = Math.atan2(dy, dx);
|
|
||||||
if (distance > rsmall && distance < rbig) {
|
|
||||||
double H = map(o, -Math.PI, Math.PI, 0, 255);
|
|
||||||
double S = 255;
|
|
||||||
double L = 152;
|
|
||||||
pixelWriter.setColor(x, y, HSL2RGB(H, S, L));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return raster;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Image getSLCricle(int width, int height) {
|
|
||||||
WritableImage raster = new WritableImage(width, height);
|
|
||||||
PixelWriter pixelWriter = raster.getPixelWriter();
|
|
||||||
Point2D center = new Point2D((double) width / 2, (double) height / 2);
|
|
||||||
for (int y = 0; y < height; y++) {
|
|
||||||
for (int x = 0; x < width; x++) {
|
|
||||||
double dy = x - center.getX();
|
|
||||||
double dx = y - center.getY();
|
|
||||||
pixelWriter.setColor(x, y, getColor(dx, dy));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return raster;
|
|
||||||
}
|
|
||||||
|
|
||||||
private double clamp(double from, double small, double big) {
|
|
||||||
return Math.min(Math.max(from, small), big);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Color getColor(double dx, double dy) {
|
|
||||||
double distance = Math.sqrt((dx * dx) + (dy * dy));
|
|
||||||
double rverysmall = 0.65 * ((double) pickerSize / 2);
|
|
||||||
Color pixelColor = Color.BLUE;
|
|
||||||
|
|
||||||
if (distance <= rverysmall * 1.1) {
|
|
||||||
double angle = -Math.PI / 2.;
|
|
||||||
double angle1 = angle + 2 * Math.PI / 3.;
|
|
||||||
double angle2 = angle1 + 2 * Math.PI / 3.;
|
|
||||||
double x1 = rverysmall * Math.sin(angle1);
|
|
||||||
double y1 = rverysmall * Math.cos(angle1);
|
|
||||||
double x2 = rverysmall * Math.sin(angle2);
|
|
||||||
double y2 = rverysmall * Math.cos(angle2);
|
|
||||||
dx += 0.01;
|
|
||||||
double[] circle = circleFrom3Points(new Point2D(x1, y1), new Point2D(x2, y2), new Point2D(dx, dy));
|
|
||||||
double xArc = circle[0];
|
|
||||||
double yArc = 0;
|
|
||||||
double arcR = circle[2];
|
|
||||||
double Arco = Math.atan2(dx - xArc, dy - yArc);
|
|
||||||
double Arco1 = Math.atan2(x1 - xArc, y1 - yArc);
|
|
||||||
double Arco2 = Math.atan2(x2 - xArc, y2 - yArc);
|
|
||||||
|
|
||||||
double finalX = xArc > 0 ? xArc - arcR : xArc + arcR;
|
|
||||||
|
|
||||||
double saturation = map(finalX, -rverysmall, rverysmall, 255, 0);
|
|
||||||
|
|
||||||
double lightness = 255;
|
|
||||||
double diffAngle = Arco2 - Arco1;
|
|
||||||
double diffArco = Arco - Arco1;
|
|
||||||
if (dx < x1) {
|
|
||||||
diffAngle = diffAngle < 0 ? 2 * Math.PI + diffAngle : diffAngle;
|
|
||||||
diffAngle = Math.abs(2 * Math.PI - diffAngle);
|
|
||||||
diffArco = diffArco < 0 ? 2 * Math.PI + diffArco : diffArco;
|
|
||||||
diffArco = Math.abs(2 * Math.PI - diffArco);
|
|
||||||
}
|
|
||||||
lightness = map(diffArco, 0, diffAngle, 0, 255);
|
|
||||||
|
|
||||||
|
|
||||||
if (distance > rverysmall) {
|
|
||||||
saturation = 255 - saturation;
|
|
||||||
if (lightness < 0 && dy < 0) {
|
|
||||||
lightness = 255;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lightness = clamp(lightness, 0, 255);
|
|
||||||
if ((saturation < 10 && dx < x1) || (saturation > 240 && dx > x1)) {
|
|
||||||
saturation = 255 - saturation;
|
|
||||||
}
|
|
||||||
saturation = clamp(saturation, 0, 255);
|
|
||||||
pixelColor = HSL2RGB(currentHue, saturation, lightness);
|
|
||||||
}
|
|
||||||
return pixelColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
/***************************************************************************
|
|
||||||
* *
|
|
||||||
* Hues Animation *
|
|
||||||
* *
|
|
||||||
**************************************************************************/
|
|
||||||
|
|
||||||
private final class CurveTransition extends Transition {
|
|
||||||
Point2D from;
|
|
||||||
double fromTheta;
|
|
||||||
double toTheta;
|
|
||||||
|
|
||||||
public CurveTransition(Point2D from, Point2D to) {
|
|
||||||
this.from = from;
|
|
||||||
double fromDx = from.getX() - centerX;
|
|
||||||
double fromDy = from.getY() - centerY;
|
|
||||||
fromTheta = Math.atan2(fromDy, fromDx);
|
|
||||||
double toDx = to.getX() - centerX;
|
|
||||||
double toDy = to.getY() - centerY;
|
|
||||||
toTheta = Math.atan2(toDy, toDx);
|
|
||||||
setInterpolator(Interpolator.EASE_BOTH);
|
|
||||||
setDelay(Duration.millis(0));
|
|
||||||
setCycleDuration(Duration.millis(240));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void interpolate(double frac) {
|
|
||||||
double dif = Math.min(Math.abs(toTheta - fromTheta), 2 * Math.PI - Math.abs(toTheta - fromTheta));
|
|
||||||
if (dif == 2 * Math.PI - Math.abs(toTheta - fromTheta)) {
|
|
||||||
int dir = -1;
|
|
||||||
if (toTheta < fromTheta) {
|
|
||||||
dir = 1;
|
|
||||||
}
|
|
||||||
dif = dir * dif;
|
|
||||||
} else {
|
|
||||||
dif = toTheta - fromTheta;
|
|
||||||
}
|
|
||||||
|
|
||||||
Point2D newP = rotate(from, new Point2D(centerX, centerY), frac * dif);
|
|
||||||
colorSelector.setRotate(90 + Math.toDegrees(Math.atan2(newP.getY() - centerY, newP.getX() - centerX)));
|
|
||||||
colorSelector.setTranslateX(newP.getX() - colorSelector.getPrefWidth() / 2);
|
|
||||||
colorSelector.setTranslateY(newP.getY() - colorSelector.getPrefHeight() / 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/***************************************************************************
|
|
||||||
* *
|
|
||||||
* Util methods *
|
|
||||||
* *
|
|
||||||
**************************************************************************/
|
|
||||||
|
|
||||||
private double map(double val, double min1, double max1, double min2, double max2) {
|
|
||||||
return min2 + (max2 - min2) * ((val - min1) / (max1 - min1));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Color HSL2RGB(double hue, double sat, double lum) {
|
|
||||||
hue = map(hue, 0, 255, 0, 359);
|
|
||||||
sat = map(sat, 0, 255, 0, 1);
|
|
||||||
lum = map(lum, 0, 255, 0, 1);
|
|
||||||
double v;
|
|
||||||
double red, green, blue;
|
|
||||||
double m;
|
|
||||||
double sv;
|
|
||||||
int sextant;
|
|
||||||
double fract, vsf, mid1, mid2;
|
|
||||||
|
|
||||||
red = lum; // default to gray
|
|
||||||
green = lum;
|
|
||||||
blue = lum;
|
|
||||||
v = (lum <= 0.5) ? (lum * (1.0 + sat)) : (lum + sat - lum * sat);
|
|
||||||
m = lum + lum - v;
|
|
||||||
sv = (v - m) / v;
|
|
||||||
hue /= 60.0; //get into range 0..6
|
|
||||||
sextant = (int) Math.floor(hue); // int32 rounds up or down.
|
|
||||||
fract = hue - sextant;
|
|
||||||
vsf = v * sv * fract;
|
|
||||||
mid1 = m + vsf;
|
|
||||||
mid2 = v - vsf;
|
|
||||||
|
|
||||||
if (v > 0) {
|
|
||||||
switch (sextant) {
|
|
||||||
case 0:
|
|
||||||
red = v;
|
|
||||||
green = mid1;
|
|
||||||
blue = m;
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
red = mid2;
|
|
||||||
green = v;
|
|
||||||
blue = m;
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
red = m;
|
|
||||||
green = v;
|
|
||||||
blue = mid1;
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
red = m;
|
|
||||||
green = mid2;
|
|
||||||
blue = v;
|
|
||||||
break;
|
|
||||||
case 4:
|
|
||||||
red = mid1;
|
|
||||||
green = m;
|
|
||||||
blue = v;
|
|
||||||
break;
|
|
||||||
case 5:
|
|
||||||
red = v;
|
|
||||||
green = m;
|
|
||||||
blue = mid2;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new Color(red, green, blue, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private double[] circleFrom3Points(Point2D a, Point2D b, Point2D c) {
|
|
||||||
double ax, ay, bx, by, cx, cy, x1, y11, dx1, dy1, x2, y2, dx2, dy2, ox, oy, dx, dy, radius; // Variables Used and to Declared
|
|
||||||
ax = a.getX();
|
|
||||||
ay = a.getY(); //first Point X and Y
|
|
||||||
bx = b.getX();
|
|
||||||
by = b.getY(); // Second Point X and Y
|
|
||||||
cx = c.getX();
|
|
||||||
cy = c.getY(); // Third Point X and Y
|
|
||||||
|
|
||||||
////****************Following are Basic Procedure**********************///
|
|
||||||
x1 = (bx + ax) / 2;
|
|
||||||
y11 = (by + ay) / 2;
|
|
||||||
dy1 = bx - ax;
|
|
||||||
dx1 = -(by - ay);
|
|
||||||
|
|
||||||
x2 = (cx + bx) / 2;
|
|
||||||
y2 = (cy + by) / 2;
|
|
||||||
dy2 = cx - bx;
|
|
||||||
dx2 = -(cy - by);
|
|
||||||
|
|
||||||
ox = (y11 * dx1 * dx2 + x2 * dx1 * dy2 - x1 * dy1 * dx2 - y2 * dx1 * dx2) / (dx1 * dy2 - dy1 * dx2);
|
|
||||||
oy = (ox - x1) * dy1 / dx1 + y11;
|
|
||||||
|
|
||||||
dx = ox - ax;
|
|
||||||
dy = oy - ay;
|
|
||||||
radius = Math.sqrt(dx * dx + dy * dy);
|
|
||||||
return new double[]{ox, oy, radius};
|
|
||||||
}
|
|
||||||
|
|
||||||
private Point2D getPointFromSL(int saturation, int lightness, double radius) {
|
|
||||||
double dy = map(saturation, 0, 255, -radius, radius);
|
|
||||||
double angle = 0.;
|
|
||||||
double angle1 = angle + 2 * Math.PI / 3.;
|
|
||||||
double angle2 = angle1 + 2 * Math.PI / 3.;
|
|
||||||
double x1 = radius * Math.sin(angle1);
|
|
||||||
double y1 = radius * Math.cos(angle1);
|
|
||||||
double x2 = radius * Math.sin(angle2);
|
|
||||||
double y2 = radius * Math.cos(angle2);
|
|
||||||
double dx = 0;
|
|
||||||
double[] circle = circleFrom3Points(new Point2D(x1, y1), new Point2D(dx, dy), new Point2D(x2, y2));
|
|
||||||
double xArc = circle[0];
|
|
||||||
double yArc = circle[1];
|
|
||||||
double arcR = circle[2];
|
|
||||||
double Arco1 = Math.atan2(x1 - xArc, y1 - yArc);
|
|
||||||
double Arco2 = Math.atan2(x2 - xArc, y2 - yArc);
|
|
||||||
double ArcoFinal = map(lightness, 0, 255, Arco2, Arco1);
|
|
||||||
double finalX = xArc + arcR * Math.sin(ArcoFinal);
|
|
||||||
double finalY = yArc + arcR * Math.cos(ArcoFinal);
|
|
||||||
if (dy < y1) {
|
|
||||||
ArcoFinal = map(lightness, 0, 255, Arco1, Arco2 + 2 * Math.PI);
|
|
||||||
finalX = -xArc - arcR * Math.sin(ArcoFinal);
|
|
||||||
finalY = yArc + arcR * Math.cos(ArcoFinal);
|
|
||||||
}
|
|
||||||
return new Point2D(finalX, finalY);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Point2D rotate(Point2D a, Point2D center, double angle) {
|
|
||||||
double resultX = center.getX() + (a.getX() - center.getX()) * Math.cos(angle) - (a.getY() - center.getY()) * Math
|
|
||||||
.sin(angle);
|
|
||||||
double resultY = center.getY() + (a.getX() - center.getX()) * Math.sin(angle) + (a.getY() - center.getY()) * Math
|
|
||||||
.cos(angle);
|
|
||||||
return new Point2D(resultX, resultY);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,314 +0,0 @@
|
|||||||
//
|
|
||||||
// Source code recreated from a .class file by IntelliJ IDEA
|
|
||||||
// (powered by Fernflower decompiler)
|
|
||||||
//
|
|
||||||
|
|
||||||
package com.jfoenix.skins;
|
|
||||||
|
|
||||||
import com.jfoenix.adapters.skins.ComboBoxListViewSkin;
|
|
||||||
import com.jfoenix.concurrency.JFXUtilities;
|
|
||||||
import com.jfoenix.controls.JFXComboBox;
|
|
||||||
import com.jfoenix.transitions.CachedTransition;
|
|
||||||
import javafx.animation.*;
|
|
||||||
import javafx.application.Platform;
|
|
||||||
import javafx.beans.binding.Bindings;
|
|
||||||
import javafx.beans.binding.BooleanBinding;
|
|
||||||
import javafx.beans.property.ObjectProperty;
|
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
|
||||||
import javafx.event.EventTarget;
|
|
||||||
import javafx.geometry.Insets;
|
|
||||||
import javafx.geometry.Pos;
|
|
||||||
import javafx.scene.Node;
|
|
||||||
import javafx.scene.control.ComboBoxBase;
|
|
||||||
import javafx.scene.control.ListView;
|
|
||||||
import javafx.scene.control.skin.VirtualFlow;
|
|
||||||
import javafx.scene.input.MouseEvent;
|
|
||||||
import javafx.scene.layout.*;
|
|
||||||
import javafx.scene.paint.Color;
|
|
||||||
import javafx.scene.paint.Paint;
|
|
||||||
import javafx.scene.text.Text;
|
|
||||||
import javafx.scene.transform.Scale;
|
|
||||||
import javafx.util.Duration;
|
|
||||||
import org.jackhuang.hmcl.ui.FXUtils;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class JFXComboBoxListViewSkin<T> extends ComboBoxListViewSkin<T> {
|
|
||||||
protected final ObjectProperty<Paint> promptTextFill;
|
|
||||||
private boolean invalid = true;
|
|
||||||
private final StackPane customPane;
|
|
||||||
private final StackPane line = new StackPane();
|
|
||||||
private final StackPane focusedLine = new StackPane();
|
|
||||||
private final Text promptText = new Text();
|
|
||||||
private final double initScale = 0.05;
|
|
||||||
private final Scale scale;
|
|
||||||
private final Timeline linesAnimation;
|
|
||||||
private ParallelTransition transition;
|
|
||||||
private CachedTransition promptTextUpTransition;
|
|
||||||
private CachedTransition promptTextDownTransition;
|
|
||||||
private CachedTransition promptTextColorTransition;
|
|
||||||
private final Scale promptTextScale;
|
|
||||||
private Paint oldPromptTextFill;
|
|
||||||
private final BooleanBinding usePromptText;
|
|
||||||
|
|
||||||
public JFXComboBoxListViewSkin(JFXComboBox<T> comboBox) {
|
|
||||||
super(comboBox);
|
|
||||||
this.scale = new Scale(this.initScale, 1.0F);
|
|
||||||
this.linesAnimation = new Timeline(new KeyFrame(Duration.ZERO, new KeyValue(this.scale.xProperty(), this.initScale, Interpolator.EASE_BOTH), new KeyValue(this.focusedLine.opacityProperty(), 0, Interpolator.EASE_BOTH)), new KeyFrame(Duration.millis(1.0F), new KeyValue(this.focusedLine.opacityProperty(), 1, Interpolator.EASE_BOTH)), new KeyFrame(Duration.millis(160.0F), new KeyValue(this.scale.xProperty(), 1, Interpolator.EASE_BOTH)));
|
|
||||||
this.promptTextScale = new Scale(1.0F, 1.0F, 0.0F, 0.0F);
|
|
||||||
this.promptTextFill = new SimpleObjectProperty(Color.valueOf("#B2B2B2"));
|
|
||||||
this.usePromptText = Bindings.createBooleanBinding(this::usePromptText, ((JFXComboBox) this.getSkinnable()).valueProperty(), this.getSkinnable().promptTextProperty());
|
|
||||||
this.getArrowButton().setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, null, null)));
|
|
||||||
this.promptText.textProperty().bind(comboBox.promptTextProperty());
|
|
||||||
this.promptText.fillProperty().bind(this.promptTextFill);
|
|
||||||
this.promptText.getStyleClass().addAll("text", "prompt-text");
|
|
||||||
this.promptText.getTransforms().add(this.promptTextScale);
|
|
||||||
if (!comboBox.isLabelFloat()) {
|
|
||||||
this.promptText.visibleProperty().bind(this.usePromptText);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.customPane = new StackPane();
|
|
||||||
this.customPane.setMouseTransparent(true);
|
|
||||||
this.customPane.getStyleClass().add("combo-box-button-container");
|
|
||||||
this.customPane.backgroundProperty().bindBidirectional(this.getSkinnable().backgroundProperty());
|
|
||||||
this.customPane.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, null, null)));
|
|
||||||
this.customPane.getChildren().add(this.promptText);
|
|
||||||
this.getChildren().add(0, this.customPane);
|
|
||||||
StackPane.setAlignment(this.promptText, Pos.CENTER_LEFT);
|
|
||||||
this.line.getStyleClass().add("input-line");
|
|
||||||
this.focusedLine.getStyleClass().add("input-focused-line");
|
|
||||||
this.getChildren().add(this.line);
|
|
||||||
this.getChildren().add(this.focusedLine);
|
|
||||||
this.line.setPrefHeight(1.0F);
|
|
||||||
this.line.setTranslateY(1.0F);
|
|
||||||
this.line.setManaged(false);
|
|
||||||
this.line.setBackground(new Background(new BackgroundFill(((JFXComboBox) this.getSkinnable()).getUnFocusColor(), CornerRadii.EMPTY, Insets.EMPTY)));
|
|
||||||
if (this.getSkinnable().isDisabled()) {
|
|
||||||
this.line.setBorder(new Border(new BorderStroke(((JFXComboBox) this.getSkinnable()).getUnFocusColor(), BorderStrokeStyle.DASHED, CornerRadii.EMPTY, new BorderWidths(1.0F))));
|
|
||||||
this.line.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY)));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.focusedLine.setPrefHeight(2.0F);
|
|
||||||
this.focusedLine.setTranslateY(0.0F);
|
|
||||||
this.focusedLine.setBackground(new Background(new BackgroundFill(((JFXComboBox) this.getSkinnable()).getFocusColor(), CornerRadii.EMPTY, Insets.EMPTY)));
|
|
||||||
this.focusedLine.setOpacity(0.0F);
|
|
||||||
this.focusedLine.getTransforms().add(this.scale);
|
|
||||||
this.focusedLine.setManaged(false);
|
|
||||||
if (comboBox.isEditable()) {
|
|
||||||
comboBox.getEditor().setStyle("-fx-background-color:TRANSPARENT;-fx-padding: 4 8 4 8");
|
|
||||||
comboBox.getEditor().promptTextProperty().unbind();
|
|
||||||
comboBox.getEditor().setPromptText(null);
|
|
||||||
comboBox.getEditor().textProperty().addListener((o, oldVal, newVal) -> {
|
|
||||||
this.usePromptText.invalidate();
|
|
||||||
comboBox.setValue(this.getConverter().fromString(newVal));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
comboBox.labelFloatProperty().addListener((o, oldVal, newVal) -> {
|
|
||||||
if (newVal) {
|
|
||||||
this.promptText.visibleProperty().unbind();
|
|
||||||
JFXUtilities.runInFX(() -> this.createFloatingAnimation());
|
|
||||||
} else {
|
|
||||||
this.promptText.visibleProperty().bind(this.usePromptText);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.createFocusTransition();
|
|
||||||
});
|
|
||||||
comboBox.focusColorProperty().addListener((o, oldVal, newVal) -> {
|
|
||||||
if (newVal != null) {
|
|
||||||
this.focusedLine.setBackground(new Background(new BackgroundFill(newVal, CornerRadii.EMPTY, Insets.EMPTY)));
|
|
||||||
if (((JFXComboBox) this.getSkinnable()).isLabelFloat()) {
|
|
||||||
this.promptTextColorTransition = new CachedTransition(this.customPane, new Timeline(new KeyFrame(Duration.millis(1300.0F), new KeyValue(this.promptTextFill, newVal, Interpolator.EASE_BOTH)))) {
|
|
||||||
{
|
|
||||||
this.setDelay(Duration.millis(0.0F));
|
|
||||||
this.setCycleDuration(Duration.millis(160.0F));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void starting() {
|
|
||||||
super.starting();
|
|
||||||
JFXComboBoxListViewSkin.this.oldPromptTextFill = JFXComboBoxListViewSkin.this.promptTextFill.get();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.transition = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
comboBox.unFocusColorProperty().addListener((o, oldVal, newVal) -> {
|
|
||||||
if (newVal != null) {
|
|
||||||
this.line.setBackground(new Background(new BackgroundFill(newVal, CornerRadii.EMPTY, Insets.EMPTY)));
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
comboBox.disabledProperty().addListener((o, oldVal, newVal) -> {
|
|
||||||
this.line.setBorder(newVal ? new Border(new BorderStroke(((JFXComboBox) this.getSkinnable()).getUnFocusColor(), BorderStrokeStyle.DASHED, CornerRadii.EMPTY, new BorderWidths(this.line.getHeight()))) : Border.EMPTY);
|
|
||||||
this.line.setBackground(new Background(new BackgroundFill(newVal ? Color.TRANSPARENT : ((JFXComboBox) this.getSkinnable()).getUnFocusColor(), CornerRadii.EMPTY, Insets.EMPTY)));
|
|
||||||
});
|
|
||||||
comboBox.focusedProperty().addListener((o, oldVal, newVal) -> {
|
|
||||||
if (newVal) {
|
|
||||||
this.focus();
|
|
||||||
} else {
|
|
||||||
this.unFocus();
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
comboBox.valueProperty().addListener((o, oldVal, newVal) -> {
|
|
||||||
if (((JFXComboBox) this.getSkinnable()).isLabelFloat()) {
|
|
||||||
this.animateFloatingLabel(newVal != null && !newVal.toString().isEmpty());
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
for (Node child : getChildren()) {
|
|
||||||
if (child instanceof ListView<?> listView && "list-view".equals(listView.getId())) {
|
|
||||||
// The default hideOnClick logic of ComboBoxListViewSkin doesn't work well with FloatScrollBarSkin.
|
|
||||||
// We implement hideOnClick ourselves to prevent the popup from
|
|
||||||
// hiding when releasing the mouse on the scrollbar.
|
|
||||||
|
|
||||||
this.setHideOnClick(false);
|
|
||||||
listView.addEventFilter(MouseEvent.MOUSE_RELEASED, t -> {
|
|
||||||
EventTarget target = t.getTarget();
|
|
||||||
if (target instanceof Node targetNode) {
|
|
||||||
List<String> s = targetNode.getStyleClass();
|
|
||||||
if (s.contains("thumb")
|
|
||||||
|| s.contains("track")
|
|
||||||
|| s.contains("decrement-arrow")
|
|
||||||
|| s.contains("increment-arrow")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
comboBox.hide();
|
|
||||||
});
|
|
||||||
Platform.runLater(() -> {
|
|
||||||
if (listView.lookup(".virtual-flow") instanceof VirtualFlow<?> virtualFlow) {
|
|
||||||
FXUtils.smoothScrolling(virtualFlow);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void layoutChildren(double x, double y, double w, double h) {
|
|
||||||
super.layoutChildren(x, y, w, h);
|
|
||||||
this.customPane.resizeRelocate(x, y, w, h);
|
|
||||||
if (this.invalid) {
|
|
||||||
this.invalid = false;
|
|
||||||
if (!this.getSkinnable().isEditable()) {
|
|
||||||
Text javaPromptText = (Text) super.getDisplayNode().lookup(".text");
|
|
||||||
if (javaPromptText != null) {
|
|
||||||
this.promptTextFill.set(javaPromptText.getFill());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.createFloatingAnimation();
|
|
||||||
if (((ComboBoxBase) this.getSkinnable()).getValue() != null) {
|
|
||||||
this.animateFloatingLabel(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.focusedLine.resizeRelocate(x, this.getSkinnable().getHeight(), w, this.focusedLine.prefHeight(-1.0F));
|
|
||||||
this.line.resizeRelocate(x, this.getSkinnable().getHeight(), w, this.line.prefHeight(-1.0F));
|
|
||||||
this.scale.setPivotX(w / (double) 2.0F);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createFloatingAnimation() {
|
|
||||||
this.promptTextUpTransition = new CachedTransition(this.customPane, new Timeline(new KeyFrame(Duration.millis(1300.0F), new KeyValue(this.promptText.translateYProperty(), -this.customPane.getHeight() + 6.05, Interpolator.EASE_BOTH), new KeyValue(this.promptTextScale.xProperty(), 0.85, Interpolator.EASE_BOTH), new KeyValue(this.promptTextScale.yProperty(), 0.85, Interpolator.EASE_BOTH)))) {
|
|
||||||
{
|
|
||||||
this.setDelay(Duration.millis(0.0F));
|
|
||||||
this.setCycleDuration(Duration.millis(240.0F));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.promptTextColorTransition = new CachedTransition(this.customPane, new Timeline(new KeyFrame(Duration.millis(1300.0F), new KeyValue(this.promptTextFill, ((JFXComboBox) this.getSkinnable()).getFocusColor(), Interpolator.EASE_BOTH)))) {
|
|
||||||
{
|
|
||||||
this.setDelay(Duration.millis(0.0F));
|
|
||||||
this.setCycleDuration(Duration.millis(160.0F));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void starting() {
|
|
||||||
super.starting();
|
|
||||||
JFXComboBoxListViewSkin.this.oldPromptTextFill = JFXComboBoxListViewSkin.this.promptTextFill.get();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.promptTextDownTransition = new CachedTransition(this.customPane, new Timeline(new KeyFrame(Duration.millis(1300.0F), new KeyValue(this.promptText.translateYProperty(), 0, Interpolator.EASE_BOTH), new KeyValue(this.promptTextScale.xProperty(), 1, Interpolator.EASE_BOTH), new KeyValue(this.promptTextScale.yProperty(), 1, Interpolator.EASE_BOTH)))) {
|
|
||||||
{
|
|
||||||
this.setDelay(Duration.millis(0.0F));
|
|
||||||
this.setCycleDuration(Duration.millis(240.0F));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.promptTextDownTransition.setOnFinished((finish) -> {
|
|
||||||
this.promptText.setTranslateY(0.0F);
|
|
||||||
this.promptTextScale.setX(1.0F);
|
|
||||||
this.promptTextScale.setY(1.0F);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void focus() {
|
|
||||||
if (this.transition == null) {
|
|
||||||
this.createFocusTransition();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.transition.play();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void animateFloatingLabel(boolean up) {
|
|
||||||
if (this.promptText == null) {
|
|
||||||
Platform.runLater(() -> this.animateFloatingLabel(up));
|
|
||||||
} else {
|
|
||||||
if (this.transition != null) {
|
|
||||||
this.transition.stop();
|
|
||||||
this.transition.getChildren().remove(this.promptTextUpTransition);
|
|
||||||
this.transition.getChildren().remove(this.promptTextColorTransition);
|
|
||||||
this.transition = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (up && this.promptText.getTranslateY() == (double) 0.0F) {
|
|
||||||
this.promptTextDownTransition.stop();
|
|
||||||
this.promptTextUpTransition.play();
|
|
||||||
if (this.getSkinnable().isFocused()) {
|
|
||||||
this.promptTextColorTransition.play();
|
|
||||||
}
|
|
||||||
} else if (!up) {
|
|
||||||
this.promptTextUpTransition.stop();
|
|
||||||
if (this.getSkinnable().isFocused()) {
|
|
||||||
this.promptTextFill.set(this.oldPromptTextFill);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.promptTextDownTransition.play();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createFocusTransition() {
|
|
||||||
this.transition = new ParallelTransition();
|
|
||||||
if (((JFXComboBox) this.getSkinnable()).isLabelFloat()) {
|
|
||||||
this.transition.getChildren().add(this.promptTextUpTransition);
|
|
||||||
this.transition.getChildren().add(this.promptTextColorTransition);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.transition.getChildren().add(this.linesAnimation);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void unFocus() {
|
|
||||||
if (this.transition != null) {
|
|
||||||
this.transition.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.scale.setX(this.initScale);
|
|
||||||
this.focusedLine.setOpacity(0.0F);
|
|
||||||
if (((JFXComboBox) this.getSkinnable()).isLabelFloat() && this.oldPromptTextFill != null) {
|
|
||||||
this.promptTextFill.set(this.oldPromptTextFill);
|
|
||||||
if (this.usePromptText()) {
|
|
||||||
this.promptTextDownTransition.play();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean usePromptText() {
|
|
||||||
Object txt = ((JFXComboBox) this.getSkinnable()).getValue();
|
|
||||||
String promptTxt = this.getSkinnable().getPromptText();
|
|
||||||
return (txt == null || txt.toString().isEmpty()) && promptTxt != null && !promptTxt.isEmpty() && !this.promptTextFill.get().equals(Color.TRANSPARENT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,520 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.jfoenix.skins;
|
|
||||||
|
|
||||||
import com.jfoenix.effects.JFXDepthManager;
|
|
||||||
import com.jfoenix.transitions.CachedTransition;
|
|
||||||
import javafx.animation.KeyFrame;
|
|
||||||
import javafx.animation.KeyValue;
|
|
||||||
import javafx.animation.Timeline;
|
|
||||||
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.geometry.Point2D;
|
|
||||||
import javafx.scene.Node;
|
|
||||||
import javafx.scene.effect.DropShadow;
|
|
||||||
import javafx.scene.input.MouseEvent;
|
|
||||||
import javafx.scene.layout.Pane;
|
|
||||||
import javafx.scene.paint.Color;
|
|
||||||
import javafx.scene.shape.*;
|
|
||||||
import javafx.scene.transform.Rotate;
|
|
||||||
import javafx.util.Duration;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
|
|
||||||
import static javafx.animation.Interpolator.EASE_BOTH;
|
|
||||||
|
|
||||||
/// @author Shadi Shaheen
|
|
||||||
final class JFXCustomColorPicker extends Pane {
|
|
||||||
|
|
||||||
ObjectProperty<RecentColorPath> selectedPath = new SimpleObjectProperty<>();
|
|
||||||
private MoveTo startPoint;
|
|
||||||
private CubicCurveTo curve0To;
|
|
||||||
private CubicCurveTo outerCircleCurveTo;
|
|
||||||
private CubicCurveTo curve1To;
|
|
||||||
private CubicCurveTo innerCircleCurveTo;
|
|
||||||
private final ArrayList<CubicCurve> curves = new ArrayList<>();
|
|
||||||
|
|
||||||
private final double distance = 200;
|
|
||||||
private final double centerX = distance;
|
|
||||||
private final double centerY = distance;
|
|
||||||
private final double radius = 110;
|
|
||||||
|
|
||||||
private static final int shapesNumber = 13;
|
|
||||||
private final ArrayList<RecentColorPath> shapes = new ArrayList<>();
|
|
||||||
private CachedTransition showAnimation;
|
|
||||||
private final JFXColorPickerUI hslColorPicker;
|
|
||||||
|
|
||||||
public JFXCustomColorPicker() {
|
|
||||||
this.setPickOnBounds(false);
|
|
||||||
this.setMinSize(distance * 2, distance * 2);
|
|
||||||
|
|
||||||
final DoubleProperty rotationAngle = new SimpleDoubleProperty(2.1);
|
|
||||||
|
|
||||||
// draw recent colors shape using cubic curves
|
|
||||||
init(rotationAngle, centerX + 53, centerY + 162);
|
|
||||||
|
|
||||||
hslColorPicker = new JFXColorPickerUI((int) distance);
|
|
||||||
hslColorPicker.setLayoutX(centerX - distance / 2);
|
|
||||||
hslColorPicker.setLayoutY(centerY - distance / 2);
|
|
||||||
this.getChildren().add(hslColorPicker);
|
|
||||||
// add recent colors shapes
|
|
||||||
final int shapesStartIndex = this.getChildren().size();
|
|
||||||
final int shapesEndIndex = shapesStartIndex + shapesNumber;
|
|
||||||
for (int i = 0; i < shapesNumber; i++) {
|
|
||||||
final double angle = 2 * i * Math.PI / shapesNumber;
|
|
||||||
final RecentColorPath path = new RecentColorPath(startPoint,
|
|
||||||
curve0To,
|
|
||||||
outerCircleCurveTo,
|
|
||||||
curve1To,
|
|
||||||
innerCircleCurveTo);
|
|
||||||
shapes.add(path);
|
|
||||||
path.setPickOnBounds(false);
|
|
||||||
final Rotate rotate = new Rotate(Math.toDegrees(angle), centerX, centerY);
|
|
||||||
path.getTransforms().add(rotate);
|
|
||||||
this.getChildren().add(shapesStartIndex, path);
|
|
||||||
path.setFill(Color.valueOf(getDefaultColor(i)));
|
|
||||||
path.setFocusTraversable(true);
|
|
||||||
path.addEventHandler(MouseEvent.MOUSE_CLICKED, (event) -> {
|
|
||||||
path.requestFocus();
|
|
||||||
selectedPath.set(path);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// add selection listeners
|
|
||||||
selectedPath.addListener((o, oldVal, newVal) -> {
|
|
||||||
if (oldVal != null) {
|
|
||||||
hslColorPicker.removeColorSelectionNode(oldVal);
|
|
||||||
oldVal.playTransition(-1);
|
|
||||||
}
|
|
||||||
// re-arrange children
|
|
||||||
while (this.getChildren().indexOf(newVal) != shapesEndIndex - 1) {
|
|
||||||
final Node temp = this.getChildren().get(shapesEndIndex - 1);
|
|
||||||
this.getChildren().remove(shapesEndIndex - 1);
|
|
||||||
this.getChildren().add(shapesStartIndex, temp);
|
|
||||||
}
|
|
||||||
// update path fill according to the color picker
|
|
||||||
newVal.setStroke(Color.rgb(255, 255, 255, 0.87));
|
|
||||||
newVal.playTransition(1);
|
|
||||||
hslColorPicker.moveToColor((Color) newVal.getFill());
|
|
||||||
hslColorPicker.addColorSelectionNode(newVal);
|
|
||||||
});
|
|
||||||
// init selection
|
|
||||||
selectedPath.set((RecentColorPath) this.getChildren().get(shapesStartIndex));
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getShapesNumber() {
|
|
||||||
return shapesNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getSelectedIndex() {
|
|
||||||
if (selectedPath.get() != null) {
|
|
||||||
return shapes.indexOf(selectedPath.get());
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setColor(final Color color) {
|
|
||||||
shapes.get(getSelectedIndex()).setFill(color);
|
|
||||||
hslColorPicker.moveToColor(color);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Color getColor(final int index) {
|
|
||||||
if (index >= 0 && index < shapes.size()) {
|
|
||||||
return (Color) shapes.get(index).getFill();
|
|
||||||
} else {
|
|
||||||
return Color.WHITE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void preAnimate() {
|
|
||||||
final CubicCurve firstCurve = curves.get(0);
|
|
||||||
final double x = firstCurve.getStartX();
|
|
||||||
final double y = firstCurve.getStartY();
|
|
||||||
firstCurve.setStartX(centerX);
|
|
||||||
firstCurve.setStartY(centerY);
|
|
||||||
|
|
||||||
final CubicCurve secondCurve = curves.get(1);
|
|
||||||
final double x1 = secondCurve.getStartX();
|
|
||||||
final double y1 = secondCurve.getStartY();
|
|
||||||
secondCurve.setStartX(centerX);
|
|
||||||
secondCurve.setStartY(centerY);
|
|
||||||
|
|
||||||
final double cx1 = firstCurve.getControlX1();
|
|
||||||
final double cy1 = firstCurve.getControlY1();
|
|
||||||
firstCurve.setControlX1(centerX + radius);
|
|
||||||
firstCurve.setControlY1(centerY + radius / 2);
|
|
||||||
|
|
||||||
final KeyFrame keyFrame = new KeyFrame(Duration.millis(1000),
|
|
||||||
new KeyValue(firstCurve.startXProperty(), x, EASE_BOTH),
|
|
||||||
new KeyValue(firstCurve.startYProperty(), y, EASE_BOTH),
|
|
||||||
new KeyValue(secondCurve.startXProperty(), x1, EASE_BOTH),
|
|
||||||
new KeyValue(secondCurve.startYProperty(), y1, EASE_BOTH),
|
|
||||||
new KeyValue(firstCurve.controlX1Property(), cx1, EASE_BOTH),
|
|
||||||
new KeyValue(firstCurve.controlY1Property(), cy1, EASE_BOTH)
|
|
||||||
);
|
|
||||||
final Timeline timeline = new Timeline(keyFrame);
|
|
||||||
showAnimation = new CachedTransition(this, timeline) {
|
|
||||||
{
|
|
||||||
setCycleDuration(Duration.millis(240));
|
|
||||||
setDelay(Duration.millis(0));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public void animate() {
|
|
||||||
showAnimation.play();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void init(final DoubleProperty rotationAngle, final double initControlX1, final double initControlY1) {
|
|
||||||
|
|
||||||
final Circle innerCircle = new Circle(centerX, centerY, radius, Color.TRANSPARENT);
|
|
||||||
final Circle outerCircle = new Circle(centerX, centerY, radius * 2, Color.web("blue", 0.5));
|
|
||||||
|
|
||||||
// Create a composite shape of 4 cubic curves
|
|
||||||
// create 2 cubic curves of the shape
|
|
||||||
createQuadraticCurve(rotationAngle, initControlX1, initControlY1);
|
|
||||||
|
|
||||||
// inner circle curve
|
|
||||||
final CubicCurve innerCircleCurve = new CubicCurve();
|
|
||||||
innerCircleCurve.startXProperty().bind(curves.get(0).startXProperty());
|
|
||||||
innerCircleCurve.startYProperty().bind(curves.get(0).startYProperty());
|
|
||||||
innerCircleCurve.endXProperty().bind(curves.get(1).startXProperty());
|
|
||||||
innerCircleCurve.endYProperty().bind(curves.get(1).startYProperty());
|
|
||||||
curves.get(0).startXProperty().addListener((o, oldVal, newVal) -> {
|
|
||||||
final Point2D controlPoint = makeControlPoint(newVal.doubleValue(),
|
|
||||||
curves.get(0).getStartY(),
|
|
||||||
innerCircle,
|
|
||||||
shapesNumber,
|
|
||||||
-1);
|
|
||||||
innerCircleCurve.setControlX1(controlPoint.getX());
|
|
||||||
innerCircleCurve.setControlY1(controlPoint.getY());
|
|
||||||
});
|
|
||||||
curves.get(0).startYProperty().addListener((o, oldVal, newVal) -> {
|
|
||||||
final Point2D controlPoint = makeControlPoint(curves.get(0).getStartX(),
|
|
||||||
newVal.doubleValue(),
|
|
||||||
innerCircle,
|
|
||||||
shapesNumber,
|
|
||||||
-1);
|
|
||||||
innerCircleCurve.setControlX1(controlPoint.getX());
|
|
||||||
innerCircleCurve.setControlY1(controlPoint.getY());
|
|
||||||
});
|
|
||||||
curves.get(1).startXProperty().addListener((o, oldVal, newVal) -> {
|
|
||||||
final Point2D controlPoint = makeControlPoint(newVal.doubleValue(),
|
|
||||||
curves.get(1).getStartY(),
|
|
||||||
innerCircle,
|
|
||||||
shapesNumber,
|
|
||||||
1);
|
|
||||||
innerCircleCurve.setControlX2(controlPoint.getX());
|
|
||||||
innerCircleCurve.setControlY2(controlPoint.getY());
|
|
||||||
});
|
|
||||||
curves.get(1).startYProperty().addListener((o, oldVal, newVal) -> {
|
|
||||||
final Point2D controlPoint = makeControlPoint(curves.get(1).getStartX(),
|
|
||||||
newVal.doubleValue(),
|
|
||||||
innerCircle,
|
|
||||||
shapesNumber,
|
|
||||||
1);
|
|
||||||
innerCircleCurve.setControlX2(controlPoint.getX());
|
|
||||||
innerCircleCurve.setControlY2(controlPoint.getY());
|
|
||||||
});
|
|
||||||
Point2D controlPoint = makeControlPoint(curves.get(0).getStartX(),
|
|
||||||
curves.get(0).getStartY(),
|
|
||||||
innerCircle,
|
|
||||||
shapesNumber,
|
|
||||||
-1);
|
|
||||||
innerCircleCurve.setControlX1(controlPoint.getX());
|
|
||||||
innerCircleCurve.setControlY1(controlPoint.getY());
|
|
||||||
controlPoint = makeControlPoint(curves.get(1).getStartX(),
|
|
||||||
curves.get(1).getStartY(),
|
|
||||||
innerCircle,
|
|
||||||
shapesNumber,
|
|
||||||
1);
|
|
||||||
innerCircleCurve.setControlX2(controlPoint.getX());
|
|
||||||
innerCircleCurve.setControlY2(controlPoint.getY());
|
|
||||||
|
|
||||||
// outer circle curve
|
|
||||||
final CubicCurve outerCircleCurve = new CubicCurve();
|
|
||||||
outerCircleCurve.startXProperty().bind(curves.get(0).endXProperty());
|
|
||||||
outerCircleCurve.startYProperty().bind(curves.get(0).endYProperty());
|
|
||||||
outerCircleCurve.endXProperty().bind(curves.get(1).endXProperty());
|
|
||||||
outerCircleCurve.endYProperty().bind(curves.get(1).endYProperty());
|
|
||||||
controlPoint = makeControlPoint(curves.get(0).getEndX(),
|
|
||||||
curves.get(0).getEndY(),
|
|
||||||
outerCircle,
|
|
||||||
shapesNumber,
|
|
||||||
-1);
|
|
||||||
outerCircleCurve.setControlX1(controlPoint.getX());
|
|
||||||
outerCircleCurve.setControlY1(controlPoint.getY());
|
|
||||||
controlPoint = makeControlPoint(curves.get(1).getEndX(), curves.get(1).getEndY(), outerCircle, shapesNumber, 1);
|
|
||||||
outerCircleCurve.setControlX2(controlPoint.getX());
|
|
||||||
outerCircleCurve.setControlY2(controlPoint.getY());
|
|
||||||
|
|
||||||
startPoint = new MoveTo();
|
|
||||||
startPoint.xProperty().bind(curves.get(0).startXProperty());
|
|
||||||
startPoint.yProperty().bind(curves.get(0).startYProperty());
|
|
||||||
|
|
||||||
curve0To = new CubicCurveTo();
|
|
||||||
curve0To.controlX1Property().bind(curves.get(0).controlX1Property());
|
|
||||||
curve0To.controlY1Property().bind(curves.get(0).controlY1Property());
|
|
||||||
curve0To.controlX2Property().bind(curves.get(0).controlX2Property());
|
|
||||||
curve0To.controlY2Property().bind(curves.get(0).controlY2Property());
|
|
||||||
curve0To.xProperty().bind(curves.get(0).endXProperty());
|
|
||||||
curve0To.yProperty().bind(curves.get(0).endYProperty());
|
|
||||||
|
|
||||||
outerCircleCurveTo = new CubicCurveTo();
|
|
||||||
outerCircleCurveTo.controlX1Property().bind(outerCircleCurve.controlX1Property());
|
|
||||||
outerCircleCurveTo.controlY1Property().bind(outerCircleCurve.controlY1Property());
|
|
||||||
outerCircleCurveTo.controlX2Property().bind(outerCircleCurve.controlX2Property());
|
|
||||||
outerCircleCurveTo.controlY2Property().bind(outerCircleCurve.controlY2Property());
|
|
||||||
outerCircleCurveTo.xProperty().bind(outerCircleCurve.endXProperty());
|
|
||||||
outerCircleCurveTo.yProperty().bind(outerCircleCurve.endYProperty());
|
|
||||||
|
|
||||||
curve1To = new CubicCurveTo();
|
|
||||||
curve1To.controlX1Property().bind(curves.get(1).controlX2Property());
|
|
||||||
curve1To.controlY1Property().bind(curves.get(1).controlY2Property());
|
|
||||||
curve1To.controlX2Property().bind(curves.get(1).controlX1Property());
|
|
||||||
curve1To.controlY2Property().bind(curves.get(1).controlY1Property());
|
|
||||||
curve1To.xProperty().bind(curves.get(1).startXProperty());
|
|
||||||
curve1To.yProperty().bind(curves.get(1).startYProperty());
|
|
||||||
|
|
||||||
innerCircleCurveTo = new CubicCurveTo();
|
|
||||||
innerCircleCurveTo.controlX1Property().bind(innerCircleCurve.controlX2Property());
|
|
||||||
innerCircleCurveTo.controlY1Property().bind(innerCircleCurve.controlY2Property());
|
|
||||||
innerCircleCurveTo.controlX2Property().bind(innerCircleCurve.controlX1Property());
|
|
||||||
innerCircleCurveTo.controlY2Property().bind(innerCircleCurve.controlY1Property());
|
|
||||||
innerCircleCurveTo.xProperty().bind(innerCircleCurve.startXProperty());
|
|
||||||
innerCircleCurveTo.yProperty().bind(innerCircleCurve.startYProperty());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createQuadraticCurve(final DoubleProperty rotationAngle, final double initControlX1, final double initControlY1) {
|
|
||||||
for (int i = 0; i < 2; i++) {
|
|
||||||
|
|
||||||
double angle = 2 * i * Math.PI / shapesNumber;
|
|
||||||
double xOffset = radius * Math.cos(angle);
|
|
||||||
double yOffset = radius * Math.sin(angle);
|
|
||||||
final double startx = centerX + xOffset;
|
|
||||||
final double starty = centerY + yOffset;
|
|
||||||
|
|
||||||
final double diffStartCenterX = startx - centerX;
|
|
||||||
final double diffStartCenterY = starty - centerY;
|
|
||||||
final double sinRotAngle = Math.sin(rotationAngle.get());
|
|
||||||
final double cosRotAngle = Math.cos(rotationAngle.get());
|
|
||||||
final double startXR = cosRotAngle * diffStartCenterX - sinRotAngle * diffStartCenterY + centerX;
|
|
||||||
final double startYR = sinRotAngle * diffStartCenterX + cosRotAngle * diffStartCenterY + centerY;
|
|
||||||
|
|
||||||
angle = 2 * i * Math.PI / shapesNumber;
|
|
||||||
xOffset = distance * Math.cos(angle);
|
|
||||||
yOffset = distance * Math.sin(angle);
|
|
||||||
|
|
||||||
final double endx = centerX + xOffset;
|
|
||||||
final double endy = centerY + yOffset;
|
|
||||||
|
|
||||||
final CubicCurve curvedLine = new CubicCurve();
|
|
||||||
curvedLine.setStartX(startXR);
|
|
||||||
curvedLine.setStartY(startYR);
|
|
||||||
curvedLine.setControlX1(startXR);
|
|
||||||
curvedLine.setControlY1(startYR);
|
|
||||||
curvedLine.setControlX2(endx);
|
|
||||||
curvedLine.setControlY2(endy);
|
|
||||||
curvedLine.setEndX(endx);
|
|
||||||
curvedLine.setEndY(endy);
|
|
||||||
curvedLine.setStroke(Color.FORESTGREEN);
|
|
||||||
curvedLine.setStrokeWidth(1);
|
|
||||||
curvedLine.setStrokeLineCap(StrokeLineCap.ROUND);
|
|
||||||
curvedLine.setFill(Color.TRANSPARENT);
|
|
||||||
curvedLine.setMouseTransparent(true);
|
|
||||||
rotationAngle.addListener((o, oldVal, newVal) -> {
|
|
||||||
final double newstartXR = ((cosRotAngle * diffStartCenterX) - (sinRotAngle * diffStartCenterY)) + centerX;
|
|
||||||
final double newstartYR = (sinRotAngle * diffStartCenterX) + (cosRotAngle * diffStartCenterY) + centerY;
|
|
||||||
curvedLine.setStartX(newstartXR);
|
|
||||||
curvedLine.setStartY(newstartYR);
|
|
||||||
});
|
|
||||||
|
|
||||||
curves.add(curvedLine);
|
|
||||||
|
|
||||||
if (i == 0) {
|
|
||||||
curvedLine.setControlX1(initControlX1);
|
|
||||||
curvedLine.setControlY1(initControlY1);
|
|
||||||
} else {
|
|
||||||
final CubicCurve firstCurve = curves.get(0);
|
|
||||||
final double curveTheta = 2 * curves.indexOf(curvedLine) * Math.PI / shapesNumber;
|
|
||||||
|
|
||||||
curvedLine.controlX1Property().bind(Bindings.createDoubleBinding(() -> {
|
|
||||||
final double a = firstCurve.getControlX1() - centerX;
|
|
||||||
final double b = Math.sin(curveTheta) * (firstCurve.getControlY1() - centerY);
|
|
||||||
return ((Math.cos(curveTheta) * a) - b) + centerX;
|
|
||||||
}, firstCurve.controlX1Property(), firstCurve.controlY1Property()));
|
|
||||||
|
|
||||||
curvedLine.controlY1Property().bind(Bindings.createDoubleBinding(() -> {
|
|
||||||
final double a = Math.sin(curveTheta) * (firstCurve.getControlX1() - centerX);
|
|
||||||
final double b = Math.cos(curveTheta) * (firstCurve.getControlY1() - centerY);
|
|
||||||
return a + b + centerY;
|
|
||||||
}, firstCurve.controlX1Property(), firstCurve.controlY1Property()));
|
|
||||||
|
|
||||||
|
|
||||||
curvedLine.controlX2Property().bind(Bindings.createDoubleBinding(() -> {
|
|
||||||
final double a = firstCurve.getControlX2() - centerX;
|
|
||||||
final double b = firstCurve.getControlY2() - centerY;
|
|
||||||
return ((Math.cos(curveTheta) * a) - (Math.sin(curveTheta) * b)) + centerX;
|
|
||||||
}, firstCurve.controlX2Property(), firstCurve.controlY2Property()));
|
|
||||||
|
|
||||||
curvedLine.controlY2Property().bind(Bindings.createDoubleBinding(() -> {
|
|
||||||
final double a = Math.sin(curveTheta) * (firstCurve.getControlX2() - centerX);
|
|
||||||
final double b = Math.cos(curveTheta) * (firstCurve.getControlY2() - centerY);
|
|
||||||
return a + b + centerY;
|
|
||||||
}, firstCurve.controlX2Property(), firstCurve.controlY2Property()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getDefaultColor(final int i) {
|
|
||||||
String color = "#FFFFFF";
|
|
||||||
switch (i) {
|
|
||||||
case 0:
|
|
||||||
color = "#8F3F7E";
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
color = "#B5305F";
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
color = "#CE584A";
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
color = "#DB8D5C";
|
|
||||||
break;
|
|
||||||
case 4:
|
|
||||||
color = "#DA854E";
|
|
||||||
break;
|
|
||||||
case 5:
|
|
||||||
color = "#E9AB44";
|
|
||||||
break;
|
|
||||||
case 6:
|
|
||||||
color = "#FEE435";
|
|
||||||
break;
|
|
||||||
case 7:
|
|
||||||
color = "#99C286";
|
|
||||||
break;
|
|
||||||
case 8:
|
|
||||||
color = "#01A05E";
|
|
||||||
break;
|
|
||||||
case 9:
|
|
||||||
color = "#4A8895";
|
|
||||||
break;
|
|
||||||
case 10:
|
|
||||||
color = "#16669B";
|
|
||||||
break;
|
|
||||||
case 11:
|
|
||||||
color = "#2F65A5";
|
|
||||||
break;
|
|
||||||
case 12:
|
|
||||||
color = "#4E6A9C";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return color;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Point2D rotate(final Point2D a, final Point2D center, final double angle) {
|
|
||||||
final double resultX = center.getX() + (a.getX() - center.getX()) * Math.cos(angle) - (a.getY() - center.getY()) * Math
|
|
||||||
.sin(angle);
|
|
||||||
final double resultY = center.getY() + (a.getX() - center.getX()) * Math.sin(angle) + (a.getY() - center.getY()) * Math
|
|
||||||
.cos(angle);
|
|
||||||
return new Point2D(resultX, resultY);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Point2D makeControlPoint(final double endX, final double endY, final Circle circle, final int numSegments, int direction) {
|
|
||||||
final double controlPointDistance = (4.0 / 3.0) * Math.tan(Math.PI / (2 * numSegments)) * circle.getRadius();
|
|
||||||
final Point2D center = new Point2D(circle.getCenterX(), circle.getCenterY());
|
|
||||||
final Point2D end = new Point2D(endX, endY);
|
|
||||||
Point2D perp = rotate(center, end, direction * Math.PI / 2.);
|
|
||||||
Point2D diff = perp.subtract(end);
|
|
||||||
diff = diff.normalize();
|
|
||||||
diff = scale(diff, controlPointDistance);
|
|
||||||
return end.add(diff);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Point2D scale(final Point2D a, final double scale) {
|
|
||||||
return new Point2D(a.getX() * scale, a.getY() * scale);
|
|
||||||
}
|
|
||||||
|
|
||||||
final class RecentColorPath extends Path {
|
|
||||||
PathClickTransition transition;
|
|
||||||
|
|
||||||
RecentColorPath(final PathElement... elements) {
|
|
||||||
super(elements);
|
|
||||||
this.setStrokeLineCap(StrokeLineCap.ROUND);
|
|
||||||
this.setStrokeWidth(0);
|
|
||||||
this.setStrokeType(StrokeType.CENTERED);
|
|
||||||
this.setCache(true);
|
|
||||||
JFXDepthManager.setDepth(this, 2);
|
|
||||||
this.transition = new PathClickTransition(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
void playTransition(final double rate) {
|
|
||||||
transition.setRate(rate);
|
|
||||||
transition.play();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final class PathClickTransition extends CachedTransition {
|
|
||||||
PathClickTransition(final Path path) {
|
|
||||||
super(JFXCustomColorPicker.this, new Timeline(
|
|
||||||
new KeyFrame(Duration.ZERO,
|
|
||||||
new KeyValue(((DropShadow) path.getEffect()).radiusProperty(),
|
|
||||||
JFXDepthManager.getShadowAt(2).radiusProperty().get(),
|
|
||||||
EASE_BOTH),
|
|
||||||
new KeyValue(((DropShadow) path.getEffect()).spreadProperty(),
|
|
||||||
JFXDepthManager.getShadowAt(2).spreadProperty().get(),
|
|
||||||
EASE_BOTH),
|
|
||||||
new KeyValue(((DropShadow) path.getEffect()).offsetXProperty(),
|
|
||||||
JFXDepthManager.getShadowAt(2).offsetXProperty().get(),
|
|
||||||
EASE_BOTH),
|
|
||||||
new KeyValue(((DropShadow) path.getEffect()).offsetYProperty(),
|
|
||||||
JFXDepthManager.getShadowAt(2).offsetYProperty().get(),
|
|
||||||
EASE_BOTH),
|
|
||||||
new KeyValue(path.strokeWidthProperty(), 0, EASE_BOTH)
|
|
||||||
),
|
|
||||||
new KeyFrame(Duration.millis(1000),
|
|
||||||
new KeyValue(((DropShadow) path.getEffect()).radiusProperty(),
|
|
||||||
JFXDepthManager.getShadowAt(5).radiusProperty().get(),
|
|
||||||
EASE_BOTH),
|
|
||||||
new KeyValue(((DropShadow) path.getEffect()).spreadProperty(),
|
|
||||||
JFXDepthManager.getShadowAt(5).spreadProperty().get(),
|
|
||||||
EASE_BOTH),
|
|
||||||
new KeyValue(((DropShadow) path.getEffect()).offsetXProperty(),
|
|
||||||
JFXDepthManager.getShadowAt(5).offsetXProperty().get(),
|
|
||||||
EASE_BOTH),
|
|
||||||
new KeyValue(((DropShadow) path.getEffect()).offsetYProperty(),
|
|
||||||
JFXDepthManager.getShadowAt(5).offsetYProperty().get(),
|
|
||||||
EASE_BOTH),
|
|
||||||
new KeyValue(path.strokeWidthProperty(), 2, EASE_BOTH)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
// reduce the number to increase the shifting , increase number to reduce shifting
|
|
||||||
setCycleDuration(Duration.millis(120));
|
|
||||||
setDelay(Duration.seconds(0));
|
|
||||||
setAutoReverse(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,459 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.jfoenix.skins;
|
|
||||||
|
|
||||||
import com.jfoenix.controls.*;
|
|
||||||
import com.jfoenix.svg.SVGGlyph;
|
|
||||||
import com.jfoenix.transitions.JFXFillTransition;
|
|
||||||
import javafx.animation.*;
|
|
||||||
import javafx.application.Platform;
|
|
||||||
import javafx.beans.InvalidationListener;
|
|
||||||
import javafx.beans.Observable;
|
|
||||||
import javafx.beans.binding.Bindings;
|
|
||||||
import javafx.beans.property.ObjectProperty;
|
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
|
||||||
import javafx.event.EventHandler;
|
|
||||||
import javafx.geometry.Insets;
|
|
||||||
import javafx.geometry.Rectangle2D;
|
|
||||||
import javafx.scene.Scene;
|
|
||||||
import javafx.scene.control.Tab;
|
|
||||||
import javafx.scene.control.TextFormatter;
|
|
||||||
import javafx.scene.input.KeyEvent;
|
|
||||||
import javafx.scene.layout.*;
|
|
||||||
import javafx.scene.paint.Color;
|
|
||||||
import javafx.stage.*;
|
|
||||||
import javafx.util.Duration;
|
|
||||||
import org.glavo.monetfx.ColorScheme;
|
|
||||||
import org.jackhuang.hmcl.setting.StyleSheets;
|
|
||||||
import org.jackhuang.hmcl.theme.Themes;
|
|
||||||
import org.jackhuang.hmcl.util.StringUtils;
|
|
||||||
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Shadi Shaheen
|
|
||||||
*/
|
|
||||||
public class JFXCustomColorPickerDialog extends StackPane {
|
|
||||||
private final Stage dialog = new Stage();
|
|
||||||
// used for concurrency control and preventing FX-thread over use
|
|
||||||
private final AtomicInteger concurrencyController = new AtomicInteger(-1);
|
|
||||||
|
|
||||||
private final ObjectProperty<Color> currentColorProperty = new SimpleObjectProperty<>(Color.WHITE);
|
|
||||||
private final ObjectProperty<Color> customColorProperty = new SimpleObjectProperty<>(Color.TRANSPARENT);
|
|
||||||
private Runnable onSave;
|
|
||||||
|
|
||||||
private final Scene customScene;
|
|
||||||
private final JFXCustomColorPicker curvedColorPicker;
|
|
||||||
private ParallelTransition paraTransition;
|
|
||||||
private final JFXDecorator pickerDecorator;
|
|
||||||
private boolean systemChange = false;
|
|
||||||
private boolean userChange = false;
|
|
||||||
private boolean initOnce = true;
|
|
||||||
private final Runnable initRun;
|
|
||||||
|
|
||||||
public JFXCustomColorPickerDialog(Window owner) {
|
|
||||||
getStyleClass().add("custom-color-dialog");
|
|
||||||
if (owner != null) {
|
|
||||||
dialog.initOwner(owner);
|
|
||||||
}
|
|
||||||
dialog.initModality(Modality.APPLICATION_MODAL);
|
|
||||||
dialog.initStyle(StageStyle.TRANSPARENT);
|
|
||||||
dialog.setResizable(false);
|
|
||||||
|
|
||||||
// create JFX Decorator
|
|
||||||
pickerDecorator = new JFXDecorator(dialog, this, false, false, false);
|
|
||||||
pickerDecorator.setOnCloseButtonAction(this::close);
|
|
||||||
pickerDecorator.setPickOnBounds(false);
|
|
||||||
customScene = new Scene(pickerDecorator, Color.TRANSPARENT);
|
|
||||||
StyleSheets.init(customScene);
|
|
||||||
curvedColorPicker = new JFXCustomColorPicker();
|
|
||||||
|
|
||||||
StackPane pane = new StackPane(curvedColorPicker);
|
|
||||||
pane.setPadding(new Insets(18));
|
|
||||||
|
|
||||||
VBox container = new VBox();
|
|
||||||
container.getChildren().add(pane);
|
|
||||||
|
|
||||||
JFXTabPane tabs = new JFXTabPane();
|
|
||||||
|
|
||||||
JFXTextField rgbField = new JFXTextField();
|
|
||||||
JFXTextField hsbField = new JFXTextField();
|
|
||||||
JFXTextField hexField = new JFXTextField();
|
|
||||||
|
|
||||||
rgbField.getStyleClass().add("custom-color-field");
|
|
||||||
rgbField.setPromptText("RGB Color");
|
|
||||||
rgbField.setTextFormatter(colorCharFormatter());
|
|
||||||
rgbField.textProperty().addListener((o, oldVal, newVal) -> updateColorFromUserInput(newVal));
|
|
||||||
|
|
||||||
hsbField.getStyleClass().add("custom-color-field");
|
|
||||||
hsbField.setPromptText("HSB Color");
|
|
||||||
hsbField.setTextFormatter(colorCharFormatter());
|
|
||||||
hsbField.textProperty().addListener((o, oldVal, newVal) -> updateColorFromUserInput(newVal));
|
|
||||||
|
|
||||||
hexField.getStyleClass().add("custom-color-field");
|
|
||||||
hexField.setPromptText("#HEX Color");
|
|
||||||
hexField.setTextFormatter(colorCharFormatter());
|
|
||||||
hexField.textProperty().addListener((o, oldVal, newVal) -> updateColorFromUserInput(newVal));
|
|
||||||
|
|
||||||
StackPane tabContent = new StackPane();
|
|
||||||
tabContent.getChildren().add(rgbField);
|
|
||||||
tabContent.setMinHeight(100);
|
|
||||||
|
|
||||||
Tab rgbTab = new Tab("RGB");
|
|
||||||
rgbTab.setContent(tabContent);
|
|
||||||
Tab hsbTab = new Tab("HSB");
|
|
||||||
hsbTab.setContent(hsbField);
|
|
||||||
Tab hexTab = new Tab("HEX");
|
|
||||||
hexTab.setContent(hexField);
|
|
||||||
|
|
||||||
tabs.getTabs().add(hexTab);
|
|
||||||
tabs.getTabs().add(rgbTab);
|
|
||||||
tabs.getTabs().add(hsbTab);
|
|
||||||
|
|
||||||
curvedColorPicker.selectedPath.addListener((o, oldVal, newVal) -> {
|
|
||||||
if (paraTransition != null) {
|
|
||||||
paraTransition.stop();
|
|
||||||
}
|
|
||||||
Region tabsHeader = (Region) tabs.lookup(".tab-header-background");
|
|
||||||
pane.backgroundProperty().unbind();
|
|
||||||
tabsHeader.backgroundProperty().unbind();
|
|
||||||
JFXFillTransition fillTransition = new JFXFillTransition(Duration.millis(240),
|
|
||||||
pane,
|
|
||||||
(Color) oldVal.getFill(),
|
|
||||||
(Color) newVal.getFill());
|
|
||||||
JFXFillTransition tabsFillTransition = new JFXFillTransition(Duration.millis(240),
|
|
||||||
tabsHeader,
|
|
||||||
(Color) oldVal.getFill(),
|
|
||||||
(Color) newVal.getFill());
|
|
||||||
paraTransition = new ParallelTransition(fillTransition, tabsFillTransition);
|
|
||||||
paraTransition.setOnFinished((finish) -> {
|
|
||||||
tabsHeader.backgroundProperty().bind(Bindings.createObjectBinding(() -> {
|
|
||||||
return new Background(new BackgroundFill(newVal.getFill(), CornerRadii.EMPTY, Insets.EMPTY));
|
|
||||||
}, newVal.fillProperty()));
|
|
||||||
pane.backgroundProperty().bind(Bindings.createObjectBinding(() -> {
|
|
||||||
return new Background(new BackgroundFill(newVal.getFill(), CornerRadii.EMPTY, Insets.EMPTY));
|
|
||||||
}, newVal.fillProperty()));
|
|
||||||
});
|
|
||||||
paraTransition.play();
|
|
||||||
});
|
|
||||||
|
|
||||||
container.getChildren().add(tabs);
|
|
||||||
|
|
||||||
HBox actionsHBox = new HBox();
|
|
||||||
actionsHBox.getStyleClass().add("jfx-color-dialog-actions");
|
|
||||||
|
|
||||||
JFXButton acceptButton = new JFXButton(i18n("button.ok"));
|
|
||||||
acceptButton.setOnAction(event -> updateColor());
|
|
||||||
acceptButton.getStyleClass().add("jfx-color-dialog-accept");
|
|
||||||
|
|
||||||
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
|
|
||||||
cancelButton.setOnAction(event -> close());
|
|
||||||
cancelButton.getStyleClass().add("jfx-color-dialog-cancel");
|
|
||||||
|
|
||||||
actionsHBox.getChildren().addAll(acceptButton, cancelButton);
|
|
||||||
container.getChildren().add(actionsHBox);
|
|
||||||
|
|
||||||
initRun = () -> {
|
|
||||||
// change tabs labels font color according to the selected color
|
|
||||||
pane.backgroundProperty().addListener((o, oldVal, newVal) -> {
|
|
||||||
if (concurrencyController.getAndSet(1) == -1) {
|
|
||||||
Color fontColor = ((Color) newVal.getFills().get(0).getFill()).grayscale()
|
|
||||||
.getRed() > 0.5 ? Color.valueOf(
|
|
||||||
"rgba(40, 40, 40, 0.87)") : Color.valueOf("rgba(255, 255, 255, 0.87)");
|
|
||||||
// for (Node tabNode : tabs.lookupAll(".tab")) {
|
|
||||||
// for (Node node : tabNode.lookupAll(".tab-label")) {
|
|
||||||
// ((Label) node).setTextFill(fontColor);
|
|
||||||
// }
|
|
||||||
// for (Node node : tabNode.lookupAll(".jfx-rippler")) {
|
|
||||||
// ((JFXRippler) node).setRipplerFill(fontColor);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// ((Pane) tabs.lookup(".tab-selected-line")).setBackground(new Background(new BackgroundFill(fontColor, CornerRadii.EMPTY, Insets.EMPTY)));
|
|
||||||
pickerDecorator.lookupAll(".jfx-decorator-button").forEach(button -> {
|
|
||||||
((JFXButton) button).setRipplerFill(fontColor);
|
|
||||||
((SVGGlyph) ((JFXButton) button).getGraphic()).setFill(fontColor);
|
|
||||||
});
|
|
||||||
|
|
||||||
Color newColor = (Color) newVal.getFills().get(0).getFill();
|
|
||||||
String hex = String.format("#%02X%02X%02X",
|
|
||||||
(int) (newColor.getRed() * 255),
|
|
||||||
(int) (newColor.getGreen() * 255),
|
|
||||||
(int) (newColor.getBlue() * 255));
|
|
||||||
String rgb = String.format("rgb(%d, %d, %d)",
|
|
||||||
(int) (newColor.getRed() * 255),
|
|
||||||
(int) (newColor.getGreen() * 255),
|
|
||||||
(int) (newColor.getBlue() * 255));
|
|
||||||
String hsb = String.format("hsl(%d, %d%%, %d%%)",
|
|
||||||
(int) (newColor.getHue()),
|
|
||||||
(int) (newColor.getSaturation() * 100),
|
|
||||||
(int) (newColor.getBrightness() * 100));
|
|
||||||
|
|
||||||
if (!userChange) {
|
|
||||||
systemChange = true;
|
|
||||||
rgbField.setText(rgb);
|
|
||||||
hsbField.setText(hsb);
|
|
||||||
hexField.setText(hex);
|
|
||||||
systemChange = false;
|
|
||||||
}
|
|
||||||
concurrencyController.getAndSet(-1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// initial selected colors
|
|
||||||
Platform.runLater(() -> {
|
|
||||||
pane.setBackground(new Background(new BackgroundFill(curvedColorPicker.getColor(curvedColorPicker.getSelectedIndex()),
|
|
||||||
CornerRadii.EMPTY,
|
|
||||||
Insets.EMPTY)));
|
|
||||||
((Region) tabs.lookup(".tab-header-background")).setBackground(new Background(new BackgroundFill(
|
|
||||||
curvedColorPicker.getColor(curvedColorPicker.getSelectedIndex()),
|
|
||||||
CornerRadii.EMPTY,
|
|
||||||
Insets.EMPTY)));
|
|
||||||
Region tabsHeader = (Region) tabs.lookup(".tab-header-background");
|
|
||||||
pane.backgroundProperty().unbind();
|
|
||||||
tabsHeader.backgroundProperty().unbind();
|
|
||||||
tabsHeader.backgroundProperty().bind(Bindings.createObjectBinding(() -> {
|
|
||||||
return new Background(new BackgroundFill(curvedColorPicker.selectedPath.get().getFill(),
|
|
||||||
CornerRadii.EMPTY,
|
|
||||||
Insets.EMPTY));
|
|
||||||
}, curvedColorPicker.selectedPath.get().fillProperty()));
|
|
||||||
pane.backgroundProperty().bind(Bindings.createObjectBinding(() -> {
|
|
||||||
return new Background(new BackgroundFill(curvedColorPicker.selectedPath.get().getFill(),
|
|
||||||
CornerRadii.EMPTY,
|
|
||||||
Insets.EMPTY));
|
|
||||||
}, curvedColorPicker.selectedPath.get().fillProperty()));
|
|
||||||
|
|
||||||
// bind text field line color
|
|
||||||
rgbField.focusColorProperty().bind(Bindings.createObjectBinding(() -> {
|
|
||||||
return pane.getBackground().getFills().get(0).getFill();
|
|
||||||
}, pane.backgroundProperty()));
|
|
||||||
hsbField.focusColorProperty().bind(Bindings.createObjectBinding(() -> {
|
|
||||||
return pane.getBackground().getFills().get(0).getFill();
|
|
||||||
}, pane.backgroundProperty()));
|
|
||||||
hexField.focusColorProperty().bind(Bindings.createObjectBinding(() -> {
|
|
||||||
return pane.getBackground().getFills().get(0).getFill();
|
|
||||||
}, pane.backgroundProperty()));
|
|
||||||
acceptButton.textFillProperty().bind(Bindings.createObjectBinding(() -> {
|
|
||||||
Color fill = (Color) pane.getBackground().getFills().get(0).getFill();
|
|
||||||
return ColorScheme.newBuilder(Themes.getColorScheme())
|
|
||||||
.setPrimaryColorSeed(fill)
|
|
||||||
.build()
|
|
||||||
.getPrimary();
|
|
||||||
}, pane.backgroundProperty()));
|
|
||||||
|
|
||||||
((Pane) pickerDecorator.lookup(".jfx-decorator-buttons-container")).backgroundProperty()
|
|
||||||
.bind(Bindings.createObjectBinding(() -> {
|
|
||||||
return new Background(new BackgroundFill(
|
|
||||||
pane.getBackground()
|
|
||||||
.getFills()
|
|
||||||
.get(0)
|
|
||||||
.getFill(),
|
|
||||||
CornerRadii.EMPTY,
|
|
||||||
Insets.EMPTY));
|
|
||||||
}, pane.backgroundProperty()));
|
|
||||||
|
|
||||||
((Pane) pickerDecorator.lookup(".jfx-decorator-content-container")).borderProperty()
|
|
||||||
.bind(Bindings.createObjectBinding(() -> {
|
|
||||||
return new Border(new BorderStroke(
|
|
||||||
pane.getBackground()
|
|
||||||
.getFills()
|
|
||||||
.get(0)
|
|
||||||
.getFill(),
|
|
||||||
BorderStrokeStyle.SOLID,
|
|
||||||
CornerRadii.EMPTY,
|
|
||||||
new BorderWidths(0,
|
|
||||||
4,
|
|
||||||
4,
|
|
||||||
4)));
|
|
||||||
}, pane.backgroundProperty()));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
this.getChildren().add(container);
|
|
||||||
this.setPadding(new Insets(0));
|
|
||||||
|
|
||||||
dialog.setScene(customScene);
|
|
||||||
final EventHandler<KeyEvent> keyEventListener = key -> {
|
|
||||||
switch (key.getCode()) {
|
|
||||||
case ESCAPE:
|
|
||||||
close();
|
|
||||||
break;
|
|
||||||
case ENTER:
|
|
||||||
updateColor();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
dialog.addEventHandler(KeyEvent.ANY, keyEventListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateColor() {
|
|
||||||
close();
|
|
||||||
this.customColorProperty.set(curvedColorPicker.getColor(curvedColorPicker.getSelectedIndex()));
|
|
||||||
this.onSave.run();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateColorFromUserInput(String colorWebString) {
|
|
||||||
if (!systemChange) {
|
|
||||||
userChange = true;
|
|
||||||
try {
|
|
||||||
Color color = Color.valueOf(colorWebString);
|
|
||||||
if (color.getOpacity() != 1.0) {
|
|
||||||
color = Color.color(color.getRed(), color.getGreen(), color.getBlue());
|
|
||||||
}
|
|
||||||
curvedColorPicker.setColor(color);
|
|
||||||
} catch (IllegalArgumentException ignored) {
|
|
||||||
// if color is not valid then do nothing
|
|
||||||
}
|
|
||||||
userChange = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void close() {
|
|
||||||
dialog.setScene(null);
|
|
||||||
dialog.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCurrentColor(Color currentColor) {
|
|
||||||
this.currentColorProperty.set(currentColor);
|
|
||||||
if (curvedColorPicker != null && currentColor != null) {
|
|
||||||
curvedColorPicker.setColor(currentColor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Color getCurrentColor() {
|
|
||||||
return currentColorProperty.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
ObjectProperty<Color> customColorProperty() {
|
|
||||||
return customColorProperty;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setCustomColor(Color color) {
|
|
||||||
customColorProperty.set(color);
|
|
||||||
}
|
|
||||||
|
|
||||||
Color getCustomColor() {
|
|
||||||
return customColorProperty.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Runnable getOnSave() {
|
|
||||||
return onSave;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOnSave(Runnable onSave) {
|
|
||||||
this.onSave = onSave;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOnHidden(EventHandler<WindowEvent> onHidden) {
|
|
||||||
dialog.setOnHidden(onHidden);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void show() {
|
|
||||||
dialog.setOpacity(0);
|
|
||||||
// pickerDecorator.setOpacity(0);
|
|
||||||
if (dialog.getOwner() != null) {
|
|
||||||
dialog.widthProperty().addListener(positionAdjuster);
|
|
||||||
dialog.heightProperty().addListener(positionAdjuster);
|
|
||||||
positionAdjuster.invalidated(null);
|
|
||||||
}
|
|
||||||
if (dialog.getScene() == null) {
|
|
||||||
dialog.setScene(customScene);
|
|
||||||
}
|
|
||||||
curvedColorPicker.preAnimate();
|
|
||||||
dialog.show();
|
|
||||||
if (initOnce) {
|
|
||||||
initRun.run();
|
|
||||||
initOnce = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Timeline timeline = new Timeline(new KeyFrame(Duration.millis(120),
|
|
||||||
new KeyValue(dialog.opacityProperty(),
|
|
||||||
1,
|
|
||||||
Interpolator.EASE_BOTH)));
|
|
||||||
timeline.setOnFinished((finish) -> curvedColorPicker.animate());
|
|
||||||
timeline.play();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// add option to show color picker using JFX Dialog
|
|
||||||
private InvalidationListener positionAdjuster = new InvalidationListener() {
|
|
||||||
@Override
|
|
||||||
public void invalidated(Observable ignored) {
|
|
||||||
if (Double.isNaN(dialog.getWidth()) || Double.isNaN(dialog.getHeight())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
dialog.widthProperty().removeListener(positionAdjuster);
|
|
||||||
dialog.heightProperty().removeListener(positionAdjuster);
|
|
||||||
fixPosition();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private void fixPosition() {
|
|
||||||
Window w = dialog.getOwner();
|
|
||||||
Screen s = com.sun.javafx.util.Utils.getScreen(w);
|
|
||||||
Rectangle2D sb = s.getBounds();
|
|
||||||
double xR = w.getX() + w.getWidth();
|
|
||||||
double xL = w.getX() - dialog.getWidth();
|
|
||||||
double x;
|
|
||||||
double y;
|
|
||||||
if (sb.getMaxX() >= xR + dialog.getWidth()) {
|
|
||||||
x = xR;
|
|
||||||
} else if (sb.getMinX() <= xL) {
|
|
||||||
x = xL;
|
|
||||||
} else {
|
|
||||||
x = Math.max(sb.getMinX(), sb.getMaxX() - dialog.getWidth());
|
|
||||||
}
|
|
||||||
y = Math.max(sb.getMinY(), Math.min(sb.getMaxY() - dialog.getHeight(), w.getY()));
|
|
||||||
dialog.setX(x);
|
|
||||||
dialog.setY(y);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void layoutChildren() {
|
|
||||||
super.layoutChildren();
|
|
||||||
if (dialog.getMinWidth() > 0 && dialog.getMinHeight() > 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
double minWidth = Math.max(0, computeMinWidth(getHeight()) + (dialog.getWidth() - customScene.getWidth()));
|
|
||||||
double minHeight = Math.max(0, computeMinHeight(getWidth()) + (dialog.getHeight() - customScene.getHeight()));
|
|
||||||
dialog.setMinWidth(minWidth);
|
|
||||||
dialog.setMinHeight(minHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final Pattern COLOR_CHAR_PATTERN = Pattern.compile("[0-9a-zA-Z#(),%.\\s]*");
|
|
||||||
|
|
||||||
private static TextFormatter<String> colorCharFormatter() {
|
|
||||||
return new TextFormatter<>(change -> {
|
|
||||||
if (!change.isContentChange()) return change;
|
|
||||||
|
|
||||||
String ins = StringUtils.toHalfWidth(change.getText());
|
|
||||||
if (!COLOR_CHAR_PATTERN.matcher(ins).matches()) return null;
|
|
||||||
String full = StringUtils.toHalfWidth(change.getControlNewText());
|
|
||||||
long h = full.chars().filter(c -> c == '#').count();
|
|
||||||
if (h > 1 || (h == 1 && full.indexOf('#') != 0)) return null;
|
|
||||||
change.setText(ins);
|
|
||||||
return change;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,731 +0,0 @@
|
|||||||
//
|
|
||||||
// Source code recreated from a .class file by IntelliJ IDEA
|
|
||||||
// (powered by Fernflower decompiler)
|
|
||||||
//
|
|
||||||
|
|
||||||
package com.jfoenix.skins;
|
|
||||||
|
|
||||||
import com.jfoenix.controls.JFXButton;
|
|
||||||
import com.jfoenix.controls.JFXDatePicker;
|
|
||||||
import com.jfoenix.controls.JFXListView;
|
|
||||||
import com.jfoenix.svg.SVGGlyph;
|
|
||||||
import com.jfoenix.transitions.CachedTransition;
|
|
||||||
import javafx.animation.*;
|
|
||||||
import javafx.animation.Animation.Status;
|
|
||||||
import javafx.beans.InvalidationListener;
|
|
||||||
import javafx.beans.property.ObjectProperty;
|
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
|
||||||
import javafx.css.PseudoClass;
|
|
||||||
import javafx.geometry.Insets;
|
|
||||||
import javafx.geometry.Pos;
|
|
||||||
import javafx.scene.Node;
|
|
||||||
import javafx.scene.SnapshotParameters;
|
|
||||||
import javafx.scene.control.*;
|
|
||||||
import javafx.scene.image.ImageView;
|
|
||||||
import javafx.scene.image.WritableImage;
|
|
||||||
import javafx.scene.input.KeyEvent;
|
|
||||||
import javafx.scene.input.MouseButton;
|
|
||||||
import javafx.scene.input.MouseEvent;
|
|
||||||
import javafx.scene.layout.*;
|
|
||||||
import javafx.scene.paint.Color;
|
|
||||||
import javafx.scene.text.Font;
|
|
||||||
import javafx.scene.text.FontWeight;
|
|
||||||
import javafx.util.Duration;
|
|
||||||
import org.jackhuang.hmcl.setting.SettingsManager;
|
|
||||||
import org.jackhuang.hmcl.ui.FXUtils;
|
|
||||||
import org.jackhuang.hmcl.ui.SVG;
|
|
||||||
import org.jackhuang.hmcl.ui.SVGContainer;
|
|
||||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
|
||||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
|
||||||
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
|
||||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
|
||||||
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
|
|
||||||
|
|
||||||
import java.time.DateTimeException;
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.Year;
|
|
||||||
import java.time.YearMonth;
|
|
||||||
import java.time.chrono.ChronoLocalDate;
|
|
||||||
import java.time.chrono.Chronology;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
import java.time.format.DecimalStyle;
|
|
||||||
import java.time.temporal.ChronoUnit;
|
|
||||||
import java.time.temporal.WeekFields;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.stream.IntStream;
|
|
||||||
|
|
||||||
public class JFXDatePickerContent extends VBox {
|
|
||||||
private static final String SPINNER_LABEL = "spinner-label";
|
|
||||||
private static final String ROBOTO = "Roboto";
|
|
||||||
protected JFXDatePicker datePicker;
|
|
||||||
private JFXButton backMonthButton;
|
|
||||||
private JFXButton forwardMonthButton;
|
|
||||||
private Label selectedDateLabel;
|
|
||||||
private Label selectedYearLabel;
|
|
||||||
private Label monthYearLabel;
|
|
||||||
protected GridPane contentGrid;
|
|
||||||
private final StackPane calendarPlaceHolder = new StackPane();
|
|
||||||
private final CachedTransition showTransition;
|
|
||||||
private final CachedTransition hideTransition;
|
|
||||||
private ParallelTransition tempImageTransition;
|
|
||||||
private final int daysPerWeek = 7;
|
|
||||||
private final List<DateCell> weekDaysCells = new ArrayList<>();
|
|
||||||
private final List<DateCell> weekNumberCells = new ArrayList<>();
|
|
||||||
protected List<DateCell> dayCells = new ArrayList<>();
|
|
||||||
private LocalDate[] dayCellDates;
|
|
||||||
private DateCell currentFocusedDayCell = null;
|
|
||||||
private final ListView<String> yearsListView = new JFXListView<>() {
|
|
||||||
{
|
|
||||||
this.getStyleClass().addAll("date-picker-list-view", "no-padding");
|
|
||||||
this.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
|
|
||||||
this.setCellFactory((listView) -> new ListCell<>() {
|
|
||||||
static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
|
|
||||||
|
|
||||||
final Label label = new Label();
|
|
||||||
final StackPane root;
|
|
||||||
final RipplerContainer ripplerContainer;
|
|
||||||
|
|
||||||
final WeakListenerHolder holder = new WeakListenerHolder();
|
|
||||||
|
|
||||||
{
|
|
||||||
label.getStyleClass().add(SPINNER_LABEL);
|
|
||||||
label.setMaxWidth(Double.MAX_VALUE);
|
|
||||||
label.textProperty().bind(itemProperty());
|
|
||||||
StackPane pane = new StackPane(label);
|
|
||||||
pane.setPadding(new Insets(7));
|
|
||||||
ripplerContainer = new RipplerContainer(pane);
|
|
||||||
root = new StackPane(ripplerContainer);
|
|
||||||
root.getStyleClass().add("data-picker-list-cell");
|
|
||||||
this.setGraphic(root);
|
|
||||||
|
|
||||||
var selectedListenerWeak = holder.weak((InvalidationListener) observable -> {
|
|
||||||
String newVal = JFXDatePickerContent.this.selectedYearLabel.getText();
|
|
||||||
if (newVal != null && getItem() != null && getItem().equals(newVal))
|
|
||||||
updateSelected(true);
|
|
||||||
});
|
|
||||||
JFXDatePickerContent.this.selectedYearLabel.textProperty().addListener(selectedListenerWeak);
|
|
||||||
itemProperty().addListener(selectedListenerWeak);
|
|
||||||
|
|
||||||
FXUtils.onChangeAndOperate(selectedProperty(), selected -> {
|
|
||||||
if (getListView() != null) {
|
|
||||||
if (selected) {
|
|
||||||
getListView().getSelectionModel().select(getIndex());
|
|
||||||
} else {
|
|
||||||
getListView().getSelectionModel().clearSelection(getIndex());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
root.pseudoClassStateChanged(SELECTED, selected);
|
|
||||||
if (selected) {
|
|
||||||
int offset = Integer.parseInt(getItem()) - Integer.parseInt(JFXDatePickerContent.this.selectedYearLabel.getText());
|
|
||||||
JFXDatePickerContent.this.forward(offset, ChronoUnit.YEARS, false, false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void updateItem(String item, boolean empty) {
|
|
||||||
String oldItem = getItem();
|
|
||||||
boolean oldEmpty = isEmpty();
|
|
||||||
|
|
||||||
super.updateItem(item, empty);
|
|
||||||
setText(null);
|
|
||||||
|
|
||||||
if (Objects.equals(oldItem, item) && oldEmpty == empty) return;
|
|
||||||
ripplerContainer.resetFeedback();
|
|
||||||
|
|
||||||
if (!empty && item != null) {
|
|
||||||
setGraphic(root);
|
|
||||||
} else {
|
|
||||||
setGraphic(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
final DateTimeFormatter fullFormatter = DateTimeFormatter.ofPattern("EEE, MMM yyyy");
|
|
||||||
final DateTimeFormatter monthFormatter = DateTimeFormatter.ofPattern("MMMM");
|
|
||||||
final DateTimeFormatter yearFormatter = DateTimeFormatter.ofPattern("y");
|
|
||||||
final DateTimeFormatter weekNumberFormatter = DateTimeFormatter.ofPattern("w");
|
|
||||||
final DateTimeFormatter weekDayNameFormatter = DateTimeFormatter.ofPattern("ccc");
|
|
||||||
final DateTimeFormatter dayCellFormatter = DateTimeFormatter.ofPattern("d");
|
|
||||||
private final ObjectProperty<YearMonth> selectedYearMonth = new SimpleObjectProperty<>(this, "selectedYearMonth");
|
|
||||||
|
|
||||||
final ObjectProperty<LocalDate> valueProperty = new SimpleObjectProperty<>(this, "value");
|
|
||||||
|
|
||||||
JFXDatePickerContent(final DatePicker datePicker) {
|
|
||||||
this.datePicker = (JFXDatePicker) datePicker;
|
|
||||||
this.getStyleClass().add("date-picker-popup");
|
|
||||||
LocalDate date = datePicker.getValue();
|
|
||||||
this.valueProperty.set(date);
|
|
||||||
this.valueProperty.addListener((o, old, n) -> {
|
|
||||||
this.displayedYearMonthProperty().set(n != null ? YearMonth.from(n) : YearMonth.now());
|
|
||||||
this.updateValues();
|
|
||||||
});
|
|
||||||
this.selectedYearMonth.set(date != null ? YearMonth.from(date) : YearMonth.now());
|
|
||||||
this.selectedYearMonth.addListener((observable, oldValue, newValue) -> this.updateValues());
|
|
||||||
this.getChildren().add(this.createHeaderPane());
|
|
||||||
this.getChildren().add(new Separator());
|
|
||||||
this.contentGrid = new GridPane() {
|
|
||||||
protected double computePrefWidth(double height) {
|
|
||||||
int nCols = JFXDatePickerContent.this.daysPerWeek + (datePicker.isShowWeekNumbers() ? 1 : 0);
|
|
||||||
double leftSpace = this.snapSpaceX(this.getInsets().getLeft());
|
|
||||||
double rightSpace = this.snapSpaceX(this.getInsets().getRight());
|
|
||||||
double hgaps = this.snapSpaceX(this.getHgap()) * (double) (nCols - 1);
|
|
||||||
double contentWidth = super.computePrefWidth(height) - leftSpace - rightSpace - hgaps;
|
|
||||||
return this.snapSizeX(contentWidth / (double) nCols) * (double) nCols + leftSpace + rightSpace + hgaps;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void layoutChildren() {
|
|
||||||
if (this.getWidth() > (double) 0.0F && this.getHeight() > (double) 0.0F) {
|
|
||||||
super.layoutChildren();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.contentGrid.setFocusTraversable(true);
|
|
||||||
this.contentGrid.getStyleClass().add("calendar-grid");
|
|
||||||
this.contentGrid.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY)));
|
|
||||||
this.contentGrid.setPadding(new Insets(0.0F, 12.0F, 12.0F, 12.0F));
|
|
||||||
this.contentGrid.setVgap(4);
|
|
||||||
this.contentGrid.setHgap(4);
|
|
||||||
this.createWeekDaysCells();
|
|
||||||
this.createDayCells();
|
|
||||||
|
|
||||||
this.yearsListView.getItems().addAll(IntStream.rangeClosed(1900, Year.now().getValue() + 100).mapToObj(Integer::toString).toList());
|
|
||||||
|
|
||||||
StackPane main = new StackPane();
|
|
||||||
this.yearsListView.setVisible(false);
|
|
||||||
this.yearsListView.setOpacity(0.0F);
|
|
||||||
this.yearsListView.maxWidthProperty().bind(main.widthProperty());
|
|
||||||
this.yearsListView.maxHeightProperty().bind(main.heightProperty());
|
|
||||||
main.getChildren().setAll(this.contentGrid, this.yearsListView);
|
|
||||||
|
|
||||||
VBox contentHolder = new VBox();
|
|
||||||
contentHolder.getChildren().setAll(this.createCalendarMonthLabelPane(), main);
|
|
||||||
this.calendarPlaceHolder.getStyleClass().add("content");
|
|
||||||
this.calendarPlaceHolder.getChildren().setAll(contentHolder, this.createCalendarArrowsPane());
|
|
||||||
FXUtils.setOverflowHidden(this.calendarPlaceHolder);
|
|
||||||
|
|
||||||
this.getChildren().add(this.calendarPlaceHolder);
|
|
||||||
|
|
||||||
this.getChildren().add(this.createActions());
|
|
||||||
|
|
||||||
this.refresh();
|
|
||||||
this.addEventHandler(KeyEvent.ANY, (event) -> {
|
|
||||||
Node node = this.getScene().getFocusOwner();
|
|
||||||
if (node instanceof DateCell) {
|
|
||||||
this.currentFocusedDayCell = (DateCell) node;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (event.getCode()) {
|
|
||||||
case HOME -> {
|
|
||||||
this.init();
|
|
||||||
this.goToDate(LocalDate.now(), true);
|
|
||||||
event.consume();
|
|
||||||
}
|
|
||||||
case PAGE_UP -> {
|
|
||||||
if (!this.backMonthButton.isDisabled()) {
|
|
||||||
this.forward(-1, ChronoUnit.MONTHS, true, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
event.consume();
|
|
||||||
}
|
|
||||||
case PAGE_DOWN -> {
|
|
||||||
if (!this.forwardMonthButton.isDisabled()) {
|
|
||||||
this.forward(1, ChronoUnit.MONTHS, true, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
event.consume();
|
|
||||||
}
|
|
||||||
case ESCAPE -> {
|
|
||||||
fireEvent(new DialogCloseEvent());
|
|
||||||
event.consume();
|
|
||||||
}
|
|
||||||
default -> event.consume();
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
this.showTransition = new CachedTransition(this.yearsListView, new Timeline(new KeyFrame(Duration.millis(0.0F), new KeyValue(this.yearsListView.opacityProperty(), 0, Interpolator.EASE_BOTH), new KeyValue(this.contentGrid.opacityProperty(), 1, Interpolator.EASE_BOTH)), new KeyFrame(Duration.millis(500.0F), new KeyValue(this.yearsListView.opacityProperty(), 0, Interpolator.EASE_BOTH), new KeyValue(this.contentGrid.opacityProperty(), 0, Interpolator.EASE_BOTH)), new KeyFrame(Duration.millis(1000.0F), new KeyValue(this.yearsListView.opacityProperty(), 1, Interpolator.EASE_BOTH), new KeyValue(this.contentGrid.opacityProperty(), 0, Interpolator.EASE_BOTH)))) {
|
|
||||||
{
|
|
||||||
this.setCycleDuration(Duration.millis(320.0F));
|
|
||||||
this.setDelay(Duration.seconds(0.0F));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void starting() {
|
|
||||||
super.starting();
|
|
||||||
JFXDatePickerContent.this.yearsListView.setVisible(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.hideTransition = new CachedTransition(this.yearsListView, new Timeline(new KeyFrame(Duration.millis(0.0F), new KeyValue(this.yearsListView.opacityProperty(), 1, Interpolator.EASE_BOTH), new KeyValue(this.contentGrid.opacityProperty(), 0, Interpolator.EASE_BOTH)), new KeyFrame(Duration.millis(500.0F), new KeyValue(this.yearsListView.opacityProperty(), 0, Interpolator.EASE_BOTH), new KeyValue(this.contentGrid.opacityProperty(), 0, Interpolator.EASE_BOTH)), new KeyFrame(Duration.millis(1000.0F), new KeyValue(this.yearsListView.opacityProperty(), 0, Interpolator.EASE_BOTH), new KeyValue(this.contentGrid.opacityProperty(), 1, Interpolator.EASE_BOTH)))) {
|
|
||||||
{
|
|
||||||
this.setCycleDuration(Duration.millis(320.0F));
|
|
||||||
this.setDelay(Duration.seconds(0.0F));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void stopping() {
|
|
||||||
super.stopping();
|
|
||||||
JFXDatePickerContent.this.yearsListView.setVisible(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
ObjectProperty<YearMonth> displayedYearMonthProperty() {
|
|
||||||
return this.selectedYearMonth;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createWeekDaysCells() {
|
|
||||||
for (int i = 0; i < this.daysPerWeek; ++i) {
|
|
||||||
DateCell cell = new DateCell();
|
|
||||||
cell.getStyleClass().add("day-name-cell");
|
|
||||||
cell.setFont(Font.font(ROBOTO, FontWeight.BOLD, 12.0F));
|
|
||||||
cell.setAlignment(Pos.BASELINE_CENTER);
|
|
||||||
this.weekDaysCells.add(cell);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < 6; ++i) {
|
|
||||||
DateCell cell = new DateCell();
|
|
||||||
cell.getStyleClass().add("week-number-cell");
|
|
||||||
cell.setFont(Font.font(ROBOTO, FontWeight.BOLD, 12.0F));
|
|
||||||
this.weekNumberCells.add(cell);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
protected VBox createHeaderPane() {
|
|
||||||
Label title = new Label(I18n.i18n("button.select_date"));
|
|
||||||
title.getStyleClass().add("title");
|
|
||||||
title.setFont(Font.font(ROBOTO, FontWeight.NORMAL, 12.0F));
|
|
||||||
HBox titleContainer = new HBox(title);
|
|
||||||
titleContainer.setAlignment(Pos.CENTER_LEFT);
|
|
||||||
titleContainer.setPadding(new Insets(0, 0, 5, 0));
|
|
||||||
this.selectedYearLabel = new Label();
|
|
||||||
this.selectedYearLabel.getStyleClass().add(SPINNER_LABEL);
|
|
||||||
this.selectedYearLabel.setFont(Font.font(ROBOTO, FontWeight.BOLD, 14.0F));
|
|
||||||
HBox yearLabelContainer = new HBox();
|
|
||||||
yearLabelContainer.getStyleClass().add("spinner");
|
|
||||||
yearLabelContainer.getChildren().addAll(this.selectedYearLabel);
|
|
||||||
yearLabelContainer.setAlignment(Pos.CENTER_LEFT);
|
|
||||||
yearLabelContainer.setFillHeight(false);
|
|
||||||
yearLabelContainer.setOnMouseClicked((click) -> {
|
|
||||||
if (!this.yearsListView.isVisible()) {
|
|
||||||
int yearIndex = Integer.parseInt(this.selectedYearLabel.getText()) - 1900 - 2;
|
|
||||||
this.yearsListView.scrollTo(yearIndex >= 0 ? yearIndex : yearIndex + 2);
|
|
||||||
this.hideTransition.stop();
|
|
||||||
this.showTransition.play();
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
this.selectedDateLabel = new Label();
|
|
||||||
this.selectedDateLabel.getStyleClass().add(SPINNER_LABEL);
|
|
||||||
this.selectedDateLabel.setFont(Font.font(ROBOTO, FontWeight.BOLD, 32.0F));
|
|
||||||
HBox selectedDateContainer = new HBox();
|
|
||||||
selectedDateContainer.getStyleClass().add("spinner");
|
|
||||||
selectedDateContainer.getChildren().addAll(this.selectedDateLabel);
|
|
||||||
selectedDateContainer.setAlignment(Pos.CENTER_LEFT);
|
|
||||||
selectedDateContainer.setOnMouseClicked((click) -> {
|
|
||||||
if (this.yearsListView.isVisible()) {
|
|
||||||
this.showTransition.stop();
|
|
||||||
this.hideTransition.play();
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
VBox headerPanel = new VBox();
|
|
||||||
headerPanel.getStyleClass().add("month-year-pane");
|
|
||||||
headerPanel.setPadding(new Insets(12.0F, 24.0F, 12.0F, 24.0F));
|
|
||||||
headerPanel.getChildren().addAll(titleContainer, yearLabelContainer, selectedDateContainer);
|
|
||||||
return headerPanel;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected BorderPane createCalendarArrowsPane() {
|
|
||||||
SVGGlyph leftChevron = new SVGGlyph(0, "CHEVRON_LEFT", "M 742,-37 90,614 Q 53,651 53,704.5 53,758 90,795 l 652,651 q 37,37 90.5,37 53.5,0 90.5,-37 l 75,-75 q 37,-37 37,-90.5 0,-53.5 -37,-90.5 L 512,704 998,219 q 37,-38 37,-91 0,-53 -37,-90 L 923,-37 Q 886,-74 832.5,-74 779,-74 742,-37 z", Color.GRAY);
|
|
||||||
SVGGlyph rightChevron = new SVGGlyph(0, "CHEVRON_RIGHT", "m 1099,704 q 0,-52 -37,-91 L 410,-38 q -37,-37 -90,-37 -53,0 -90,37 l -76,75 q -37,39 -37,91 0,53 37,90 l 486,486 -486,485 q -37,39 -37,91 0,53 37,90 l 76,75 q 36,38 90,38 54,0 90,-38 l 652,-651 q 37,-37 37,-90 z", Color.GRAY);
|
|
||||||
leftChevron.setSize(6.0F, 11.0F);
|
|
||||||
rightChevron.setSize(6.0F, 11.0F);
|
|
||||||
this.backMonthButton = new JFXButton();
|
|
||||||
this.backMonthButton.setMinSize(40.0F, 40.0F);
|
|
||||||
this.backMonthButton.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, new CornerRadii(40.0F), Insets.EMPTY)));
|
|
||||||
this.backMonthButton.getStyleClass().add("left-button");
|
|
||||||
this.backMonthButton.setGraphic(leftChevron);
|
|
||||||
this.backMonthButton.setRipplerFill(this.datePicker.getDefaultColor());
|
|
||||||
this.backMonthButton.setOnAction((t) -> this.forward(-1, ChronoUnit.MONTHS, false, true));
|
|
||||||
this.forwardMonthButton = new JFXButton();
|
|
||||||
this.forwardMonthButton.setMinSize(40.0F, 40.0F);
|
|
||||||
this.forwardMonthButton.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, new CornerRadii(40.0F), Insets.EMPTY)));
|
|
||||||
this.forwardMonthButton.getStyleClass().add("right-button");
|
|
||||||
this.forwardMonthButton.setGraphic(rightChevron);
|
|
||||||
this.forwardMonthButton.setRipplerFill(this.datePicker.getDefaultColor());
|
|
||||||
this.forwardMonthButton.setOnAction((t) -> this.forward(1, ChronoUnit.MONTHS, false, true));
|
|
||||||
BorderPane arrowsContainer = new BorderPane();
|
|
||||||
arrowsContainer.setLeft(this.backMonthButton);
|
|
||||||
arrowsContainer.setRight(this.forwardMonthButton);
|
|
||||||
arrowsContainer.setPadding(new Insets(4.0F, 12.0F, 2.0F, 12.0F));
|
|
||||||
arrowsContainer.setPickOnBounds(false);
|
|
||||||
return arrowsContainer;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected BorderPane createCalendarMonthLabelPane() {
|
|
||||||
this.monthYearLabel = new Label();
|
|
||||||
this.monthYearLabel.getStyleClass().add(SPINNER_LABEL);
|
|
||||||
this.monthYearLabel.setFont(Font.font(ROBOTO, FontWeight.BOLD, 13.0F));
|
|
||||||
SVGContainer svgContainer = new SVGContainer(13);
|
|
||||||
HBox box = new HBox(monthYearLabel, svgContainer);
|
|
||||||
box.setSpacing(3);
|
|
||||||
box.setAlignment(Pos.CENTER);
|
|
||||||
FXUtils.onChangeAndOperate(this.yearsListView.visibleProperty(), b -> svgContainer.setIcon(b ? SVG.ARROW_DROP_UP : SVG.ARROW_DROP_DOWN));
|
|
||||||
FXUtils.onClicked(box, () -> {
|
|
||||||
if (!this.yearsListView.isVisible()) {
|
|
||||||
int yearIndex = Integer.parseInt(this.selectedYearLabel.getText()) - 1900 - 2;
|
|
||||||
this.yearsListView.scrollTo(yearIndex >= 0 ? yearIndex : yearIndex + 2);
|
|
||||||
this.hideTransition.stop();
|
|
||||||
this.showTransition.play();
|
|
||||||
} else {
|
|
||||||
this.showTransition.stop();
|
|
||||||
this.hideTransition.play();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
BorderPane monthContainer = new BorderPane();
|
|
||||||
monthContainer.setMinHeight(50.0F);
|
|
||||||
monthContainer.setCenter(box);
|
|
||||||
monthContainer.setPadding(new Insets(2.0F, 12.0F, 2.0F, 12.0F));
|
|
||||||
return monthContainer;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected HBox createActions() {
|
|
||||||
JFXButton cancelButton = new JFXButton(I18n.i18n("button.cancel"));
|
|
||||||
cancelButton.getStyleClass().add("dialog-cancel");
|
|
||||||
FXUtils.onClicked(cancelButton, () -> fireEvent(new DialogCloseEvent()));
|
|
||||||
|
|
||||||
JFXButton acceptButton = new JFXButton(I18n.i18n("button.ok"));
|
|
||||||
acceptButton.getStyleClass().add("dialog-accept");
|
|
||||||
FXUtils.onClicked(acceptButton, () -> {
|
|
||||||
datePicker.setValue(valueProperty.get());
|
|
||||||
fireEvent(new DialogCloseEvent());
|
|
||||||
});
|
|
||||||
|
|
||||||
HBox hBox = new HBox(cancelButton, acceptButton);
|
|
||||||
hBox.setPadding(new Insets(0, 5, 3, 0));
|
|
||||||
hBox.setSpacing(5);
|
|
||||||
hBox.setAlignment(Pos.CENTER_RIGHT);
|
|
||||||
return hBox;
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateContentGrid() {
|
|
||||||
this.contentGrid.getColumnConstraints().clear();
|
|
||||||
this.contentGrid.getChildren().clear();
|
|
||||||
int colsNumber = this.daysPerWeek + (this.datePicker.isShowWeekNumbers() ? 1 : 0);
|
|
||||||
ColumnConstraints columnConstraints = new ColumnConstraints();
|
|
||||||
columnConstraints.setPercentWidth(100.0F);
|
|
||||||
|
|
||||||
for (int i = 0; i < colsNumber; ++i) {
|
|
||||||
this.contentGrid.getColumnConstraints().add(columnConstraints);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < this.daysPerWeek; ++i) {
|
|
||||||
this.contentGrid.add(this.weekDaysCells.get(i), i + colsNumber - this.daysPerWeek, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.datePicker.isShowWeekNumbers()) {
|
|
||||||
for (int i = 0; i < 6; ++i) {
|
|
||||||
this.contentGrid.add(this.weekNumberCells.get(i), 0, i + 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int row = 0; row < 6; ++row) {
|
|
||||||
for (int col = 0; col < this.daysPerWeek; ++col) {
|
|
||||||
this.contentGrid.add(this.dayCells.get(row * this.daysPerWeek + col), col + colsNumber - this.daysPerWeek, row + 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private void refresh() {
|
|
||||||
this.updateDayNameCells();
|
|
||||||
this.updateValues();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateDayNameCells() {
|
|
||||||
int weekFirstDay = WeekFields.of(this.getLocale()).getFirstDayOfWeek().getValue();
|
|
||||||
LocalDate date = LocalDate.of(2009, 7, 12 + weekFirstDay);
|
|
||||||
|
|
||||||
for (int i = 0; i < this.daysPerWeek; ++i) {
|
|
||||||
String name = this.weekDayNameFormatter.withLocale(this.getLocale()).format(date.plusDays(i));
|
|
||||||
if (!LocaleUtils.isChinese(this.weekNumberFormatter.getLocale())) {
|
|
||||||
name = name.substring(0, 1).toUpperCase(Locale.ROOT);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.weekDaysCells.get(i).setText(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateValues() {
|
|
||||||
this.updateWeekNumberDateCells();
|
|
||||||
this.updateDayCells();
|
|
||||||
this.updateMonthYearPane();
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateWeekNumberDateCells() {
|
|
||||||
if (this.datePicker.isShowWeekNumbers()) {
|
|
||||||
Locale locale = this.getLocale();
|
|
||||||
LocalDate firstDayOfMonth = this.selectedYearMonth.get().atDay(1);
|
|
||||||
|
|
||||||
for (int i = 0; i < 6; ++i) {
|
|
||||||
LocalDate date = firstDayOfMonth.plusWeeks(i);
|
|
||||||
String weekNumber = this.weekNumberFormatter.withLocale(locale).withDecimalStyle(DecimalStyle.of(locale)).format(date);
|
|
||||||
this.weekNumberCells.get(i).setText(weekNumber);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateDayCells() {
|
|
||||||
Locale locale = this.getLocale();
|
|
||||||
Chronology chrono = this.getPrimaryChronology();
|
|
||||||
int firstDayOfWeek = WeekFields.of(this.getLocale()).getFirstDayOfWeek().getValue();
|
|
||||||
int firstOfMonthIndex = this.selectedYearMonth.get().atDay(1).getDayOfWeek().getValue() - firstDayOfWeek;
|
|
||||||
firstOfMonthIndex += firstOfMonthIndex < 0 ? this.daysPerWeek : 0;
|
|
||||||
YearMonth currentYearMonth = this.selectedYearMonth.get();
|
|
||||||
int daysInCurMonth = -1;
|
|
||||||
|
|
||||||
for (int i = 0; i < 6 * this.daysPerWeek; ++i) {
|
|
||||||
DateCell dayCell = this.dayCells.get(i);
|
|
||||||
try {
|
|
||||||
if (daysInCurMonth == -1) {
|
|
||||||
daysInCurMonth = currentYearMonth.lengthOfMonth();
|
|
||||||
}
|
|
||||||
|
|
||||||
int dayIndex = i - firstOfMonthIndex + 1;
|
|
||||||
LocalDate date;
|
|
||||||
if (dayIndex <= 0) {
|
|
||||||
YearMonth previous = currentYearMonth.minusMonths(1);
|
|
||||||
date = previous.atDay(previous.lengthOfMonth() + dayIndex);
|
|
||||||
} else if (dayIndex > daysInCurMonth) {
|
|
||||||
YearMonth next = currentYearMonth.plusMonths(1);
|
|
||||||
date = next.atDay(dayIndex - daysInCurMonth);
|
|
||||||
} else {
|
|
||||||
date = currentYearMonth.atDay(dayIndex);
|
|
||||||
}
|
|
||||||
this.dayCellDates[i] = date;
|
|
||||||
if (date.equals(LocalDate.now())) {
|
|
||||||
dayCell.getStyleClass().add("today");
|
|
||||||
} else {
|
|
||||||
dayCell.getStyleClass().removeAll("today");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (date.equals(valueProperty.get())) {
|
|
||||||
dayCell.getStyleClass().add("selected");
|
|
||||||
this.currentFocusedDayCell = dayCell;
|
|
||||||
} else {
|
|
||||||
dayCell.getStyleClass().removeAll("selected");
|
|
||||||
}
|
|
||||||
|
|
||||||
ChronoLocalDate cDate = chrono.date(date);
|
|
||||||
String cellText = this.dayCellFormatter.withLocale(locale).withChronology(chrono).withDecimalStyle(DecimalStyle.of(locale)).format(cDate);
|
|
||||||
dayCell.setText(cellText);
|
|
||||||
if (i < firstOfMonthIndex) {
|
|
||||||
dayCell.getStyleClass().add("previous-month");
|
|
||||||
dayCell.getStyleClass().removeAll("next-month");
|
|
||||||
dayCell.setDisable(true);
|
|
||||||
} else if (i >= firstOfMonthIndex + daysInCurMonth) {
|
|
||||||
dayCell.getStyleClass().add("next-month");
|
|
||||||
dayCell.getStyleClass().removeAll("previous-month");
|
|
||||||
dayCell.setDisable(true);
|
|
||||||
} else {
|
|
||||||
dayCell.getStyleClass().removeAll("previous-month", "next-month");
|
|
||||||
dayCell.setDisable(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
dayCell.updateItem(date, false);
|
|
||||||
} catch (DateTimeException var13) {
|
|
||||||
dayCell.setText("");
|
|
||||||
dayCell.setDisable(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void updateMonthYearPane() {
|
|
||||||
YearMonth yearMonth = this.selectedYearMonth.get();
|
|
||||||
this.selectedDateLabel.setText(fullFormatter.withLocale(this.getLocale()).format(Objects.requireNonNullElseGet(valueProperty.get(), LocalDate::now)));
|
|
||||||
|
|
||||||
this.selectedYearLabel.setText(this.formatYear(yearMonth));
|
|
||||||
this.monthYearLabel.setText(this.formatMonth(yearMonth) + " " + this.formatYear(yearMonth));
|
|
||||||
Chronology chrono = this.datePicker.getChronology();
|
|
||||||
LocalDate firstDayOfMonth = yearMonth.atDay(1);
|
|
||||||
this.backMonthButton.setDisable(!this.isValidDate(chrono, firstDayOfMonth, -1, ChronoUnit.DAYS));
|
|
||||||
this.forwardMonthButton.setDisable(!this.isValidDate(chrono, firstDayOfMonth, 1, ChronoUnit.MONTHS));
|
|
||||||
}
|
|
||||||
|
|
||||||
private String formatMonth(YearMonth yearMonth) {
|
|
||||||
try {
|
|
||||||
Chronology chrono = this.getPrimaryChronology();
|
|
||||||
ChronoLocalDate cDate = chrono.date(yearMonth.atDay(1));
|
|
||||||
return this.monthFormatter.withLocale(this.getLocale()).withChronology(chrono).format(cDate);
|
|
||||||
} catch (DateTimeException var4) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String formatYear(YearMonth yearMonth) {
|
|
||||||
try {
|
|
||||||
Chronology chrono = this.getPrimaryChronology();
|
|
||||||
ChronoLocalDate cDate = chrono.date(yearMonth.atDay(1));
|
|
||||||
return this.yearFormatter.withLocale(this.getLocale()).withChronology(chrono).withDecimalStyle(DecimalStyle.of(this.getLocale())).format(cDate);
|
|
||||||
} catch (DateTimeException var4) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected LocalDate dayCellDate(DateCell dateCell) {
|
|
||||||
assert this.dayCellDates != null;
|
|
||||||
|
|
||||||
return this.dayCellDates[this.dayCells.indexOf(dateCell)];
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void forward(int offset, ChronoUnit unit, boolean focusDayCell, boolean withAnimation) {
|
|
||||||
if (withAnimation && (this.tempImageTransition == null || this.tempImageTransition.getStatus() == Status.STOPPED)) {
|
|
||||||
Pane monthContent = (Pane) this.calendarPlaceHolder.getChildren().get(0);
|
|
||||||
this.getParent().setManaged(false);
|
|
||||||
SnapshotParameters snapShotparams = new SnapshotParameters();
|
|
||||||
snapShotparams.setFill(Color.TRANSPARENT);
|
|
||||||
WritableImage temp = monthContent.snapshot(snapShotparams, new WritableImage((int) monthContent.getWidth(), (int) monthContent.getHeight()));
|
|
||||||
ImageView tempImage = new ImageView(temp);
|
|
||||||
this.calendarPlaceHolder.getChildren().add(this.calendarPlaceHolder.getChildren().size() - 2, tempImage);
|
|
||||||
TranslateTransition imageTransition = new TranslateTransition(Duration.millis(160.0F), tempImage);
|
|
||||||
imageTransition.setToX((double) (-offset) * this.calendarPlaceHolder.getWidth());
|
|
||||||
imageTransition.setOnFinished((finish) -> this.calendarPlaceHolder.getChildren().remove(tempImage));
|
|
||||||
monthContent.setTranslateX((double) offset * this.calendarPlaceHolder.getWidth());
|
|
||||||
TranslateTransition contentTransition = new TranslateTransition(Duration.millis(160.0F), monthContent);
|
|
||||||
contentTransition.setToX(0.0F);
|
|
||||||
this.tempImageTransition = new ParallelTransition(imageTransition, contentTransition);
|
|
||||||
this.tempImageTransition.setOnFinished((finish) -> {
|
|
||||||
this.calendarPlaceHolder.getChildren().remove(tempImage);
|
|
||||||
this.getParent().setManaged(true);
|
|
||||||
});
|
|
||||||
this.tempImageTransition.play();
|
|
||||||
}
|
|
||||||
|
|
||||||
YearMonth yearMonth = this.selectedYearMonth.get();
|
|
||||||
DateCell dateCell = this.currentFocusedDayCell;
|
|
||||||
if (dateCell == null || this.dayCellDate(dateCell).getMonth() != yearMonth.getMonth()) {
|
|
||||||
dateCell = this.findDayCellOfDate(yearMonth.atDay(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.goToDayCell(dateCell, offset, unit, focusDayCell);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void goToDayCell(DateCell dateCell, int offset, ChronoUnit unit, boolean focusDayCell) {
|
|
||||||
this.goToDate(this.dayCellDate(dateCell).plus(offset, unit), focusDayCell);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void goToDate(LocalDate date, boolean focusDayCell) {
|
|
||||||
if (this.isValidDate(this.datePicker.getChronology(), date)) {
|
|
||||||
this.valueProperty.set(date);
|
|
||||||
this.selectedYearMonth.set(YearMonth.from(date));
|
|
||||||
if (focusDayCell) {
|
|
||||||
this.findDayCellOfDate(date).requestFocus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private void selectDayCell(DateCell dateCell) {
|
|
||||||
this.valueProperty.set(this.dayCellDate(dateCell));
|
|
||||||
}
|
|
||||||
|
|
||||||
private DateCell findDayCellOfDate(LocalDate date) {
|
|
||||||
for (int i = 0; i < this.dayCellDates.length; ++i) {
|
|
||||||
if (date.equals(this.dayCellDates[i])) {
|
|
||||||
return this.dayCells.get(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.dayCells.get(this.dayCells.size() / 2 + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
void init() {
|
|
||||||
this.contentGrid.setOpacity(1.0F);
|
|
||||||
this.yearsListView.setOpacity(0.0F);
|
|
||||||
this.yearsListView.setVisible(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
void clearFocus() {
|
|
||||||
LocalDate focusDate = this.valueProperty.get();
|
|
||||||
if (focusDate == null) {
|
|
||||||
focusDate = LocalDate.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (YearMonth.from(focusDate).equals(this.selectedYearMonth.get())) {
|
|
||||||
this.goToDate(focusDate, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void createDayCells() {
|
|
||||||
for (int row = 0; row < 6; ++row) {
|
|
||||||
for (int col = 0; col < this.daysPerWeek; ++col) {
|
|
||||||
DateCell dayCell = this.createDayCell();
|
|
||||||
dayCell.addEventHandler(MouseEvent.MOUSE_CLICKED, (click) -> {
|
|
||||||
if (click.getButton() == MouseButton.PRIMARY) {
|
|
||||||
DateCell selectedDayCell = (DateCell) click.getSource();
|
|
||||||
this.selectDayCell(selectedDayCell);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
dayCell.setOnMouseEntered((event) -> dayCell.getStyleClass().add("hovered"));
|
|
||||||
dayCell.setOnMouseExited((event) -> dayCell.getStyleClass().removeAll("hovered"));
|
|
||||||
dayCell.setAlignment(Pos.BASELINE_CENTER);
|
|
||||||
dayCell.setBorder(new Border(new BorderStroke(Color.TRANSPARENT, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, new BorderWidths(5.0F))));
|
|
||||||
dayCell.setFont(Font.font(ROBOTO, FontWeight.BOLD, 12.0F));
|
|
||||||
this.dayCells.add(dayCell);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.dayCellDates = new LocalDate[6 * this.daysPerWeek];
|
|
||||||
this.updateContentGrid();
|
|
||||||
}
|
|
||||||
|
|
||||||
private DateCell createDayCell() {
|
|
||||||
DateCell dayCell = null;
|
|
||||||
if (this.datePicker.getDayCellFactory() != null) {
|
|
||||||
dayCell = this.datePicker.getDayCellFactory().call(this.datePicker);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dayCell == null) {
|
|
||||||
dayCell = new DateCell();
|
|
||||||
}
|
|
||||||
dayCell.getStyleClass().add("day-cell");
|
|
||||||
dayCell.setPrefSize(36.0F, 36.0F);
|
|
||||||
dayCell.setDisable(false);
|
|
||||||
dayCell.setStyle(null);
|
|
||||||
dayCell.setGraphic(null);
|
|
||||||
dayCell.setTooltip(null);
|
|
||||||
|
|
||||||
return dayCell;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Chronology getPrimaryChronology() {
|
|
||||||
return this.datePicker.getChronology();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Locale getLocale() {
|
|
||||||
return SettingsManager.settings().languageProperty().get().getLocale();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected boolean isValidDate(Chronology chrono, LocalDate date, int offset, ChronoUnit unit) {
|
|
||||||
return date != null && this.isValidDate(chrono, date.plus(offset, unit));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected boolean isValidDate(Chronology chrono, LocalDate date) {
|
|
||||||
try {
|
|
||||||
if (date != null) {
|
|
||||||
chrono.date(date);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (DateTimeException var4) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
//
|
|
||||||
// Source code recreated from a .class file by IntelliJ IDEA
|
|
||||||
// (powered by Fernflower decompiler)
|
|
||||||
//
|
|
||||||
|
|
||||||
package com.jfoenix.skins;
|
|
||||||
|
|
||||||
import com.jfoenix.adapters.ReflectionHelper;
|
|
||||||
import com.jfoenix.adapters.skins.DatePickerSkin;
|
|
||||||
import com.jfoenix.controls.JFXDatePicker;
|
|
||||||
import com.jfoenix.controls.JFXDialog;
|
|
||||||
import com.jfoenix.controls.JFXDialog.DialogTransition;
|
|
||||||
import javafx.beans.value.ChangeListener;
|
|
||||||
import javafx.event.ActionEvent;
|
|
||||||
import javafx.geometry.Insets;
|
|
||||||
import javafx.geometry.Pos;
|
|
||||||
import javafx.scene.Node;
|
|
||||||
import javafx.scene.control.TextField;
|
|
||||||
import javafx.scene.layout.*;
|
|
||||||
import javafx.scene.paint.Color;
|
|
||||||
import org.jackhuang.hmcl.ui.SVG;
|
|
||||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
|
||||||
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.YearMonth;
|
|
||||||
import java.util.Stack;
|
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
|
||||||
|
|
||||||
public class JFXDatePickerSkin extends DatePickerSkin {
|
|
||||||
public static final String PROPERTY_DATE_PICKER_CONTENTS = JFXDatePicker.class.getName() + ".dialog.contents";
|
|
||||||
|
|
||||||
private final JFXDatePicker jfxDatePicker;
|
|
||||||
private TextField displayNode;
|
|
||||||
private JFXDatePickerContent content;
|
|
||||||
private JFXDialog dialog;
|
|
||||||
|
|
||||||
public JFXDatePickerSkin(JFXDatePicker datePicker) {
|
|
||||||
super(datePicker);
|
|
||||||
this.jfxDatePicker = datePicker;
|
|
||||||
|
|
||||||
try {
|
|
||||||
Object expressionHelper = ReflectionHelper.getFieldContent(datePicker.focusedProperty().getClass().getSuperclass(), datePicker.focusedProperty(), "helper");
|
|
||||||
ChangeListener<Boolean>[] changeListeners = ReflectionHelper.getFieldContent(expressionHelper, "changeListeners");
|
|
||||||
|
|
||||||
int i = changeListeners.length - 1;
|
|
||||||
if (i >= 0) {
|
|
||||||
while (i >= 0 && changeListeners[i] == null) {
|
|
||||||
--i;
|
|
||||||
}
|
|
||||||
if (i >= 0) datePicker.focusedProperty().removeListener(changeListeners[i]);
|
|
||||||
}
|
|
||||||
} catch (NullPointerException e) {
|
|
||||||
LOG.warning("Cannot remove focusedProperty listener", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
datePicker.focusedProperty().addListener((obj, oldVal, newVal) -> {
|
|
||||||
if (this.getEditor() != null && !newVal) {
|
|
||||||
this.setTextFromTextFieldIntoComboBoxValue2();
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
var arrowBox = new HBox(new RipplerContainer(SVG.EDIT_CALENDAR.createIcon(30)));
|
|
||||||
arrowBox.setAlignment(Pos.CENTER);
|
|
||||||
this.setArrow(arrowBox);
|
|
||||||
this.getArrowButton().getChildren().setAll(arrowBox);
|
|
||||||
this.getArrowButton().setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY)));
|
|
||||||
this.getArrowButton().setPadding(new Insets(1.0F, 8.0F, 1.0F, 8.0F));
|
|
||||||
this.registerChangeListener2(datePicker.converterProperty(), "CONVERTER", this::updateDisplayNode2);
|
|
||||||
this.registerChangeListener2(datePicker.dayCellFactoryProperty(), "DAY_CELL_FACTORY", () -> {
|
|
||||||
this.updateDisplayNode2();
|
|
||||||
this.content = null;
|
|
||||||
this.setPopup2(null);
|
|
||||||
});
|
|
||||||
this.registerChangeListener2(datePicker.editorProperty(), "EDITOR", this::getEditableInputNode2);
|
|
||||||
this.registerChangeListener2(datePicker.showingProperty(), "SHOWING", () -> {
|
|
||||||
if (this.jfxDatePicker.isShowing()) {
|
|
||||||
if (this.content != null) {
|
|
||||||
LocalDate date = this.jfxDatePicker.getValue();
|
|
||||||
this.content.displayedYearMonthProperty().set(date != null ? YearMonth.from(date) : YearMonth.now());
|
|
||||||
this.content.updateValues();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.show();
|
|
||||||
} else {
|
|
||||||
this.hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
this.registerChangeListener2(datePicker.showWeekNumbersProperty(), "SHOW_WEEK_NUMBERS", () -> {
|
|
||||||
if (this.content != null) {
|
|
||||||
this.content.updateContentGrid();
|
|
||||||
this.content.updateWeekNumberDateCells();
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
this.registerChangeListener2(datePicker.valueProperty(), "VALUE", () -> {
|
|
||||||
this.updateDisplayNode2();
|
|
||||||
this.jfxDatePicker.fireEvent(new ActionEvent());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public JFXDatePickerContent getPopupContent() {
|
|
||||||
if (this.content == null) {
|
|
||||||
this.content = new JFXDatePickerContent(this.jfxDatePicker);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.content;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void show() {
|
|
||||||
if (!((JFXDatePicker) this.getSkinnable()).isOverLay()) {
|
|
||||||
super.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.content != null) {
|
|
||||||
this.content.init();
|
|
||||||
this.content.clearFocus();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (((JFXDatePicker) this.getSkinnable()).isOverLay() && this.dialog == null) {
|
|
||||||
StackPane dialogParent = this.jfxDatePicker.getDialogParent();
|
|
||||||
if (dialogParent == null) {
|
|
||||||
dialogParent = (StackPane) this.getSkinnable().getScene().getRoot();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.dialog = new JFXDialog(dialogParent, this.getPopupContent(), DialogTransition.CENTER, true) {
|
|
||||||
@Override
|
|
||||||
public void close() {
|
|
||||||
super.close();
|
|
||||||
Object o = getDialogContainer().getProperties().get(PROPERTY_DATE_PICKER_CONTENTS);
|
|
||||||
if (o instanceof Stack<?> stack) {
|
|
||||||
stack.pop();
|
|
||||||
}
|
|
||||||
jfxDatePicker.hide();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.getPopupContent().addEventHandler(DialogCloseEvent.CLOSE, e -> this.dialog.close());
|
|
||||||
getArrowButton().setOnMouseClicked((click) -> {
|
|
||||||
if (((JFXDatePicker) this.getSkinnable()).isOverLay()) {
|
|
||||||
StackPane parent = this.jfxDatePicker.getDialogParent();
|
|
||||||
if (parent == null) {
|
|
||||||
parent = (StackPane) this.getSkinnable().getScene().getRoot();
|
|
||||||
}
|
|
||||||
Stack<JFXDatePickerContent> contentStack = (Stack<JFXDatePickerContent>) parent.getProperties().computeIfAbsent(PROPERTY_DATE_PICKER_CONTENTS, k -> new Stack<>());
|
|
||||||
contentStack.push(this.getPopupContent());
|
|
||||||
|
|
||||||
this.getPopupContent().valueProperty.set(this.getSkinnable().getValue());
|
|
||||||
this.dialog.show(parent);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
JFXDialog getDialog() {
|
|
||||||
return this.dialog;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Node getDisplayNode() {
|
|
||||||
if (this.displayNode == null) {
|
|
||||||
this.displayNode = this.getEditableInputNode2();
|
|
||||||
this.displayNode.getStyleClass().add("date-picker-display-node");
|
|
||||||
this.updateDisplayNode2();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.displayNode.setEditable(this.jfxDatePicker.isEditable());
|
|
||||||
return this.displayNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void syncWithAutoUpdate() {
|
|
||||||
if (!this.getPopup2().isShowing() && this.jfxDatePicker.isShowing()) {
|
|
||||||
this.jfxDatePicker.hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,242 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.jfoenix.skins;
|
|
||||||
|
|
||||||
import com.jfoenix.adapters.ReflectionHelper;
|
|
||||||
import com.jfoenix.controls.behavior.JFXGenericPickerBehavior;
|
|
||||||
import com.sun.javafx.binding.ExpressionHelper;
|
|
||||||
import com.sun.javafx.event.EventHandlerManager;
|
|
||||||
import com.sun.javafx.stage.WindowEventDispatcher;
|
|
||||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
|
||||||
import javafx.beans.property.ReadOnlyBooleanPropertyBase;
|
|
||||||
import javafx.beans.value.ChangeListener;
|
|
||||||
import javafx.event.EventHandler;
|
|
||||||
import javafx.event.EventType;
|
|
||||||
import javafx.scene.control.ComboBoxBase;
|
|
||||||
import javafx.scene.control.PopupControl;
|
|
||||||
import javafx.scene.control.TextField;
|
|
||||||
import javafx.scene.control.skin.ComboBoxBaseSkin;
|
|
||||||
import javafx.scene.control.skin.ComboBoxPopupControl;
|
|
||||||
import javafx.scene.input.MouseEvent;
|
|
||||||
import javafx.scene.layout.Pane;
|
|
||||||
import javafx.stage.Window;
|
|
||||||
|
|
||||||
import java.lang.invoke.MethodHandles;
|
|
||||||
import java.lang.invoke.VarHandle;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.function.BiConsumer;
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
import java.util.function.Function;
|
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
|
||||||
|
|
||||||
public abstract class JFXGenericPickerSkin<T> extends ComboBoxPopupControl<T> {
|
|
||||||
|
|
||||||
private final EventHandler<MouseEvent> mouseEnteredEventHandler;
|
|
||||||
private final EventHandler<MouseEvent> mousePressedEventHandler;
|
|
||||||
private final EventHandler<MouseEvent> mouseReleasedEventHandler;
|
|
||||||
private final EventHandler<MouseEvent> mouseExitedEventHandler;
|
|
||||||
|
|
||||||
protected JFXGenericPickerBehavior<T> behavior;
|
|
||||||
|
|
||||||
// reference of the arrow button node in getChildren (not the actual field)
|
|
||||||
protected Pane arrowButton;
|
|
||||||
protected PopupControl popup;
|
|
||||||
|
|
||||||
public JFXGenericPickerSkin(ComboBoxBase<T> comboBoxBase) {
|
|
||||||
super(comboBoxBase);
|
|
||||||
behavior = new JFXGenericPickerBehavior<T>(comboBoxBase);
|
|
||||||
|
|
||||||
removeParentFakeFocusListener(comboBoxBase);
|
|
||||||
|
|
||||||
this.mouseEnteredEventHandler = event -> behavior.mouseEntered(event);
|
|
||||||
this.mousePressedEventHandler = event -> {
|
|
||||||
behavior.mousePressed(event);
|
|
||||||
event.consume();
|
|
||||||
};
|
|
||||||
this.mouseReleasedEventHandler = event -> {
|
|
||||||
behavior.mouseReleased(event);
|
|
||||||
event.consume();
|
|
||||||
};
|
|
||||||
this.mouseExitedEventHandler = event -> behavior.mouseExited(event);
|
|
||||||
|
|
||||||
arrowButton = (Pane) getChildren().get(0);
|
|
||||||
|
|
||||||
parentArrowEventHandlerTerminator.accept("mouseEnteredEventHandler", MouseEvent.MOUSE_ENTERED);
|
|
||||||
parentArrowEventHandlerTerminator.accept("mousePressedEventHandler", MouseEvent.MOUSE_PRESSED);
|
|
||||||
parentArrowEventHandlerTerminator.accept("mouseReleasedEventHandler", MouseEvent.MOUSE_RELEASED);
|
|
||||||
parentArrowEventHandlerTerminator.accept("mouseExitedEventHandler", MouseEvent.MOUSE_EXITED);
|
|
||||||
this.unregisterChangeListeners(comboBoxBase.editableProperty());
|
|
||||||
|
|
||||||
updateArrowButtonListeners();
|
|
||||||
registerChangeListener(comboBoxBase.editableProperty(), obs -> {
|
|
||||||
updateArrowButtonListeners();
|
|
||||||
reflectUpdateDisplayArea();
|
|
||||||
});
|
|
||||||
|
|
||||||
removeParentPopupHandlers();
|
|
||||||
|
|
||||||
popup = ReflectionHelper.getFieldContent(ComboBoxPopupControl.class, this, "popup");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
if (this.behavior != null) {
|
|
||||||
this.behavior.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/***************************************************************************
|
|
||||||
* *
|
|
||||||
* Reflections internal API *
|
|
||||||
* *
|
|
||||||
**************************************************************************/
|
|
||||||
|
|
||||||
private final BiConsumer<String, EventType<?>> parentArrowEventHandlerTerminator = (handlerName, eventType) -> {
|
|
||||||
try {
|
|
||||||
EventHandler handler = ReflectionHelper.getFieldContent(ComboBoxBaseSkin.class, this, handlerName);
|
|
||||||
arrowButton.removeEventHandler(eventType, handler);
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private static final VarHandle READ_ONLY_BOOLEAN_PROPERTY_BASE_HELPER =
|
|
||||||
findVarHandle(ReadOnlyBooleanPropertyBase.class, "helper", ExpressionHelper.class);
|
|
||||||
|
|
||||||
/// @author Glavo
|
|
||||||
private static VarHandle findVarHandle(Class<?> targetClass, String fieldName, Class<?> type) {
|
|
||||||
try {
|
|
||||||
return MethodHandles.privateLookupIn(targetClass, MethodHandles.lookup()).findVarHandle(targetClass, fieldName, type);
|
|
||||||
} catch (NoSuchFieldException | IllegalAccessException e) {
|
|
||||||
LOG.warning("Failed to get var handle", e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void removeParentFakeFocusListener(ComboBoxBase<T> comboBoxBase) {
|
|
||||||
// handle FakeFocusField cast exception
|
|
||||||
try {
|
|
||||||
final ReadOnlyBooleanProperty focusedProperty = comboBoxBase.focusedProperty();
|
|
||||||
//noinspection unchecked
|
|
||||||
ExpressionHelper<Boolean> value = (ExpressionHelper<Boolean>) READ_ONLY_BOOLEAN_PROPERTY_BASE_HELPER.get(focusedProperty);
|
|
||||||
ChangeListener<? super Boolean>[] changeListeners = ReflectionHelper.getFieldContent(value.getClass(), value, "changeListeners");
|
|
||||||
// remove parent focus listener to prevent editor class cast exception
|
|
||||||
for (int i = changeListeners.length - 1; i > 0; i--) {
|
|
||||||
if (changeListeners[i] != null && changeListeners[i].getClass().getName().contains("ComboBoxPopupControl")) {
|
|
||||||
focusedProperty.removeListener(changeListeners[i]);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void removeParentPopupHandlers() {
|
|
||||||
try {
|
|
||||||
PopupControl popup = ReflectionHelper.invoke(ComboBoxPopupControl.class, this, "getPopup");
|
|
||||||
popup.setOnAutoHide(event -> behavior.onAutoHide(popup));
|
|
||||||
WindowEventDispatcher dispatcher = ReflectionHelper.invoke(Window.class, popup, "getInternalEventDispatcher");
|
|
||||||
Map compositeEventHandlersMap = ReflectionHelper.getFieldContent(EventHandlerManager.class, dispatcher.getEventHandlerManager(), "eventHandlerMap");
|
|
||||||
compositeEventHandlersMap.remove(MouseEvent.MOUSE_CLICKED);
|
|
||||||
// CompositeEventHandler compositeEventHandler = (CompositeEventHandler) compositeEventHandlersMap.get(MouseEvent.MOUSE_CLICKED);
|
|
||||||
// Object obj = fieldConsumer.apply(()->CompositeEventHandler.class.getDeclaredField("firstRecord"),compositeEventHandler);
|
|
||||||
// EventHandler handler = (EventHandler) fieldConsumer.apply(() -> obj.getClass().getDeclaredField("eventHandler"), obj);
|
|
||||||
// popup.removeEventHandler(MouseEvent.MOUSE_CLICKED, handler);
|
|
||||||
popup.addEventHandler(MouseEvent.MOUSE_CLICKED, click -> behavior.onAutoHide(popup));
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateArrowButtonListeners() {
|
|
||||||
if (getSkinnable().isEditable()) {
|
|
||||||
arrowButton.addEventHandler(MouseEvent.MOUSE_ENTERED, mouseEnteredEventHandler);
|
|
||||||
arrowButton.addEventHandler(MouseEvent.MOUSE_PRESSED, mousePressedEventHandler);
|
|
||||||
arrowButton.addEventHandler(MouseEvent.MOUSE_RELEASED, mouseReleasedEventHandler);
|
|
||||||
arrowButton.addEventHandler(MouseEvent.MOUSE_EXITED, mouseExitedEventHandler);
|
|
||||||
} else {
|
|
||||||
arrowButton.removeEventHandler(MouseEvent.MOUSE_ENTERED, mouseEnteredEventHandler);
|
|
||||||
arrowButton.removeEventHandler(MouseEvent.MOUSE_PRESSED, mousePressedEventHandler);
|
|
||||||
arrowButton.removeEventHandler(MouseEvent.MOUSE_RELEASED, mouseReleasedEventHandler);
|
|
||||||
arrowButton.removeEventHandler(MouseEvent.MOUSE_EXITED, mouseExitedEventHandler);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/***************************************************************************
|
|
||||||
* *
|
|
||||||
* Reflections internal API for ComboBoxPopupControl *
|
|
||||||
* *
|
|
||||||
**************************************************************************/
|
|
||||||
|
|
||||||
private final HashMap<String, Method> parentCachedMethods = new HashMap<>();
|
|
||||||
|
|
||||||
Function<String, Method> methodSupplier = name -> {
|
|
||||||
if (!parentCachedMethods.containsKey(name)) {
|
|
||||||
try {
|
|
||||||
Method method = ComboBoxPopupControl.class.getDeclaredMethod(name);
|
|
||||||
method.setAccessible(true);
|
|
||||||
parentCachedMethods.put(name, method);
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return parentCachedMethods.get(name);
|
|
||||||
};
|
|
||||||
|
|
||||||
final Consumer<Method> methodInvoker = method -> {
|
|
||||||
try {
|
|
||||||
method.invoke(this);
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
final Function<Method, Object> methodReturnInvoker = method -> {
|
|
||||||
try {
|
|
||||||
return method.invoke(this);
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
protected void reflectUpdateDisplayArea() {
|
|
||||||
methodInvoker.accept(methodSupplier.apply("updateDisplayArea"));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void reflectSetTextFromTextFieldIntoComboBoxValue() {
|
|
||||||
methodInvoker.accept(methodSupplier.apply("setTextFromTextFieldIntoComboBoxValue"));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected TextField reflectGetEditableInputNode() {
|
|
||||||
return (TextField) methodReturnInvoker.apply(methodSupplier.apply("getEditableInputNode"));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void reflectUpdateDisplayNode() {
|
|
||||||
methodInvoker.accept(methodSupplier.apply("updateDisplayNode"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -20,23 +20,26 @@
|
|||||||
*/
|
*/
|
||||||
package com.jfoenix.skins;
|
package com.jfoenix.skins;
|
||||||
|
|
||||||
|
import com.jfoenix.adapters.skins.ListViewSkin;
|
||||||
import com.jfoenix.controls.JFXListView;
|
import com.jfoenix.controls.JFXListView;
|
||||||
import com.jfoenix.effects.JFXDepthManager;
|
import com.jfoenix.effects.JFXDepthManager;
|
||||||
import javafx.scene.control.ListCell;
|
import javafx.scene.control.ListCell;
|
||||||
import javafx.scene.control.skin.ListViewSkin;
|
|
||||||
import javafx.scene.control.skin.VirtualFlow;
|
import javafx.scene.control.skin.VirtualFlow;
|
||||||
|
import javafx.scene.layout.Region;
|
||||||
import org.jackhuang.hmcl.ui.FXUtils;
|
import org.jackhuang.hmcl.ui.FXUtils;
|
||||||
|
|
||||||
|
// https://github.com/HMCL-dev/HMCL/issues/4720
|
||||||
public class JFXListViewSkin<T> extends ListViewSkin<T> {
|
public class JFXListViewSkin<T> extends ListViewSkin<T> {
|
||||||
|
|
||||||
|
private final VirtualFlow<ListCell<T>> flow;
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
public JFXListViewSkin(final JFXListView<T> listView) {
|
public JFXListViewSkin(final JFXListView<T> listView) {
|
||||||
super(listView);
|
super(listView);
|
||||||
VirtualFlow<ListCell<T>> flow = getVirtualFlow();
|
flow = (VirtualFlow<ListCell<T>>) getChildren().get(0);
|
||||||
FXUtils.onChangeAndOperate(listView.depthProperty(), depth -> JFXDepthManager.setDepth(flow, depth.intValue()));
|
JFXDepthManager.setDepth(flow, listView.depthProperty().get());
|
||||||
|
listView.depthProperty().addListener((o, oldVal, newVal) -> JFXDepthManager.setDepth(flow, newVal));
|
||||||
if (!Boolean.TRUE.equals(listView.getProperties().get("no-smooth-scrolling"))) {
|
FXUtils.smoothScrolling(flow);
|
||||||
FXUtils.smoothScrolling(flow);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -44,4 +47,47 @@ public class JFXListViewSkin<T> extends ListViewSkin<T> {
|
|||||||
return 200;
|
return 200;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
|
||||||
|
final int itemsCount = getSkinnable().getItems().size();
|
||||||
|
if (getSkinnable().maxHeightProperty().isBound() || itemsCount <= 0) {
|
||||||
|
return super.computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
|
||||||
|
}
|
||||||
|
|
||||||
|
final double fixedCellSize = getSkinnable().getFixedCellSize();
|
||||||
|
double computedHeight = fixedCellSize != Region.USE_COMPUTED_SIZE ?
|
||||||
|
fixedCellSize * itemsCount + snapVerticalInsets() : estimateHeight();
|
||||||
|
double height = super.computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
|
||||||
|
if (height > computedHeight) {
|
||||||
|
height = computedHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getSkinnable().getMaxHeight() > 0 && computedHeight > getSkinnable().getMaxHeight()) {
|
||||||
|
return getSkinnable().getMaxHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double estimateHeight() {
|
||||||
|
// compute the border/padding for the list
|
||||||
|
double borderWidth = snapVerticalInsets();
|
||||||
|
// compute the gap between list cells
|
||||||
|
|
||||||
|
JFXListView<T> listview = (JFXListView<T>) getSkinnable();
|
||||||
|
double gap = listview.isExpanded() ? ((JFXListView<T>) getSkinnable()).getVerticalGap() * (getSkinnable().getItems()
|
||||||
|
.size()) : 0;
|
||||||
|
// compute the height of each list cell
|
||||||
|
double cellsHeight = 0;
|
||||||
|
for (int i = 0; i < flow.getCellCount(); i++) {
|
||||||
|
ListCell<T> cell = flow.getCell(i);
|
||||||
|
cellsHeight += cell.getHeight();
|
||||||
|
}
|
||||||
|
return cellsHeight + gap + borderWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double snapVerticalInsets() {
|
||||||
|
return getSkinnable().snappedBottomInset() + getSkinnable().snappedTopInset();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2016 JFoenix
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
* this software and associated documentation files (the "Software"), to deal in
|
|
||||||
* the Software without restriction, including without limitation the rights to
|
|
||||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
||||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
|
||||||
* subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
||||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
||||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
||||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.jfoenix.skins;
|
|
||||||
|
|
||||||
import com.jfoenix.controls.JFXPopup;
|
|
||||||
import com.jfoenix.controls.JFXPopup.PopupHPosition;
|
|
||||||
import com.jfoenix.controls.JFXPopup.PopupVPosition;
|
|
||||||
import com.jfoenix.effects.JFXDepthManager;
|
|
||||||
import javafx.animation.*;
|
|
||||||
import javafx.animation.Animation.Status;
|
|
||||||
import javafx.scene.Node;
|
|
||||||
import javafx.scene.control.Skin;
|
|
||||||
import javafx.scene.layout.*;
|
|
||||||
import javafx.scene.transform.Scale;
|
|
||||||
import javafx.util.Duration;
|
|
||||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
|
||||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
|
||||||
|
|
||||||
/// # Material Design Popup Skin
|
|
||||||
/// TODO: REWORK
|
|
||||||
///
|
|
||||||
/// @author Shadi Shaheen
|
|
||||||
/// @version 2.0
|
|
||||||
/// @since 2017-03-01
|
|
||||||
public class JFXPopupSkin implements Skin<JFXPopup> {
|
|
||||||
|
|
||||||
protected JFXPopup control;
|
|
||||||
protected StackPane container = new StackPane();
|
|
||||||
protected Region popupContent;
|
|
||||||
protected Node root;
|
|
||||||
|
|
||||||
private Animation animation;
|
|
||||||
protected Scale scale;
|
|
||||||
|
|
||||||
public JFXPopupSkin(JFXPopup control) {
|
|
||||||
this.control = control;
|
|
||||||
// set scale y to 0.01 instead of 0 to allow layout of the content,
|
|
||||||
// otherwise it will cause exception in traverse engine, when focusing the 1st node
|
|
||||||
scale = new Scale(1.0, 0.01, 0, 0);
|
|
||||||
popupContent = control.getPopupContent();
|
|
||||||
container.getStyleClass().add("jfx-popup-container");
|
|
||||||
container.getChildren().add(popupContent);
|
|
||||||
container.getTransforms().add(scale);
|
|
||||||
container.setOpacity(0);
|
|
||||||
root = JFXDepthManager.createMaterialNode(container, 4);
|
|
||||||
animation = AnimationUtils.isAnimationEnabled() ? getAnimation() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void reset(PopupVPosition vAlign, PopupHPosition hAlign, double offsetX, double offsetY) {
|
|
||||||
// postion the popup according to its animation
|
|
||||||
scale.setPivotX(hAlign == PopupHPosition.RIGHT ? container.getWidth() : 0);
|
|
||||||
scale.setPivotY(vAlign == PopupVPosition.BOTTOM ? container.getHeight() : 0);
|
|
||||||
control.setX(control.getX() + (hAlign == PopupHPosition.RIGHT ? -container.getWidth() + offsetX : offsetX));
|
|
||||||
control.setY(control.getY() + (vAlign == PopupVPosition.BOTTOM ? -container.getHeight() + offsetY : offsetY));
|
|
||||||
}
|
|
||||||
|
|
||||||
public final void animate() {
|
|
||||||
if (animation != null) {
|
|
||||||
if (animation.getStatus() == Status.STOPPED) {
|
|
||||||
container.setOpacity(1);
|
|
||||||
animation.playFromStart();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
container.setOpacity(1);
|
|
||||||
popupContent.setOpacity(1);
|
|
||||||
scale.setX(1.0);
|
|
||||||
scale.setY(1.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public JFXPopup getSkinnable() {
|
|
||||||
return control;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Node getNode() {
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void dispose() {
|
|
||||||
if (animation != null) {
|
|
||||||
animation.stop();
|
|
||||||
animation = null;
|
|
||||||
}
|
|
||||||
container = null;
|
|
||||||
control = null;
|
|
||||||
popupContent = null;
|
|
||||||
root = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Animation getAnimation() {
|
|
||||||
Interpolator interpolator = Motion.EASE;
|
|
||||||
return new Timeline(
|
|
||||||
new KeyFrame(
|
|
||||||
Duration.ZERO,
|
|
||||||
new KeyValue(popupContent.opacityProperty(), 0, interpolator),
|
|
||||||
new KeyValue(scale.xProperty(), 0, interpolator),
|
|
||||||
new KeyValue(scale.yProperty(), 0, interpolator)
|
|
||||||
),
|
|
||||||
new KeyFrame(Motion.SHORT4,
|
|
||||||
new KeyValue(popupContent.opacityProperty(), 0, interpolator),
|
|
||||||
new KeyValue(scale.xProperty(), 1, interpolator)
|
|
||||||
),
|
|
||||||
new KeyFrame(Motion.MEDIUM2,
|
|
||||||
new KeyValue(popupContent.opacityProperty(), 1, interpolator),
|
|
||||||
new KeyValue(scale.yProperty(), 1, interpolator)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void init() {
|
|
||||||
if (animation != null)
|
|
||||||
animation.stop();
|
|
||||||
container.setOpacity(0);
|
|
||||||
scale.setX(1.0);
|
|
||||||
scale.setY(0.01);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,225 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.jfoenix.skins;
|
|
||||||
|
|
||||||
import com.jfoenix.controls.JFXProgressBar;
|
|
||||||
import com.jfoenix.utils.TreeShowingProperty;
|
|
||||||
import javafx.animation.*;
|
|
||||||
import javafx.scene.Node;
|
|
||||||
import javafx.scene.control.SkinBase;
|
|
||||||
import javafx.scene.layout.*;
|
|
||||||
import javafx.scene.shape.Rectangle;
|
|
||||||
import javafx.util.Duration;
|
|
||||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
|
||||||
|
|
||||||
/// # Material Design ProgressBar Skin
|
|
||||||
///
|
|
||||||
/// @author Shadi Shaheen
|
|
||||||
/// @version 2.0
|
|
||||||
/// @since 2017-10-06
|
|
||||||
public class JFXProgressBarSkin extends SkinBase<JFXProgressBar> {
|
|
||||||
|
|
||||||
private static final double DEFAULT_HEIGHT = 4;
|
|
||||||
|
|
||||||
private final StackPane track;
|
|
||||||
private final StackPane bar;
|
|
||||||
private final Rectangle clip;
|
|
||||||
private Animation transition;
|
|
||||||
private final TreeShowingProperty treeShowingProperty;
|
|
||||||
private double fullWidth;
|
|
||||||
|
|
||||||
public JFXProgressBarSkin(JFXProgressBar control) {
|
|
||||||
super(control);
|
|
||||||
|
|
||||||
this.treeShowingProperty = new TreeShowingProperty(control);
|
|
||||||
|
|
||||||
registerChangeListener(treeShowingProperty, obs -> updateProgress(false));
|
|
||||||
registerChangeListener(control.progressProperty(), obs -> updateProgress(true));
|
|
||||||
|
|
||||||
track = new StackPane();
|
|
||||||
track.getStyleClass().setAll("track");
|
|
||||||
|
|
||||||
bar = new StackPane();
|
|
||||||
bar.getStyleClass().setAll("bar");
|
|
||||||
|
|
||||||
clip = new Rectangle();
|
|
||||||
clip.setArcWidth(DEFAULT_HEIGHT);
|
|
||||||
clip.setArcHeight(DEFAULT_HEIGHT);
|
|
||||||
bar.setClip(clip);
|
|
||||||
|
|
||||||
getChildren().setAll(track, bar);
|
|
||||||
|
|
||||||
getSkinnable().requestLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public double computeBaselineOffset(double topInset, double rightInset, double bottomInset, double leftInset) {
|
|
||||||
return Node.BASELINE_OFFSET_SAME_AS_HEIGHT;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
|
|
||||||
return Math.max(100, leftInset + bar.prefWidth(getSkinnable().getWidth()) + rightInset);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
|
|
||||||
return topInset + DEFAULT_HEIGHT + bottomInset;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
|
|
||||||
return getSkinnable().prefWidth(height);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
|
|
||||||
return getSkinnable().prefHeight(width);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void layoutChildren(double x, double y, double w, double h) {
|
|
||||||
track.resizeRelocate(x, y, w, h);
|
|
||||||
bar.resizeRelocate(x, y, w, h);
|
|
||||||
|
|
||||||
clip.relocate(0, 0);
|
|
||||||
clip.setWidth(0);
|
|
||||||
clip.setHeight(h);
|
|
||||||
clip.setTranslateX(0);
|
|
||||||
|
|
||||||
fullWidth = w;
|
|
||||||
|
|
||||||
clearAnimation();
|
|
||||||
updateProgress(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean wasIndeterminate = false;
|
|
||||||
|
|
||||||
private void updateProgress(boolean playProgressAnimation) {
|
|
||||||
double progress = Math.min(getSkinnable().getProgress(), 1.0);
|
|
||||||
boolean isIndeterminate = progress < 0.0;
|
|
||||||
boolean isTreeShowing = treeShowingProperty.get();
|
|
||||||
|
|
||||||
if (isIndeterminate != wasIndeterminate) {
|
|
||||||
wasIndeterminate = isIndeterminate;
|
|
||||||
clearAnimation();
|
|
||||||
clip.setTranslateX(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isIndeterminate) { // indeterminate
|
|
||||||
if (isTreeShowing) {
|
|
||||||
if (transition == null) {
|
|
||||||
transition = createIndeterminateTransition();
|
|
||||||
transition.playFromStart();
|
|
||||||
} else {
|
|
||||||
transition.play();
|
|
||||||
}
|
|
||||||
} else if (transition != null) {
|
|
||||||
transition.pause();
|
|
||||||
}
|
|
||||||
} else { // determinate
|
|
||||||
clearAnimation();
|
|
||||||
if (isTreeShowing && playProgressAnimation
|
|
||||||
&& AnimationUtils.isAnimationEnabled()
|
|
||||||
&& getSkinnable().isSmoothProgress()) {
|
|
||||||
transition = createDeterminateTransition(progress);
|
|
||||||
transition.playFromStart();
|
|
||||||
} else {
|
|
||||||
clip.setWidth(computeBarWidth(progress));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final Duration INDETERMINATE_DURATION = Duration.seconds(1);
|
|
||||||
|
|
||||||
private Transition createIndeterminateTransition() {
|
|
||||||
double minWidth = 0;
|
|
||||||
double maxWidth = fullWidth * 0.4;
|
|
||||||
Transition transition = new Transition() {
|
|
||||||
{
|
|
||||||
setInterpolator(Interpolator.LINEAR);
|
|
||||||
setCycleDuration(INDETERMINATE_DURATION);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void interpolate(double frac) {
|
|
||||||
double currentWidth;
|
|
||||||
|
|
||||||
if (frac <= 0.5) {
|
|
||||||
currentWidth = Interpolator.EASE_IN.interpolate(minWidth, maxWidth, frac / 0.5);
|
|
||||||
} else {
|
|
||||||
currentWidth = Interpolator.EASE_OUT.interpolate(maxWidth, minWidth, (frac - 0.5) / 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
double targetCenter;
|
|
||||||
if (frac <= 0.1) {
|
|
||||||
targetCenter = 0.0;
|
|
||||||
} else if (frac >= 0.9) {
|
|
||||||
targetCenter = fullWidth;
|
|
||||||
} else {
|
|
||||||
targetCenter = ((frac - 0.1) / 0.8) * fullWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
clip.setWidth(currentWidth);
|
|
||||||
clip.setTranslateX(targetCenter - currentWidth / 2.0);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
transition.setCycleCount(Timeline.INDEFINITE);
|
|
||||||
return transition;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final Duration DETERMINATE_DURATION = Duration.seconds(0.2);
|
|
||||||
|
|
||||||
private Timeline createDeterminateTransition(double targetProgress) {
|
|
||||||
Timeline timeline = new Timeline(
|
|
||||||
new KeyFrame(Duration.ZERO,
|
|
||||||
new KeyValue(clip.widthProperty(), clip.getWidth(), Interpolator.EASE_OUT)),
|
|
||||||
new KeyFrame(DETERMINATE_DURATION,
|
|
||||||
new KeyValue(clip.widthProperty(), computeBarWidth(targetProgress), Interpolator.EASE_OUT))
|
|
||||||
);
|
|
||||||
timeline.setOnFinished(e -> {
|
|
||||||
if (transition == timeline) {
|
|
||||||
transition = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return timeline;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void clearAnimation() {
|
|
||||||
if (transition != null) {
|
|
||||||
transition.stop();
|
|
||||||
transition = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
treeShowingProperty.dispose();
|
|
||||||
clearAnimation();
|
|
||||||
}
|
|
||||||
|
|
||||||
private double computeBarWidth(double progress) {
|
|
||||||
assert progress >= 0 && progress <= 1;
|
|
||||||
double barWidth = ((int) fullWidth * 2 * progress) / 2.0;
|
|
||||||
return progress > 0 ? Math.max(barWidth, DEFAULT_HEIGHT) : barWidth;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,179 +0,0 @@
|
|||||||
//
|
|
||||||
// Source code recreated from a .class file by IntelliJ IDEA
|
|
||||||
// (powered by FernFlower decompiler)
|
|
||||||
//
|
|
||||||
|
|
||||||
package com.jfoenix.skins;
|
|
||||||
|
|
||||||
import com.jfoenix.controls.JFXRadioButton;
|
|
||||||
import com.jfoenix.controls.JFXRippler;
|
|
||||||
import com.jfoenix.controls.JFXRippler.RipplerMask;
|
|
||||||
import javafx.animation.Interpolator;
|
|
||||||
import javafx.animation.KeyFrame;
|
|
||||||
import javafx.animation.KeyValue;
|
|
||||||
import javafx.animation.Timeline;
|
|
||||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
|
||||||
import javafx.geometry.HPos;
|
|
||||||
import javafx.geometry.Insets;
|
|
||||||
import javafx.geometry.VPos;
|
|
||||||
import javafx.scene.control.RadioButton;
|
|
||||||
import javafx.scene.control.skin.RadioButtonSkin;
|
|
||||||
import javafx.scene.layout.AnchorPane;
|
|
||||||
import javafx.scene.layout.StackPane;
|
|
||||||
import javafx.scene.paint.Color;
|
|
||||||
import javafx.scene.shape.Circle;
|
|
||||||
import javafx.scene.text.Text;
|
|
||||||
import javafx.util.Duration;
|
|
||||||
import org.jackhuang.hmcl.ui.FXUtils;
|
|
||||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
|
||||||
|
|
||||||
public class JFXRadioButtonSkin extends RadioButtonSkin {
|
|
||||||
private static final double PADDING = 15.0;
|
|
||||||
|
|
||||||
private boolean invalid = true;
|
|
||||||
private final JFXRippler rippler;
|
|
||||||
private final Circle radio;
|
|
||||||
private final Circle dot;
|
|
||||||
private Timeline timeline;
|
|
||||||
private final AnchorPane container = new AnchorPane();
|
|
||||||
private final double labelOffset = -10.0;
|
|
||||||
|
|
||||||
public JFXRadioButtonSkin(JFXRadioButton control) {
|
|
||||||
super(control);
|
|
||||||
double radioRadius = 7.0;
|
|
||||||
this.radio = new Circle(radioRadius);
|
|
||||||
this.radio.getStyleClass().setAll("radio");
|
|
||||||
this.radio.setStrokeWidth(2.0);
|
|
||||||
this.radio.setFill(Color.TRANSPARENT);
|
|
||||||
|
|
||||||
this.dot = new Circle(4);
|
|
||||||
this.dot.getStyleClass().setAll("dot");
|
|
||||||
this.dot.fillProperty().bind(control.selectedColorProperty());
|
|
||||||
this.dot.setScaleX(0.0);
|
|
||||||
this.dot.setScaleY(0.0);
|
|
||||||
|
|
||||||
StackPane boxContainer = new StackPane();
|
|
||||||
boxContainer.getChildren().addAll(this.radio, this.dot);
|
|
||||||
boxContainer.setPadding(new Insets(PADDING));
|
|
||||||
this.rippler = new JFXRippler(boxContainer, RipplerMask.CIRCLE);
|
|
||||||
this.container.getChildren().add(this.rippler);
|
|
||||||
AnchorPane.setRightAnchor(this.rippler, this.labelOffset);
|
|
||||||
|
|
||||||
this.updateChildren();
|
|
||||||
ReadOnlyBooleanProperty focusVisibleProperty = FXUtils.focusVisibleProperty(control);
|
|
||||||
if (focusVisibleProperty == null) {
|
|
||||||
focusVisibleProperty = control.focusedProperty();
|
|
||||||
}
|
|
||||||
|
|
||||||
focusVisibleProperty.addListener((o, oldVal, newVal) -> {
|
|
||||||
if (newVal) {
|
|
||||||
if (!this.getSkinnable().isPressed()) {
|
|
||||||
this.rippler.showOverlay();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.rippler.hideOverlay();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
control.pressedProperty().addListener((o, oldVal, newVal) -> this.rippler.hideOverlay());
|
|
||||||
this.registerChangeListener(control.selectedColorProperty(), ignored -> updateColors());
|
|
||||||
this.registerChangeListener(control.unSelectedColorProperty(), ignored -> updateColors());
|
|
||||||
this.registerChangeListener(control.selectedProperty(), ignored -> {
|
|
||||||
updateColors();
|
|
||||||
this.playAnimation();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void updateChildren() {
|
|
||||||
super.updateChildren();
|
|
||||||
if (this.radio != null) {
|
|
||||||
this.removeRadio();
|
|
||||||
this.getChildren().add(this.container);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void layoutChildren(double x, double y, double w, double h) {
|
|
||||||
RadioButton radioButton = this.getSkinnable();
|
|
||||||
double contWidth = this.snapSizeX(this.container.prefWidth(-1.0)) + (double) (this.invalid ? 2 : 0);
|
|
||||||
double contHeight = this.snapSizeY(this.container.prefHeight(-1.0)) + (double) (this.invalid ? 2 : 0);
|
|
||||||
double computeWidth = Math.min(radioButton.prefWidth(-1.0), radioButton.minWidth(-1.0)) + this.labelOffset + 2.0 * this.PADDING;
|
|
||||||
double labelWidth = Math.min(computeWidth - contWidth, w - this.snapSizeX(contWidth)) + this.labelOffset + 2.0 * PADDING;
|
|
||||||
double labelHeight = Math.min(radioButton.prefHeight(labelWidth), h);
|
|
||||||
double maxHeight = Math.max(contHeight, labelHeight);
|
|
||||||
double xOffset = computeXOffset(w, labelWidth + contWidth, radioButton.getAlignment().getHpos()) + x;
|
|
||||||
double yOffset = computeYOffset(h, maxHeight, radioButton.getAlignment().getVpos()) + x;
|
|
||||||
if (this.invalid) {
|
|
||||||
this.initializeComponents();
|
|
||||||
this.invalid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.layoutLabelInArea(xOffset + contWidth, yOffset, labelWidth, maxHeight, radioButton.getAlignment());
|
|
||||||
((Text) this.getChildren().get(this.getChildren().get(0) instanceof Text ? 0 : 1)).textProperty().set(this.getSkinnable().textProperty().get());
|
|
||||||
this.container.resize(this.snapSizeX(contWidth), this.snapSizeY(contHeight));
|
|
||||||
this.positionInArea(this.container, xOffset, yOffset, contWidth, maxHeight, 0.0, radioButton.getAlignment().getHpos(), radioButton.getAlignment().getVpos());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initializeComponents() {
|
|
||||||
this.updateColors();
|
|
||||||
this.playAnimation();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void playAnimation() {
|
|
||||||
if (AnimationUtils.isAnimationEnabled()) {
|
|
||||||
if (this.timeline == null) {
|
|
||||||
this.timeline = new Timeline(
|
|
||||||
new KeyFrame(Duration.ZERO,
|
|
||||||
new KeyValue(this.dot.scaleXProperty(), 0, Interpolator.EASE_BOTH),
|
|
||||||
new KeyValue(this.dot.scaleYProperty(), 0, Interpolator.EASE_BOTH)),
|
|
||||||
new KeyFrame(Duration.millis(200.0),
|
|
||||||
new KeyValue(this.dot.scaleXProperty(), 1, Interpolator.EASE_BOTH),
|
|
||||||
new KeyValue(this.dot.scaleYProperty(), 1, Interpolator.EASE_BOTH))
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.timeline.stop();
|
|
||||||
}
|
|
||||||
this.timeline.setRate(this.getSkinnable().isSelected() ? 1.0 : -1.0);
|
|
||||||
this.timeline.play();
|
|
||||||
} else {
|
|
||||||
double endScale = this.getSkinnable().isSelected() ? 1.0 : 0.0;
|
|
||||||
this.dot.setScaleX(endScale);
|
|
||||||
this.dot.setScaleY(endScale);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void removeRadio() {
|
|
||||||
this.getChildren().removeIf(node -> "radio".equals(node.getStyleClass().get(0)));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateColors() {
|
|
||||||
var control = (JFXRadioButton) getSkinnable();
|
|
||||||
boolean isSelected = control.isSelected();
|
|
||||||
Color unSelectedColor = control.getUnSelectedColor();
|
|
||||||
Color selectedColor = control.getSelectedColor();
|
|
||||||
rippler.setRipplerFill(isSelected ? selectedColor : unSelectedColor);
|
|
||||||
radio.setStroke(isSelected ? selectedColor : unSelectedColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
|
|
||||||
return super.computePrefWidth(height, topInset, rightInset, bottomInset, leftInset) + this.snapSizeX(this.radio.minWidth(-1.0)) + this.labelOffset + 2.0 * PADDING;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
|
|
||||||
return super.computePrefWidth(height, topInset, rightInset, bottomInset, leftInset) + this.snapSizeX(this.radio.prefWidth(-1.0)) + this.labelOffset + 2.0 * PADDING;
|
|
||||||
}
|
|
||||||
|
|
||||||
static double computeXOffset(double width, double contentWidth, HPos hpos) {
|
|
||||||
return switch (hpos) {
|
|
||||||
case LEFT -> 0.0;
|
|
||||||
case CENTER -> (width - contentWidth) / 2.0;
|
|
||||||
case RIGHT -> width - contentWidth;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static double computeYOffset(double height, double contentHeight, VPos vpos) {
|
|
||||||
return switch (vpos) {
|
|
||||||
case TOP, BASELINE -> 0.0;
|
|
||||||
case CENTER -> (height - contentHeight) / 2.0;
|
|
||||||
case BOTTOM -> height - contentHeight;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,291 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.jfoenix.skins;
|
|
||||||
|
|
||||||
import com.jfoenix.controls.JFXSlider;
|
|
||||||
import com.jfoenix.controls.JFXSlider.IndicatorPosition;
|
|
||||||
import javafx.animation.Interpolator;
|
|
||||||
import javafx.animation.KeyFrame;
|
|
||||||
import javafx.animation.KeyValue;
|
|
||||||
import javafx.animation.Timeline;
|
|
||||||
import javafx.beans.binding.Bindings;
|
|
||||||
import javafx.beans.property.DoubleProperty;
|
|
||||||
import javafx.collections.ListChangeListener;
|
|
||||||
import javafx.collections.ObservableList;
|
|
||||||
import javafx.css.PseudoClass;
|
|
||||||
import javafx.geometry.Orientation;
|
|
||||||
import javafx.scene.Node;
|
|
||||||
import javafx.scene.chart.NumberAxis;
|
|
||||||
import javafx.scene.control.Slider;
|
|
||||||
import javafx.scene.control.skin.SliderSkin;
|
|
||||||
import javafx.scene.input.MouseEvent;
|
|
||||||
import javafx.scene.layout.Pane;
|
|
||||||
import javafx.scene.layout.StackPane;
|
|
||||||
import javafx.scene.text.Text;
|
|
||||||
import javafx.util.Duration;
|
|
||||||
|
|
||||||
/// # Material Design Slider Skin
|
|
||||||
///
|
|
||||||
/// rework of JFXSliderSkin by extending Java SliderSkin
|
|
||||||
/// this solves padding and resizing issues
|
|
||||||
///
|
|
||||||
/// @author Shadi Shaheen
|
|
||||||
/// @version 1.0
|
|
||||||
/// @since 2016-03-09
|
|
||||||
public class JFXSliderSkin extends SliderSkin {
|
|
||||||
|
|
||||||
private static final PseudoClass MIN_VALUE = PseudoClass.getPseudoClass("min");
|
|
||||||
private static final PseudoClass MAX_VALUE = PseudoClass.getPseudoClass("max");
|
|
||||||
|
|
||||||
private final Pane mouseHandlerPane = new Pane();
|
|
||||||
private final Text sliderValue;
|
|
||||||
private final StackPane coloredTrack;
|
|
||||||
private final StackPane thumb;
|
|
||||||
private final StackPane track;
|
|
||||||
private final StackPane animatedThumb;
|
|
||||||
private NumberAxis tickLine;
|
|
||||||
|
|
||||||
private Timeline timeline;
|
|
||||||
|
|
||||||
private double indicatorRotation;
|
|
||||||
private double horizontalRotation;
|
|
||||||
private double shifting;
|
|
||||||
|
|
||||||
public JFXSliderSkin(JFXSlider slider) {
|
|
||||||
super(slider);
|
|
||||||
|
|
||||||
track = (StackPane) getSkinnable().lookup(".track");
|
|
||||||
thumb = (StackPane) getSkinnable().lookup(".thumb");
|
|
||||||
tickLine = (NumberAxis) getSkinnable().lookup(".axis");
|
|
||||||
if (tickLine != null) tickLine.setAnimated(false);
|
|
||||||
|
|
||||||
coloredTrack = new StackPane();
|
|
||||||
coloredTrack.getStyleClass().add("colored-track");
|
|
||||||
coloredTrack.setMouseTransparent(true);
|
|
||||||
|
|
||||||
sliderValue = new Text();
|
|
||||||
sliderValue.getStyleClass().setAll("slider-value");
|
|
||||||
|
|
||||||
animatedThumb = new StackPane();
|
|
||||||
animatedThumb.getStyleClass().add("animated-thumb");
|
|
||||||
animatedThumb.getChildren().add(sliderValue);
|
|
||||||
animatedThumb.setMouseTransparent(true);
|
|
||||||
animatedThumb.setScaleX(0);
|
|
||||||
animatedThumb.setScaleY(0);
|
|
||||||
|
|
||||||
thumb.layoutXProperty().addListener(x -> {
|
|
||||||
if (slider.getOrientation() == Orientation.VERTICAL) initAnimation(Orientation.VERTICAL);
|
|
||||||
});
|
|
||||||
thumb.layoutYProperty().addListener(y -> {
|
|
||||||
if (slider.getOrientation() == Orientation.HORIZONTAL) initAnimation(Orientation.HORIZONTAL);
|
|
||||||
});
|
|
||||||
|
|
||||||
addJFXChildren();
|
|
||||||
getChildren().addListener((ListChangeListener<Node>) c -> {
|
|
||||||
while (c.next()) {
|
|
||||||
if (c.wasAdded()) {
|
|
||||||
c.getAddedSubList().forEach(added -> {
|
|
||||||
if (added instanceof NumberAxis) {
|
|
||||||
tickLine = (NumberAxis) added;
|
|
||||||
tickLine.setAnimated(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
registerChangeListener(slider.showTickMarksProperty(), e -> addJFXChildren());
|
|
||||||
registerChangeListener(slider.showTickLabelsProperty(), e -> addJFXChildren());
|
|
||||||
registerChangeListener(slider.valueFactoryProperty(), obs -> refreshSliderValueBinding());
|
|
||||||
|
|
||||||
initListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addJFXChildren() {
|
|
||||||
ObservableList<Node> children = getChildren();
|
|
||||||
Slider slider = getSkinnable();
|
|
||||||
if ((slider.isShowTickMarks() || slider.isShowTickLabels()) && tickLine != null && !children.contains(tickLine)) {
|
|
||||||
children.add(0, tickLine);
|
|
||||||
}
|
|
||||||
if (children.contains(coloredTrack)) return;
|
|
||||||
children.add(children.indexOf(thumb), coloredTrack);
|
|
||||||
children.add(children.indexOf(thumb), animatedThumb);
|
|
||||||
children.add(0, mouseHandlerPane);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void refreshSliderValueBinding() {
|
|
||||||
sliderValue.textProperty().unbind();
|
|
||||||
if (((JFXSlider) getSkinnable()).getValueFactory() != null) {
|
|
||||||
sliderValue.textProperty()
|
|
||||||
.bind(((JFXSlider) getSkinnable()).getValueFactory().call((JFXSlider) getSkinnable()));
|
|
||||||
} else {
|
|
||||||
sliderValue.textProperty().bind(Bindings.createStringBinding(() -> {
|
|
||||||
if (getSkinnable().getLabelFormatter() != null) {
|
|
||||||
return getSkinnable().getLabelFormatter().toString(getSkinnable().getValue());
|
|
||||||
} else {
|
|
||||||
return String.valueOf(Math.round(getSkinnable().getValue()));
|
|
||||||
}
|
|
||||||
}, getSkinnable().valueProperty()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void layoutChildren(double x, double y, double w, double h) {
|
|
||||||
super.layoutChildren(x, y, w, h);
|
|
||||||
|
|
||||||
if (timeline == null) {
|
|
||||||
initAnimation(getSkinnable().getOrientation());
|
|
||||||
}
|
|
||||||
|
|
||||||
double prefWidth = animatedThumb.prefWidth(-1);
|
|
||||||
animatedThumb.resize(prefWidth, animatedThumb.prefHeight(prefWidth));
|
|
||||||
|
|
||||||
boolean horizontal = getSkinnable().getOrientation() == Orientation.HORIZONTAL;
|
|
||||||
double width, height, layoutX, layoutY;
|
|
||||||
if (horizontal) {
|
|
||||||
width = thumb.getLayoutX() - snappedLeftInset();
|
|
||||||
height = track.getHeight();
|
|
||||||
layoutX = track.getLayoutX();
|
|
||||||
layoutY = track.getLayoutY();
|
|
||||||
animatedThumb.setLayoutX(thumb.getLayoutX() + thumb.getWidth() / 2 - animatedThumb.getWidth() / 2);
|
|
||||||
} else {
|
|
||||||
height = track.getLayoutBounds().getMaxY() + track.getLayoutY() - thumb.getLayoutY() - snappedBottomInset();
|
|
||||||
width = track.getWidth();
|
|
||||||
layoutX = track.getLayoutX();
|
|
||||||
layoutY = thumb.getLayoutY();
|
|
||||||
animatedThumb.setLayoutY(thumb.getLayoutY() + thumb.getHeight() / 2 - animatedThumb.getHeight() / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
coloredTrack.resizeRelocate(layoutX, layoutY, width, height);
|
|
||||||
mouseHandlerPane.resizeRelocate(x, y, w, h);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initializeVariables() {
|
|
||||||
shifting = 30 + thumb.getWidth();
|
|
||||||
if (getSkinnable().getOrientation() != Orientation.HORIZONTAL) {
|
|
||||||
horizontalRotation = -90;
|
|
||||||
}
|
|
||||||
if (((JFXSlider) getSkinnable()).getIndicatorPosition() != IndicatorPosition.LEFT) {
|
|
||||||
indicatorRotation = 180;
|
|
||||||
shifting = -shifting;
|
|
||||||
}
|
|
||||||
final double rotationAngle = 45;
|
|
||||||
sliderValue.setRotate(rotationAngle + indicatorRotation + 3 * horizontalRotation);
|
|
||||||
animatedThumb.setRotate(-rotationAngle + indicatorRotation + horizontalRotation);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initListeners() {
|
|
||||||
// delegate slider mouse events to track node
|
|
||||||
mouseHandlerPane.setOnMousePressed(this::delegateToTrack);
|
|
||||||
mouseHandlerPane.setOnMouseReleased(this::delegateToTrack);
|
|
||||||
mouseHandlerPane.setOnMouseDragged(this::delegateToTrack);
|
|
||||||
|
|
||||||
// animate value node
|
|
||||||
track.addEventHandler(MouseEvent.MOUSE_PRESSED, (event) -> {
|
|
||||||
timeline.setRate(1);
|
|
||||||
timeline.play();
|
|
||||||
});
|
|
||||||
track.addEventHandler(MouseEvent.MOUSE_RELEASED, (event) -> {
|
|
||||||
timeline.setRate(-1);
|
|
||||||
timeline.play();
|
|
||||||
});
|
|
||||||
thumb.addEventHandler(MouseEvent.MOUSE_PRESSED, (event) -> {
|
|
||||||
timeline.setRate(1);
|
|
||||||
timeline.play();
|
|
||||||
});
|
|
||||||
thumb.addEventHandler(MouseEvent.MOUSE_RELEASED, (event) -> {
|
|
||||||
timeline.setRate(-1);
|
|
||||||
timeline.play();
|
|
||||||
});
|
|
||||||
|
|
||||||
refreshSliderValueBinding();
|
|
||||||
updateValueStyleClass();
|
|
||||||
|
|
||||||
getSkinnable().valueProperty().addListener(observable -> updateValueStyleClass());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void delegateToTrack(MouseEvent event) {
|
|
||||||
if (!event.isConsumed()) {
|
|
||||||
event.consume();
|
|
||||||
track.fireEvent(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateValueStyleClass() {
|
|
||||||
getSkinnable().pseudoClassStateChanged(MIN_VALUE, getSkinnable().getMin() == getSkinnable().getValue());
|
|
||||||
getSkinnable().pseudoClassStateChanged(MAX_VALUE, getSkinnable().getMax() == getSkinnable().getValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initAnimation(Orientation orientation) {
|
|
||||||
initializeVariables();
|
|
||||||
|
|
||||||
double thumbPos, thumbNewPos;
|
|
||||||
DoubleProperty layoutProperty;
|
|
||||||
|
|
||||||
if (orientation == Orientation.HORIZONTAL) {
|
|
||||||
if (((JFXSlider) getSkinnable()).getIndicatorPosition() == IndicatorPosition.RIGHT) {
|
|
||||||
thumbPos = thumb.getLayoutY() - thumb.getHeight();
|
|
||||||
thumbNewPos = thumbPos - shifting;
|
|
||||||
} else {
|
|
||||||
double height = animatedThumb.prefHeight(animatedThumb.prefWidth(-1));
|
|
||||||
thumbPos = thumb.getLayoutY() - height / 2;
|
|
||||||
thumbNewPos = thumb.getLayoutY() - height - thumb.getHeight();
|
|
||||||
}
|
|
||||||
layoutProperty = animatedThumb.translateYProperty();
|
|
||||||
} else {
|
|
||||||
if (((JFXSlider) getSkinnable()).getIndicatorPosition() == IndicatorPosition.RIGHT) {
|
|
||||||
thumbPos = thumb.getLayoutX() - thumb.getWidth();
|
|
||||||
thumbNewPos = thumbPos - shifting;
|
|
||||||
} else {
|
|
||||||
double width = animatedThumb.prefWidth(-1);
|
|
||||||
thumbPos = thumb.getLayoutX() - width / 2;
|
|
||||||
thumbNewPos = thumb.getLayoutX() - width - thumb.getWidth();
|
|
||||||
}
|
|
||||||
layoutProperty = animatedThumb.translateXProperty();
|
|
||||||
}
|
|
||||||
|
|
||||||
clearAnimation();
|
|
||||||
|
|
||||||
timeline = new Timeline(
|
|
||||||
new KeyFrame(
|
|
||||||
Duration.ZERO,
|
|
||||||
new KeyValue(animatedThumb.scaleXProperty(), 0, Interpolator.EASE_BOTH),
|
|
||||||
new KeyValue(animatedThumb.scaleYProperty(), 0, Interpolator.EASE_BOTH),
|
|
||||||
new KeyValue(layoutProperty, thumbPos, Interpolator.EASE_BOTH)),
|
|
||||||
new KeyFrame(
|
|
||||||
Duration.seconds(0.2),
|
|
||||||
new KeyValue(animatedThumb.scaleXProperty(), 1, Interpolator.EASE_BOTH),
|
|
||||||
new KeyValue(animatedThumb.scaleYProperty(), 1, Interpolator.EASE_BOTH),
|
|
||||||
new KeyValue(layoutProperty, thumbNewPos, Interpolator.EASE_BOTH)));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
clearAnimation();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void clearAnimation() {
|
|
||||||
if (timeline != null) {
|
|
||||||
timeline.stop();
|
|
||||||
timeline.getKeyFrames().clear();
|
|
||||||
timeline = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,264 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.jfoenix.skins;
|
|
||||||
|
|
||||||
import com.jfoenix.controls.JFXSpinner;
|
|
||||||
import com.jfoenix.utils.TreeShowingProperty;
|
|
||||||
import javafx.animation.Interpolator;
|
|
||||||
import javafx.animation.KeyFrame;
|
|
||||||
import javafx.animation.KeyValue;
|
|
||||||
import javafx.animation.Timeline;
|
|
||||||
import javafx.scene.Group;
|
|
||||||
import javafx.scene.control.SkinBase;
|
|
||||||
import javafx.scene.layout.Region;
|
|
||||||
import javafx.scene.layout.StackPane;
|
|
||||||
import javafx.scene.paint.Color;
|
|
||||||
import javafx.scene.shape.Arc;
|
|
||||||
import javafx.scene.shape.Rectangle;
|
|
||||||
import javafx.scene.shape.StrokeLineCap;
|
|
||||||
import javafx.util.Duration;
|
|
||||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/// JFXSpinner material design skin
|
|
||||||
///
|
|
||||||
/// @author Shadi Shaheen & Gerard Moubarak
|
|
||||||
/// @version 1.0
|
|
||||||
/// @since 2017-09-25
|
|
||||||
public class JFXSpinnerSkin extends SkinBase<JFXSpinner> {
|
|
||||||
|
|
||||||
private static final double DEFAULT_STROKE_WIDTH = 4;
|
|
||||||
|
|
||||||
private JFXSpinner control;
|
|
||||||
private final TreeShowingProperty treeShowingProperty;
|
|
||||||
private boolean isValid = false;
|
|
||||||
|
|
||||||
private Timeline timeline;
|
|
||||||
private Arc arc;
|
|
||||||
private Arc track;
|
|
||||||
private final StackPane arcPane;
|
|
||||||
private final Rectangle fillRect;
|
|
||||||
|
|
||||||
private final double startingAngle;
|
|
||||||
|
|
||||||
public JFXSpinnerSkin(JFXSpinner control) {
|
|
||||||
super(control);
|
|
||||||
|
|
||||||
this.control = control;
|
|
||||||
this.treeShowingProperty = new TreeShowingProperty(control);
|
|
||||||
this.startingAngle = control.getStartingAngle();
|
|
||||||
|
|
||||||
arc = new Arc();
|
|
||||||
arc.setManaged(false);
|
|
||||||
arc.setLength(-180);
|
|
||||||
arc.getStyleClass().setAll("arc");
|
|
||||||
arc.setFill(Color.TRANSPARENT);
|
|
||||||
arc.setStrokeWidth(DEFAULT_STROKE_WIDTH);
|
|
||||||
arc.setStrokeLineCap(StrokeLineCap.ROUND);
|
|
||||||
|
|
||||||
track = new Arc();
|
|
||||||
track.setManaged(false);
|
|
||||||
track.setLength(360);
|
|
||||||
track.setStrokeWidth(DEFAULT_STROKE_WIDTH);
|
|
||||||
track.getStyleClass().setAll("track");
|
|
||||||
track.setFill(Color.TRANSPARENT);
|
|
||||||
|
|
||||||
fillRect = new Rectangle();
|
|
||||||
fillRect.setFill(Color.TRANSPARENT);
|
|
||||||
final Group group = new Group(fillRect, track, arc);
|
|
||||||
group.setManaged(false);
|
|
||||||
arcPane = new StackPane(group);
|
|
||||||
arcPane.setPrefSize(50, 50);
|
|
||||||
getChildren().setAll(arcPane);
|
|
||||||
|
|
||||||
// register listeners
|
|
||||||
registerChangeListener(control.progressProperty(), obs -> updateProgress());
|
|
||||||
registerChangeListener(treeShowingProperty, obs -> updateProgress());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateProgress() {
|
|
||||||
double progress = Double.min(getSkinnable().getProgress(), 1.0);
|
|
||||||
if (progress < 0) { // indeterminate
|
|
||||||
boolean treeShowing = treeShowingProperty.get();
|
|
||||||
if (treeShowing) {
|
|
||||||
if (timeline == null) {
|
|
||||||
timeline = createTransition();
|
|
||||||
timeline.playFromStart();
|
|
||||||
} else {
|
|
||||||
timeline.play();
|
|
||||||
}
|
|
||||||
} else if (timeline != null) {
|
|
||||||
timeline.pause();
|
|
||||||
}
|
|
||||||
} else { // determinate
|
|
||||||
clearAnimation();
|
|
||||||
arc.setStartAngle(90);
|
|
||||||
arc.setLength(-360 * progress);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private double computeSize() {
|
|
||||||
return control.getRadius() * 2 + arc.getStrokeWidth() * 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
|
|
||||||
if (Region.USE_COMPUTED_SIZE == control.getRadius()) {
|
|
||||||
return super.computeMaxHeight(width, topInset, rightInset, bottomInset, leftInset);
|
|
||||||
} else {
|
|
||||||
return computeSize();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
|
|
||||||
if (Region.USE_COMPUTED_SIZE == control.getRadius()) {
|
|
||||||
return super.computeMaxWidth(height, topInset, rightInset, bottomInset, leftInset);
|
|
||||||
} else {
|
|
||||||
return computeSize();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
|
|
||||||
if (Region.USE_COMPUTED_SIZE == control.getRadius()) {
|
|
||||||
return arcPane.prefWidth(-1);
|
|
||||||
} else {
|
|
||||||
return computeSize();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
|
|
||||||
if (Region.USE_COMPUTED_SIZE == control.getRadius()) {
|
|
||||||
return arcPane.prefHeight(-1);
|
|
||||||
} else {
|
|
||||||
return computeSize();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected void layoutChildren(double contentX, double contentY, double contentWidth, double contentHeight) {
|
|
||||||
final double strokeWidth = arc.getStrokeWidth();
|
|
||||||
double radius = control.getRadius();
|
|
||||||
final double arcSize = snapSizeX(radius * 2 + strokeWidth);
|
|
||||||
|
|
||||||
arcPane.resizeRelocate((contentWidth - arcSize) / 2 + 1, (contentHeight - arcSize) / 2 + 1, arcSize, arcSize);
|
|
||||||
updateArcLayout(radius, arcSize);
|
|
||||||
|
|
||||||
fillRect.setWidth(arcSize);
|
|
||||||
fillRect.setHeight(arcSize);
|
|
||||||
|
|
||||||
if (!isValid) {
|
|
||||||
updateProgress();
|
|
||||||
isValid = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateArcLayout(double radius, double arcSize) {
|
|
||||||
arc.setRadiusX(radius);
|
|
||||||
arc.setRadiusY(radius);
|
|
||||||
arc.setCenterX(arcSize / 2);
|
|
||||||
arc.setCenterY(arcSize / 2);
|
|
||||||
|
|
||||||
track.setRadiusX(radius);
|
|
||||||
track.setRadiusY(radius);
|
|
||||||
track.setCenterX(arcSize / 2);
|
|
||||||
track.setCenterY(arcSize / 2);
|
|
||||||
track.setStrokeWidth(arc.getStrokeWidth());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addKeyFrames(List<KeyFrame> frames, double angle, double duration) {
|
|
||||||
frames.add(new KeyFrame(Duration.seconds(duration),
|
|
||||||
new KeyValue(arc.lengthProperty(), -5, Interpolator.LINEAR),
|
|
||||||
new KeyValue(arc.startAngleProperty(),
|
|
||||||
-(angle + 45 + startingAngle),
|
|
||||||
Interpolator.LINEAR)));
|
|
||||||
frames.add(new KeyFrame(Duration.seconds(duration + 0.4),
|
|
||||||
new KeyValue(arc.lengthProperty(), -250, Interpolator.LINEAR),
|
|
||||||
new KeyValue(arc.startAngleProperty(),
|
|
||||||
-(angle + 90 + startingAngle),
|
|
||||||
Interpolator.LINEAR)));
|
|
||||||
frames.add(new KeyFrame(Duration.seconds(duration + 0.7),
|
|
||||||
new KeyValue(arc.lengthProperty(), -250, Interpolator.LINEAR),
|
|
||||||
new KeyValue(arc.startAngleProperty(),
|
|
||||||
-(angle + 135 + startingAngle),
|
|
||||||
Interpolator.LINEAR)));
|
|
||||||
frames.add(new KeyFrame(Duration.seconds(duration + 1.1),
|
|
||||||
new KeyValue(arc.lengthProperty(), -5, Interpolator.LINEAR),
|
|
||||||
new KeyValue(arc.startAngleProperty(),
|
|
||||||
-(angle + 435 + startingAngle),
|
|
||||||
Interpolator.LINEAR)));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Timeline createTransition() {
|
|
||||||
Timeline timeline;
|
|
||||||
if (AnimationUtils.isAnimationEnabled()) {
|
|
||||||
var keyFrames = new ArrayList<KeyFrame>(17);
|
|
||||||
addKeyFrames(keyFrames, 0, 0);
|
|
||||||
addKeyFrames(keyFrames, 450, 1.4);
|
|
||||||
addKeyFrames(keyFrames, 900, 2.8);
|
|
||||||
addKeyFrames(keyFrames, 1350, 4.2);
|
|
||||||
keyFrames.add(new KeyFrame(Duration.seconds(5.6),
|
|
||||||
new KeyValue(arc.lengthProperty(), -5, Interpolator.LINEAR),
|
|
||||||
new KeyValue(arc.startAngleProperty(),
|
|
||||||
-(1845 + startingAngle),
|
|
||||||
Interpolator.LINEAR)));
|
|
||||||
|
|
||||||
timeline = new Timeline();
|
|
||||||
timeline.getKeyFrames().setAll(keyFrames);
|
|
||||||
} else {
|
|
||||||
final double arcLength = -250;
|
|
||||||
timeline = new Timeline(
|
|
||||||
new KeyFrame(Duration.ZERO,
|
|
||||||
new KeyValue(arc.startAngleProperty(), -(45 + startingAngle), Interpolator.LINEAR),
|
|
||||||
new KeyValue(arc.lengthProperty(), arcLength, Interpolator.DISCRETE)),
|
|
||||||
new KeyFrame(Duration.seconds(1.2),
|
|
||||||
new KeyValue(arc.startAngleProperty(), -(45 + 360 + startingAngle), Interpolator.LINEAR),
|
|
||||||
new KeyValue(arc.lengthProperty(), arcLength, Interpolator.DISCRETE))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
timeline.setCycleCount(Timeline.INDEFINITE);
|
|
||||||
return timeline;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void clearAnimation() {
|
|
||||||
if (timeline != null) {
|
|
||||||
timeline.stop();
|
|
||||||
timeline = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
treeShowingProperty.dispose();
|
|
||||||
clearAnimation();
|
|
||||||
arc = null;
|
|
||||||
track = null;
|
|
||||||
control = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,587 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
|
||||||
package com.jfoenix.skins;
|
|
||||||
|
|
||||||
import com.jfoenix.controls.JFXSwitch;
|
|
||||||
import javafx.animation.KeyFrame;
|
|
||||||
import javafx.animation.KeyValue;
|
|
||||||
import javafx.animation.Timeline;
|
|
||||||
import javafx.animation.Transition;
|
|
||||||
import javafx.beans.property.DoubleProperty;
|
|
||||||
import javafx.beans.property.ObjectProperty;
|
|
||||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
|
||||||
import javafx.beans.property.SimpleDoubleProperty;
|
|
||||||
import javafx.beans.value.ChangeListener;
|
|
||||||
import javafx.beans.value.ObservableValue;
|
|
||||||
import javafx.css.PseudoClass;
|
|
||||||
import javafx.css.StyleOrigin;
|
|
||||||
import javafx.css.StyleableProperty;
|
|
||||||
import javafx.event.EventHandler;
|
|
||||||
import javafx.geometry.Point2D;
|
|
||||||
import javafx.scene.control.SkinBase;
|
|
||||||
import javafx.scene.input.KeyCode;
|
|
||||||
import javafx.scene.input.KeyEvent;
|
|
||||||
import javafx.scene.input.MouseButton;
|
|
||||||
import javafx.scene.input.MouseEvent;
|
|
||||||
import javafx.scene.layout.Pane;
|
|
||||||
import javafx.scene.paint.Color;
|
|
||||||
import javafx.scene.paint.Paint;
|
|
||||||
import javafx.scene.shape.Circle;
|
|
||||||
import javafx.scene.shape.Rectangle;
|
|
||||||
import javafx.scene.shape.SVGPath;
|
|
||||||
import javafx.scene.transform.Scale;
|
|
||||||
import javafx.util.Duration;
|
|
||||||
import org.jackhuang.hmcl.ui.FXUtils;
|
|
||||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
|
||||||
import org.jetbrains.annotations.NotNullByDefault;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
|
|
||||||
/// Renders a Material Design 3 switch and handles input.
|
|
||||||
/// Geometry uses unscaled MD3 units and is mirrored in RTL layouts.
|
|
||||||
@NotNullByDefault
|
|
||||||
public final class JFXSwitchSkin extends SkinBase<JFXSwitch> {
|
|
||||||
/// Track width in unscaled MD3 artwork coordinates.
|
|
||||||
private static final double TRACK_WIDTH = 52;
|
|
||||||
|
|
||||||
/// Track height corresponding to the control's default `-jfx-size`.
|
|
||||||
private static final double TRACK_HEIGHT = 32;
|
|
||||||
|
|
||||||
/// Unscaled interaction target height.
|
|
||||||
private static final double TARGET_HEIGHT = 48;
|
|
||||||
|
|
||||||
/// Vertical center shared by the track, handle, state layer, and icon.
|
|
||||||
private static final double CENTER_Y = TARGET_HEIGHT / 2;
|
|
||||||
|
|
||||||
/// Handle center at the leading end of the track.
|
|
||||||
private static final double OFF_CENTER = TRACK_HEIGHT / 2;
|
|
||||||
|
|
||||||
/// Handle center at the trailing end of the track.
|
|
||||||
private static final double ON_CENTER = TRACK_WIDTH - OFF_CENTER;
|
|
||||||
|
|
||||||
/// Diameter of the unselected handle without an icon.
|
|
||||||
private static final double OFF_DIAMETER = 16;
|
|
||||||
|
|
||||||
/// Diameter of a selected handle or a handle containing an icon.
|
|
||||||
private static final double ON_DIAMETER = 24;
|
|
||||||
|
|
||||||
/// Handle diameter while pressed or dragged.
|
|
||||||
private static final double PRESSED_DIAMETER = 28;
|
|
||||||
|
|
||||||
/// Radius of the hover, focus, and pressed overlay.
|
|
||||||
private static final double STATE_LAYER_RADIUS = 20;
|
|
||||||
|
|
||||||
/// Minimum pointer movement in control pixels before a press becomes a drag.
|
|
||||||
private static final double DRAG_THRESHOLD = 3;
|
|
||||||
|
|
||||||
/// Indicates visible keyboard focus.
|
|
||||||
private static final PseudoClass VISUAL_FOCUSED = PseudoClass.getPseudoClass("visual-focused");
|
|
||||||
|
|
||||||
/// The rendered switch.
|
|
||||||
private final JFXSwitch control;
|
|
||||||
|
|
||||||
/// Unmanaged switch artwork.
|
|
||||||
private final Pane artwork = new Pane();
|
|
||||||
|
|
||||||
/// Scales artwork from its leading top corner.
|
|
||||||
private final Scale artworkScale = new Scale();
|
|
||||||
|
|
||||||
/// Rounded track with its optional outline.
|
|
||||||
private final Rectangle track = new Rectangle(0, (TARGET_HEIGHT - TRACK_HEIGHT) / 2, TRACK_WIDTH, TRACK_HEIGHT);
|
|
||||||
|
|
||||||
/// Interaction feedback behind the handle.
|
|
||||||
private final Circle stateLayer = new Circle(OFF_CENTER, CENTER_Y, STATE_LAYER_RADIUS);
|
|
||||||
|
|
||||||
/// Switch handle.
|
|
||||||
private final Circle handle = new Circle(OFF_CENTER, CENTER_Y, OFF_DIAMETER / 2);
|
|
||||||
|
|
||||||
/// Optional handle icon.
|
|
||||||
private @Nullable SVGPath icon;
|
|
||||||
|
|
||||||
/// Current logical handle center measured from the track's leading edge.
|
|
||||||
private final DoubleProperty center = new SimpleDoubleProperty(OFF_CENTER) {
|
|
||||||
/// Updates handle position.
|
|
||||||
@Override
|
|
||||||
protected void invalidated() {
|
|
||||||
layoutHandle();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Unscaled handle diameter.
|
|
||||||
private final DoubleProperty diameter = new SimpleDoubleProperty(OFF_DIAMETER) {
|
|
||||||
/// Updates handle size.
|
|
||||||
@Override
|
|
||||||
protected void invalidated() {
|
|
||||||
layoutHandle();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Optional movement and resize animation.
|
|
||||||
private @Nullable Timeline geometryTransition;
|
|
||||||
|
|
||||||
/// Animated CSS paints.
|
|
||||||
private final List<CssColor> colors = new ArrayList<>();
|
|
||||||
|
|
||||||
/// Optional shared color animation.
|
|
||||||
private @Nullable Transition colorTransition;
|
|
||||||
|
|
||||||
/// Whether a selection color update is pending.
|
|
||||||
private boolean animateColors;
|
|
||||||
|
|
||||||
/// Prevents animation writes from being interpreted as new CSS targets.
|
|
||||||
private boolean applyingColors;
|
|
||||||
|
|
||||||
/// Pointer event handler.
|
|
||||||
private final EventHandler<MouseEvent> mouseHandler = this::handleMouse;
|
|
||||||
|
|
||||||
/// Keyboard event handler.
|
|
||||||
private final EventHandler<KeyEvent> keyHandler = this::handleKey;
|
|
||||||
|
|
||||||
/// Whether a primary press is awaiting click or drag completion.
|
|
||||||
private boolean mouseDown;
|
|
||||||
|
|
||||||
/// Whether the handle is following the pointer instead of an animation.
|
|
||||||
private boolean dragging;
|
|
||||||
|
|
||||||
/// Whether a Space press is awaiting release.
|
|
||||||
private boolean keyDown;
|
|
||||||
|
|
||||||
/// Pointer position in unscaled artwork coordinates at press time.
|
|
||||||
private double pressX;
|
|
||||||
|
|
||||||
/// Handle center at press time, preserving the pointer's grab offset.
|
|
||||||
private double pressCenter;
|
|
||||||
|
|
||||||
/// Creates a skin for the switch.
|
|
||||||
public JFXSwitchSkin(JFXSwitch control) {
|
|
||||||
super(control);
|
|
||||||
this.control = control;
|
|
||||||
artwork.getStyleClass().setAll("switch-artwork");
|
|
||||||
track.getStyleClass().setAll("switch-track");
|
|
||||||
stateLayer.getStyleClass().setAll("switch-state-layer");
|
|
||||||
handle.getStyleClass().setAll("switch-handle");
|
|
||||||
track.setArcWidth(TRACK_HEIGHT);
|
|
||||||
track.setArcHeight(TRACK_HEIGHT);
|
|
||||||
artwork.setManaged(false);
|
|
||||||
artwork.setMouseTransparent(true);
|
|
||||||
artwork.getTransforms().add(artworkScale);
|
|
||||||
artwork.getChildren().setAll(track, stateLayer, handle);
|
|
||||||
for (var node : artwork.getChildren())
|
|
||||||
node.setManaged(false);
|
|
||||||
getChildren().add(artwork);
|
|
||||||
registerChangeListener(control.selectedProperty(), value -> updateSelection());
|
|
||||||
registerChangeListener(control.armedProperty(), value -> update(false));
|
|
||||||
@Nullable ReadOnlyBooleanProperty focusVisible = FXUtils.focusVisibleProperty(control);
|
|
||||||
ReadOnlyBooleanProperty focusIndicator = focusVisible != null ? focusVisible : control.focusedProperty();
|
|
||||||
registerChangeListener(focusIndicator, value ->
|
|
||||||
control.pseudoClassStateChanged(VISUAL_FOCUSED, focusIndicator.get() && !control.isPressed()));
|
|
||||||
// Match the previous toggle skin: pointer press and release both dismiss focus feedback.
|
|
||||||
registerChangeListener(control.pressedProperty(), value ->
|
|
||||||
control.pseudoClassStateChanged(VISUAL_FOCUSED, false));
|
|
||||||
Consumer<ObservableValue<?>> cancelGesture = value -> cancelGesture();
|
|
||||||
registerChangeListener(control.disabledProperty(), cancelGesture);
|
|
||||||
registerChangeListener(control.sceneProperty(), cancelGesture);
|
|
||||||
registerChangeListener(control.focusedProperty(), value -> {
|
|
||||||
if (!control.isFocused())
|
|
||||||
cancelGesture();
|
|
||||||
});
|
|
||||||
control.addEventFilter(MouseEvent.ANY, mouseHandler);
|
|
||||||
control.addEventFilter(KeyEvent.ANY, keyHandler);
|
|
||||||
control.pseudoClassStateChanged(VISUAL_FOCUSED, focusIndicator.get() && !control.isPressed());
|
|
||||||
update(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Updates icon and animation options, optionally settling active animations.
|
|
||||||
public void updateOptions(boolean finishAnimation) {
|
|
||||||
if (finishAnimation)
|
|
||||||
finishColors();
|
|
||||||
update(finishAnimation);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stops animations and removes artwork, event handlers, and listeners.
|
|
||||||
@Override
|
|
||||||
public void dispose() {
|
|
||||||
control.removeEventFilter(MouseEvent.ANY, mouseHandler);
|
|
||||||
control.removeEventFilter(KeyEvent.ANY, keyHandler);
|
|
||||||
cancelGesture();
|
|
||||||
for (CssColor color : colors)
|
|
||||||
color.dispose();
|
|
||||||
control.pseudoClassStateChanged(VISUAL_FOCUSED, false);
|
|
||||||
getChildren().remove(artwork);
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Centers the scaled artwork within the control and starts pending color transitions.
|
|
||||||
@Override
|
|
||||||
protected void layoutChildren(double x, double y, double w, double h) {
|
|
||||||
double scale = sizeScale();
|
|
||||||
artworkScale.setX(scale);
|
|
||||||
artworkScale.setY(scale);
|
|
||||||
// Keep animation and CSS geometry in MD3 units; scale the entire artwork consistently.
|
|
||||||
artwork.resizeRelocate(x + (w - TRACK_WIDTH * scale) / 2,
|
|
||||||
y + (h - TARGET_HEIGHT * scale) / 2, TRACK_WIDTH, TARGET_HEIGHT);
|
|
||||||
layoutHandle();
|
|
||||||
// JavaFX applies CSS before layout, so all changed paints now share the same start time.
|
|
||||||
if (animateColors) {
|
|
||||||
animateColors = false;
|
|
||||||
for (CssColor color : colors) {
|
|
||||||
if (color.target != null) {
|
|
||||||
colorTransition().playFromStart();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reserves the scaled track width and control insets.
|
|
||||||
@Override
|
|
||||||
protected double computeMinWidth(double h, double top, double right, double bottom, double left) {
|
|
||||||
return left + TRACK_WIDTH * sizeScale() + right;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reserves the scaled interaction target height and control insets.
|
|
||||||
@Override
|
|
||||||
protected double computeMinHeight(double w, double top, double right, double bottom, double left) {
|
|
||||||
return top + TARGET_HEIGHT * sizeScale() + bottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Uses the same fixed width as the minimum size.
|
|
||||||
@Override
|
|
||||||
protected double computePrefWidth(double h, double top, double right, double bottom, double left) {
|
|
||||||
return computeMinWidth(h, top, right, bottom, left);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Uses the same fixed height as the minimum size.
|
|
||||||
@Override
|
|
||||||
protected double computePrefHeight(double w, double top, double right, double bottom, double left) {
|
|
||||||
return computeMinHeight(w, top, right, bottom, left);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Limits width to the preferred size.
|
|
||||||
@Override
|
|
||||||
protected double computeMaxWidth(double h, double top, double right, double bottom, double left) {
|
|
||||||
return computePrefWidth(h, top, right, bottom, left);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Limits height to the preferred size.
|
|
||||||
@Override
|
|
||||||
protected double computeMaxHeight(double w, double top, double right, double bottom, double left) {
|
|
||||||
return computePrefHeight(w, top, right, bottom, left);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handles clicking and dragging.
|
|
||||||
private void handleMouse(MouseEvent event) {
|
|
||||||
if (event.getEventType() == MouseEvent.MOUSE_CLICKED && event.getButton() == MouseButton.PRIMARY) {
|
|
||||||
// Activation happens on release; do not also activate the surrounding settings row.
|
|
||||||
event.consume();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (event.getEventType() == MouseEvent.MOUSE_PRESSED) {
|
|
||||||
if (event.getButton() != MouseButton.PRIMARY || keyDown
|
|
||||||
|| event.isShiftDown() || event.isControlDown() || event.isAltDown() || event.isMetaDown()
|
|
||||||
|| event.isMiddleButtonDown() || event.isSecondaryButtonDown())
|
|
||||||
return;
|
|
||||||
if (control.isDisabled() || control.selectedProperty().isBound())
|
|
||||||
return;
|
|
||||||
if (control.isFocusTraversable())
|
|
||||||
control.requestFocus();
|
|
||||||
@Nullable Point2D point = artwork.sceneToLocal(event.getSceneX(), event.getSceneY());
|
|
||||||
if (point != null) {
|
|
||||||
mouseDown = true;
|
|
||||||
pressX = point.getX();
|
|
||||||
pressCenter = center.get();
|
|
||||||
control.arm();
|
|
||||||
}
|
|
||||||
event.consume();
|
|
||||||
} else if (event.getEventType() == MouseEvent.MOUSE_DRAGGED && mouseDown && event.isPrimaryButtonDown()) {
|
|
||||||
@Nullable Point2D point = artwork.sceneToLocal(event.getSceneX(), event.getSceneY());
|
|
||||||
if (point == null)
|
|
||||||
return;
|
|
||||||
if (!dragging && Math.abs(point.getX() - pressX) * sizeScale() < DRAG_THRESHOLD)
|
|
||||||
return;
|
|
||||||
dragging = true;
|
|
||||||
if (geometryTransition != null)
|
|
||||||
geometryTransition.stop();
|
|
||||||
control.arm();
|
|
||||||
diameter.set(PRESSED_DIAMETER);
|
|
||||||
center.set(dragCenter(point.getX()));
|
|
||||||
event.consume();
|
|
||||||
} else if (event.getEventType() == MouseEvent.MOUSE_RELEASED && event.getButton() == MouseButton.PRIMARY) {
|
|
||||||
if (!mouseDown) {
|
|
||||||
event.consume();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
@Nullable Point2D point = dragging
|
|
||||||
? artwork.sceneToLocal(event.getSceneX(), event.getSceneY())
|
|
||||||
: control.sceneToLocal(event.getSceneX(), event.getSceneY());
|
|
||||||
if (dragging && point != null)
|
|
||||||
center.set(dragCenter(point.getX()));
|
|
||||||
boolean activate = point != null && (dragging
|
|
||||||
? (center.get() >= (OFF_CENTER + ON_CENTER) / 2) != control.isSelected()
|
|
||||||
: control.isArmed() && control.contains(point));
|
|
||||||
mouseDown = false;
|
|
||||||
dragging = false;
|
|
||||||
control.disarm();
|
|
||||||
event.consume();
|
|
||||||
if (activate)
|
|
||||||
control.fire();
|
|
||||||
// Action handlers may replace or remove the skin synchronously.
|
|
||||||
if (control.getSkin() == this)
|
|
||||||
update(false);
|
|
||||||
} else if (mouseDown && !dragging) {
|
|
||||||
if (event.getEventType() == MouseEvent.MOUSE_ENTERED)
|
|
||||||
control.arm();
|
|
||||||
else if (event.getEventType() == MouseEvent.MOUSE_EXITED)
|
|
||||||
control.disarm();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clamps pointer displacement to the track while preserving the initial grab offset.
|
|
||||||
private double dragCenter(double pointerX) {
|
|
||||||
return Math.max(OFF_CENTER, Math.min(ON_CENTER, pressCenter + pointerX - pressX));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handles Space activation and Escape cancellation.
|
|
||||||
private void handleKey(KeyEvent event) {
|
|
||||||
if (event.getCode() == KeyCode.ESCAPE && event.getEventType() == KeyEvent.KEY_PRESSED) {
|
|
||||||
if (mouseDown || keyDown) {
|
|
||||||
boolean awaitingRelease = keyDown;
|
|
||||||
cancelGesture();
|
|
||||||
// Ignore auto-repeat until the canceled Space press is released.
|
|
||||||
keyDown = awaitingRelease;
|
|
||||||
event.consume();
|
|
||||||
}
|
|
||||||
} else if (event.getCode() == KeyCode.SPACE) {
|
|
||||||
if (event.getEventType() == KeyEvent.KEY_PRESSED) {
|
|
||||||
if (event.isShiftDown() || event.isControlDown() || event.isAltDown() || event.isMetaDown())
|
|
||||||
return;
|
|
||||||
if (!keyDown) {
|
|
||||||
if (mouseDown || control.isDisabled() || control.selectedProperty().isBound())
|
|
||||||
return;
|
|
||||||
keyDown = true;
|
|
||||||
control.arm();
|
|
||||||
}
|
|
||||||
event.consume();
|
|
||||||
} else if (event.getEventType() == KeyEvent.KEY_RELEASED && keyDown) {
|
|
||||||
keyDown = false;
|
|
||||||
boolean activate = control.isArmed();
|
|
||||||
control.disarm();
|
|
||||||
event.consume();
|
|
||||||
if (activate)
|
|
||||||
control.fire();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Cancels input without changing selection.
|
|
||||||
private void cancelGesture() {
|
|
||||||
finishColors();
|
|
||||||
mouseDown = false;
|
|
||||||
keyDown = false;
|
|
||||||
dragging = false;
|
|
||||||
control.disarm();
|
|
||||||
update(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Updates geometry and schedules selection colors for the next CSS pass.
|
|
||||||
private void updateSelection() {
|
|
||||||
update(false);
|
|
||||||
if (control.isDisableAnimation() || control.isDisabled() || control.getScene() == null)
|
|
||||||
return;
|
|
||||||
if (colors.isEmpty()) {
|
|
||||||
colors.add(new CssColor(track.fillProperty()));
|
|
||||||
colors.add(new CssColor(handle.fillProperty()));
|
|
||||||
colors.add(new CssColor(stateLayer.fillProperty()));
|
|
||||||
colors.add(new CssColor(track.strokeProperty()));
|
|
||||||
if (icon != null)
|
|
||||||
colors.add(new CssColor(icon.fillProperty()));
|
|
||||||
}
|
|
||||||
if (colorTransition != null)
|
|
||||||
colorTransition.stop();
|
|
||||||
// A reversal before the first frame may reapply the visible paint without emitting a change event.
|
|
||||||
for (CssColor color : colors)
|
|
||||||
color.target = null;
|
|
||||||
animateColors = true;
|
|
||||||
control.requestLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Updates handle geometry, with optional animation.
|
|
||||||
private void update(boolean immediate) {
|
|
||||||
if (dragging)
|
|
||||||
return;
|
|
||||||
double currentCenter = center.get();
|
|
||||||
double currentDiameter = diameter.get();
|
|
||||||
if (geometryTransition != null)
|
|
||||||
geometryTransition.stop();
|
|
||||||
boolean selected = control.isSelected();
|
|
||||||
boolean pressed = control.isArmed() && !control.isDisabled();
|
|
||||||
boolean hasIcon = selected ? control.isShowSelectedIcon() : control.isShowUnselectedIcon();
|
|
||||||
double targetDiameter = pressed ? PRESSED_DIAMETER : selected || hasIcon ? ON_DIAMETER : OFF_DIAMETER;
|
|
||||||
double targetCenter = selected ? ON_CENTER : OFF_CENTER;
|
|
||||||
updateIcon(selected, hasIcon);
|
|
||||||
if (immediate || control.isDisableAnimation() || control.getScene() == null
|
|
||||||
|| currentCenter == targetCenter && currentDiameter == targetDiameter) {
|
|
||||||
center.set(targetCenter);
|
|
||||||
diameter.set(targetDiameter);
|
|
||||||
} else {
|
|
||||||
if (geometryTransition == null)
|
|
||||||
geometryTransition = new Timeline();
|
|
||||||
// Preserve the visible geometry when a press or reversal interrupts a transition.
|
|
||||||
center.set(currentCenter);
|
|
||||||
diameter.set(currentDiameter);
|
|
||||||
geometryTransition.getKeyFrames().setAll(
|
|
||||||
new KeyFrame(Duration.ZERO,
|
|
||||||
new KeyValue(center, currentCenter),
|
|
||||||
new KeyValue(diameter, currentDiameter)),
|
|
||||||
new KeyFrame(pressed ? Motion.SHORT2 : Motion.MEDIUM1,
|
|
||||||
new KeyValue(diameter, targetDiameter, pressed ? Motion.LINEAR : Motion.STANDARD)),
|
|
||||||
new KeyFrame(Motion.MEDIUM2,
|
|
||||||
new KeyValue(center, targetCenter, Motion.EASE_OUT_BACK)));
|
|
||||||
geometryTransition.play();
|
|
||||||
}
|
|
||||||
layoutHandle();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Updates the handle icon, creating it if needed.
|
|
||||||
private void updateIcon(boolean selected, boolean visible) {
|
|
||||||
if (icon == null && visible) {
|
|
||||||
icon = new SVGPath();
|
|
||||||
icon.getStyleClass().setAll("switch-icon");
|
|
||||||
icon.setManaged(false);
|
|
||||||
// The first CSS fill should appear immediately, without a transition from default black.
|
|
||||||
icon.setFill(null);
|
|
||||||
if (!colors.isEmpty())
|
|
||||||
colors.add(new CssColor(icon.fillProperty()));
|
|
||||||
artwork.getChildren().add(icon);
|
|
||||||
}
|
|
||||||
if (icon != null) {
|
|
||||||
icon.setVisible(visible);
|
|
||||||
if (visible)
|
|
||||||
icon.setContent(selected
|
|
||||||
? "M2 8l4 4 8-8-1.4-1.4L6 9.2 3.4 6.6Z"
|
|
||||||
: "M4 2.6l4 4 4-4L13.4 4l-4 4 4 4-1.4 1.4-4-4-4 4L2.6 12l4-4-4-4Z");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Positions and sizes the handle and its overlays.
|
|
||||||
private void layoutHandle() {
|
|
||||||
double cx = center.get();
|
|
||||||
stateLayer.setCenterX(cx);
|
|
||||||
handle.setCenterX(cx);
|
|
||||||
handle.setRadius(diameter.get() / 2);
|
|
||||||
if (icon != null && icon.isVisible())
|
|
||||||
icon.relocate(cx - icon.getLayoutBounds().getWidth() / 2,
|
|
||||||
CENTER_Y - icon.getLayoutBounds().getHeight() / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the size scale, or 1 for an invalid size.
|
|
||||||
private double sizeScale() {
|
|
||||||
double size = control.getSize();
|
|
||||||
return Double.isFinite(size) && size > 0 ? size / TRACK_HEIGHT : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the shared color animation, creating it if needed.
|
|
||||||
private Transition colorTransition() {
|
|
||||||
if (colorTransition == null) {
|
|
||||||
colorTransition = new Transition() {
|
|
||||||
{
|
|
||||||
setCycleDuration(Motion.SHORT4);
|
|
||||||
setInterpolator(Motion.STANDARD);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Updates only paints with a pending CSS target.
|
|
||||||
@Override
|
|
||||||
protected void interpolate(double fraction) {
|
|
||||||
for (CssColor color : colors)
|
|
||||||
color.interpolate(fraction);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
colorTransition.setOnFinished(event -> finishColors());
|
|
||||||
}
|
|
||||||
return colorTransition;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stops color animation and applies pending targets.
|
|
||||||
private void finishColors() {
|
|
||||||
animateColors = false;
|
|
||||||
if (colorTransition != null)
|
|
||||||
colorTransition.stop();
|
|
||||||
for (CssColor color : colors)
|
|
||||||
color.finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Animates a solid paint while preserving CSS precedence.
|
|
||||||
private final class CssColor implements ChangeListener<@Nullable Paint> {
|
|
||||||
|
|
||||||
/// The animated paint property.
|
|
||||||
private final ObjectProperty<@Nullable Paint> property;
|
|
||||||
|
|
||||||
/// Most recent solid CSS target; null means no transition is pending.
|
|
||||||
private @Nullable Color target;
|
|
||||||
|
|
||||||
/// CSS origin of the target paint.
|
|
||||||
private @Nullable StyleOrigin origin;
|
|
||||||
|
|
||||||
/// Starting color.
|
|
||||||
private Color from = Color.TRANSPARENT;
|
|
||||||
|
|
||||||
/// Observes a styleable paint property.
|
|
||||||
private CssColor(ObjectProperty<@Nullable Paint> property) {
|
|
||||||
this.property = property;
|
|
||||||
property.addListener(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Animates selection colors and applies other paint changes immediately.
|
|
||||||
@Override
|
|
||||||
public void changed(ObservableValue<? extends @Nullable Paint> observable,
|
|
||||||
@Nullable Paint previous, @Nullable Paint next) {
|
|
||||||
if (applyingColors)
|
|
||||||
return;
|
|
||||||
// CSS may reapply the same target while the displayed value is still interpolating.
|
|
||||||
if (target != null && target.equals(next) && previous != null) {
|
|
||||||
apply(previous);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
target = null;
|
|
||||||
if (!animateColors || !(previous instanceof Color start) || !(next instanceof Color end) || start.equals(end))
|
|
||||||
return;
|
|
||||||
from = start;
|
|
||||||
target = end;
|
|
||||||
origin = ((StyleableProperty<?>) property).getStyleOrigin();
|
|
||||||
interpolate(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Applies an animation frame.
|
|
||||||
private void interpolate(double fraction) {
|
|
||||||
if (target != null)
|
|
||||||
apply(fraction == 1 ? target : from.interpolate(target, fraction));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Writes a CSS value while suppressing this transition's own listener.
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private void apply(Paint value) {
|
|
||||||
applyingColors = true;
|
|
||||||
try {
|
|
||||||
((StyleableProperty<Paint>) property).applyStyle(origin, value);
|
|
||||||
} finally {
|
|
||||||
applyingColors = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Applies and clears the pending target.
|
|
||||||
private void finish() {
|
|
||||||
if (target != null) {
|
|
||||||
apply(target);
|
|
||||||
target = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Removes the paint listener.
|
|
||||||
private void dispose() {
|
|
||||||
property.removeListener(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -1,68 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.jfoenix.transitions;
|
|
||||||
|
|
||||||
import javafx.scene.CacheHint;
|
|
||||||
import javafx.scene.Node;
|
|
||||||
import javafx.scene.layout.Region;
|
|
||||||
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
|
|
||||||
public final class CacheMemento {
|
|
||||||
private boolean cache;
|
|
||||||
private boolean cacheShape;
|
|
||||||
private boolean snapToPixel;
|
|
||||||
private CacheHint cacheHint = CacheHint.DEFAULT;
|
|
||||||
private final Node node;
|
|
||||||
private final AtomicBoolean isCached = new AtomicBoolean(false);
|
|
||||||
|
|
||||||
public CacheMemento(Node node) {
|
|
||||||
this.node = node;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* this method will cache the node only if it wasn't cached before
|
|
||||||
*/
|
|
||||||
public void cache() {
|
|
||||||
if (!isCached.getAndSet(true)) {
|
|
||||||
this.cache = node.isCache();
|
|
||||||
this.cacheHint = node.getCacheHint();
|
|
||||||
node.setCache(true);
|
|
||||||
node.setCacheHint(CacheHint.SPEED);
|
|
||||||
if (node instanceof Region region) {
|
|
||||||
this.cacheShape = region.isCacheShape();
|
|
||||||
this.snapToPixel = region.isSnapToPixel();
|
|
||||||
region.setCacheShape(true);
|
|
||||||
region.setSnapToPixel(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void restore() {
|
|
||||||
if (isCached.getAndSet(false)) {
|
|
||||||
node.setCache(cache);
|
|
||||||
node.setCacheHint(cacheHint);
|
|
||||||
if (node instanceof Region region) {
|
|
||||||
region.setCacheShape(cacheShape);
|
|
||||||
region.setSnapToPixel(snapToPixel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,286 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.jfoenix.transitions;
|
|
||||||
|
|
||||||
import javafx.animation.AnimationTimer;
|
|
||||||
import javafx.beans.value.WritableValue;
|
|
||||||
import javafx.scene.Node;
|
|
||||||
import javafx.util.Duration;
|
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom AnimationTimer that can be created the same way as a timeline,
|
|
||||||
* however it doesn't behave the same yet. it only animates in one direction,
|
|
||||||
* it doesn't support animation 0 -> 1 -> 0.5
|
|
||||||
*
|
|
||||||
* @author Shadi Shaheen
|
|
||||||
* @version 1.0
|
|
||||||
* @since 2017-09-21
|
|
||||||
*/
|
|
||||||
|
|
||||||
public class JFXAnimationTimer extends AnimationTimer {
|
|
||||||
|
|
||||||
private Set<AnimationHandler> animationHandlers = new HashSet<>();
|
|
||||||
private long startTime = -1;
|
|
||||||
private boolean running = false;
|
|
||||||
private List<CacheMemento> caches = new ArrayList<>();
|
|
||||||
private double totalElapsedMilliseconds;
|
|
||||||
|
|
||||||
public JFXAnimationTimer(JFXKeyFrame... keyFrames) {
|
|
||||||
for (JFXKeyFrame keyFrame : keyFrames) {
|
|
||||||
Duration duration = keyFrame.getDuration();
|
|
||||||
final Set<JFXKeyValue<?>> keyValuesSet = keyFrame.getValues();
|
|
||||||
if (!keyValuesSet.isEmpty()) {
|
|
||||||
animationHandlers.add(new AnimationHandler(duration, keyFrame.getAnimateCondition(), keyFrame.getValues()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final HashMap<JFXKeyFrame, AnimationHandler> mutableFrames = new HashMap<>();
|
|
||||||
|
|
||||||
public void addKeyFrame(JFXKeyFrame keyFrame) throws Exception {
|
|
||||||
if (isRunning()) {
|
|
||||||
throw new Exception("Can't update animation timer while running");
|
|
||||||
}
|
|
||||||
Duration duration = keyFrame.getDuration();
|
|
||||||
final Set<JFXKeyValue<?>> keyValuesSet = keyFrame.getValues();
|
|
||||||
if (!keyValuesSet.isEmpty()) {
|
|
||||||
final AnimationHandler handler = new AnimationHandler(duration, keyFrame.getAnimateCondition(), keyFrame.getValues());
|
|
||||||
animationHandlers.add(handler);
|
|
||||||
mutableFrames.put(keyFrame, handler);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void removeKeyFrame(JFXKeyFrame keyFrame) throws Exception {
|
|
||||||
if (isRunning()) {
|
|
||||||
throw new Exception("Can't update animation timer while running");
|
|
||||||
}
|
|
||||||
AnimationHandler handler = mutableFrames.get(keyFrame);
|
|
||||||
animationHandlers.remove(handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void start() {
|
|
||||||
super.start();
|
|
||||||
running = true;
|
|
||||||
startTime = -1;
|
|
||||||
for (AnimationHandler animationHandler : animationHandlers) {
|
|
||||||
animationHandler.init();
|
|
||||||
}
|
|
||||||
for (CacheMemento cache : caches) {
|
|
||||||
cache.cache();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handle(long now) {
|
|
||||||
startTime = startTime == -1 ? now : startTime;
|
|
||||||
totalElapsedMilliseconds = (now - startTime) / 1000000.0;
|
|
||||||
boolean stop = true;
|
|
||||||
for (AnimationHandler handler : animationHandlers) {
|
|
||||||
handler.animate(totalElapsedMilliseconds);
|
|
||||||
if (!handler.finished) {
|
|
||||||
stop = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (stop) {
|
|
||||||
this.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* this method will pause the timer and reverse the animation if the timer already
|
|
||||||
* started otherwise it will start the animation.
|
|
||||||
*/
|
|
||||||
public void reverseAndContinue() {
|
|
||||||
if (isRunning()) {
|
|
||||||
super.stop();
|
|
||||||
for (AnimationHandler handler : animationHandlers) {
|
|
||||||
handler.reverse(totalElapsedMilliseconds);
|
|
||||||
}
|
|
||||||
startTime = -1;
|
|
||||||
super.start();
|
|
||||||
} else {
|
|
||||||
start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void stop() {
|
|
||||||
super.stop();
|
|
||||||
running = false;
|
|
||||||
for (AnimationHandler handler : animationHandlers) {
|
|
||||||
handler.clear();
|
|
||||||
}
|
|
||||||
for (CacheMemento cache : caches) {
|
|
||||||
cache.restore();
|
|
||||||
}
|
|
||||||
if (onFinished != null) {
|
|
||||||
onFinished.run();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void applyEndValues() {
|
|
||||||
if (isRunning()) {
|
|
||||||
super.stop();
|
|
||||||
}
|
|
||||||
for (AnimationHandler handler : animationHandlers) {
|
|
||||||
handler.applyEndValues();
|
|
||||||
}
|
|
||||||
startTime = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isRunning() {
|
|
||||||
return running;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Runnable onFinished = null;
|
|
||||||
|
|
||||||
public void setOnFinished(Runnable onFinished) {
|
|
||||||
this.onFinished = onFinished;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCacheNodes(Node... nodesToCache) {
|
|
||||||
caches.clear();
|
|
||||||
if (nodesToCache != null) {
|
|
||||||
for (Node node : nodesToCache) {
|
|
||||||
caches.add(new CacheMemento(node));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void dispose() {
|
|
||||||
caches.clear();
|
|
||||||
for (AnimationHandler handler : animationHandlers) {
|
|
||||||
handler.dispose();
|
|
||||||
}
|
|
||||||
animationHandlers.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
static class AnimationHandler {
|
|
||||||
private final double duration;
|
|
||||||
private double currentDuration;
|
|
||||||
private final Set<JFXKeyValue<?>> keyValues;
|
|
||||||
private Supplier<Boolean> animationCondition = null;
|
|
||||||
private boolean finished = false;
|
|
||||||
|
|
||||||
private final HashMap<WritableValue<?>, Object> initialValuesMap = new HashMap<>();
|
|
||||||
private final HashMap<WritableValue<?>, Object> endValuesMap = new HashMap<>();
|
|
||||||
|
|
||||||
AnimationHandler(Duration duration, Supplier<Boolean> animationCondition, Set<JFXKeyValue<?>> keyValues) {
|
|
||||||
this.duration = duration.toMillis();
|
|
||||||
currentDuration = this.duration;
|
|
||||||
this.keyValues = keyValues;
|
|
||||||
this.animationCondition = animationCondition;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void init() {
|
|
||||||
finished = animationCondition != null && !animationCondition.get();
|
|
||||||
for (JFXKeyValue<?> keyValue : keyValues) {
|
|
||||||
if (keyValue.getTarget() != null) {
|
|
||||||
// replaced putIfAbsent for mobile compatibility
|
|
||||||
if (!initialValuesMap.containsKey(keyValue.getTarget())) {
|
|
||||||
initialValuesMap.put(keyValue.getTarget(), keyValue.getTarget().getValue());
|
|
||||||
}
|
|
||||||
if (!endValuesMap.containsKey(keyValue.getTarget())) {
|
|
||||||
endValuesMap.put(keyValue.getTarget(), keyValue.getEndValue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void reverse(double now) {
|
|
||||||
finished = animationCondition != null && !animationCondition.get();
|
|
||||||
currentDuration = duration - (currentDuration - now);
|
|
||||||
// update initial values
|
|
||||||
for (JFXKeyValue<?> keyValue : keyValues) {
|
|
||||||
final WritableValue<?> target = keyValue.getTarget();
|
|
||||||
if (target != null) {
|
|
||||||
initialValuesMap.put(target, target.getValue());
|
|
||||||
endValuesMap.put(target, keyValue.getEndValue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// now in milliseconds
|
|
||||||
@SuppressWarnings({"unchecked"})
|
|
||||||
public void animate(double now) {
|
|
||||||
// if animate condition for the key frame is not met then do nothing
|
|
||||||
if (finished) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (now <= currentDuration) {
|
|
||||||
for (JFXKeyValue<?> keyValue : keyValues) {
|
|
||||||
if (keyValue.isValid()) {
|
|
||||||
@SuppressWarnings("rawtypes") final WritableValue target = keyValue.getTarget();
|
|
||||||
final Object endValue = endValuesMap.get(target);
|
|
||||||
if (endValue != null && target != null && !target.getValue().equals(endValue)) {
|
|
||||||
target.setValue(keyValue.getInterpolator().interpolate(initialValuesMap.get(target), endValue, now / currentDuration));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!finished) {
|
|
||||||
finished = true;
|
|
||||||
for (JFXKeyValue<?> keyValue : keyValues) {
|
|
||||||
if (keyValue.isValid()) {
|
|
||||||
@SuppressWarnings("rawtypes") final WritableValue target = keyValue.getTarget();
|
|
||||||
if (target != null) {
|
|
||||||
// set updated end value instead of cached
|
|
||||||
final Object endValue = keyValue.getEndValue();
|
|
||||||
if (endValue != null) {
|
|
||||||
target.setValue(endValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
currentDuration = duration;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public void applyEndValues() {
|
|
||||||
for (JFXKeyValue<?> keyValue : keyValues) {
|
|
||||||
if (keyValue.isValid()) {
|
|
||||||
@SuppressWarnings("rawtypes") final WritableValue target = keyValue.getTarget();
|
|
||||||
if (target != null) {
|
|
||||||
final Object endValue = keyValue.getEndValue();
|
|
||||||
if (endValue != null && !target.getValue().equals(endValue)) {
|
|
||||||
target.setValue(endValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void clear() {
|
|
||||||
initialValuesMap.clear();
|
|
||||||
endValuesMap.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
void dispose() {
|
|
||||||
clear();
|
|
||||||
keyValues.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.jfoenix.transitions;
|
|
||||||
|
|
||||||
import javafx.util.Duration;
|
|
||||||
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.concurrent.CopyOnWriteArraySet;
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Shadi Shaheen
|
|
||||||
* @version 1.0
|
|
||||||
* @since 2017-09-21
|
|
||||||
*/
|
|
||||||
|
|
||||||
public class JFXKeyFrame {
|
|
||||||
|
|
||||||
private Duration duration;
|
|
||||||
private Set<JFXKeyValue<?>> keyValues = new CopyOnWriteArraySet<>();
|
|
||||||
private Supplier<Boolean> animateCondition = null;
|
|
||||||
|
|
||||||
public JFXKeyFrame(Duration duration, JFXKeyValue<?>... keyValues) {
|
|
||||||
this.duration = duration;
|
|
||||||
for (final JFXKeyValue<?> keyValue : keyValues) {
|
|
||||||
if (keyValue != null) {
|
|
||||||
this.keyValues.add(keyValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private JFXKeyFrame() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public final Duration getDuration() {
|
|
||||||
return duration;
|
|
||||||
}
|
|
||||||
|
|
||||||
public final Set<JFXKeyValue<?>> getValues() {
|
|
||||||
return keyValues;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Supplier<Boolean> getAnimateCondition() {
|
|
||||||
return animateCondition;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Builder builder() {
|
|
||||||
return new Builder();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final class Builder {
|
|
||||||
private Duration duration;
|
|
||||||
private final Set<JFXKeyValue<?>> keyValues = new CopyOnWriteArraySet<>();
|
|
||||||
private Supplier<Boolean> animateCondition = null;
|
|
||||||
|
|
||||||
private Builder() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder setDuration(Duration duration) {
|
|
||||||
this.duration = duration;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder setKeyValues(JFXKeyValue<?>... keyValues) {
|
|
||||||
for (final JFXKeyValue<?> keyValue : keyValues) {
|
|
||||||
if (keyValue != null) {
|
|
||||||
this.keyValues.add(keyValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder setAnimateCondition(Supplier<Boolean> animateCondition) {
|
|
||||||
this.animateCondition = animateCondition;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public JFXKeyFrame build() {
|
|
||||||
JFXKeyFrame jFXKeyFrame = new JFXKeyFrame();
|
|
||||||
jFXKeyFrame.duration = this.duration;
|
|
||||||
jFXKeyFrame.keyValues = this.keyValues;
|
|
||||||
jFXKeyFrame.animateCondition = this.animateCondition;
|
|
||||||
return jFXKeyFrame;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.jfoenix.transitions;
|
|
||||||
|
|
||||||
import javafx.animation.Interpolator;
|
|
||||||
import javafx.beans.value.WritableValue;
|
|
||||||
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Shadi Shaheen
|
|
||||||
* @version 1.0
|
|
||||||
* @since 2017-09-21
|
|
||||||
*/
|
|
||||||
|
|
||||||
public final class JFXKeyValue<T> {
|
|
||||||
|
|
||||||
private WritableValue<T> target;
|
|
||||||
private Supplier<WritableValue<T>> targetSupplier;
|
|
||||||
private Supplier<T> endValueSupplier;
|
|
||||||
private T endValue;
|
|
||||||
private Supplier<Boolean> animateCondition = () -> true;
|
|
||||||
private Interpolator interpolator;
|
|
||||||
|
|
||||||
private JFXKeyValue() {
|
|
||||||
}
|
|
||||||
|
|
||||||
// this builder is created to ensure type inference from method arguments
|
|
||||||
public static Builder builder() {
|
|
||||||
return new Builder();
|
|
||||||
}
|
|
||||||
|
|
||||||
public T getEndValue() {
|
|
||||||
return endValue == null ? endValueSupplier.get() : endValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
public WritableValue<T> getTarget() {
|
|
||||||
return target == null ? targetSupplier.get() : target;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Interpolator getInterpolator() {
|
|
||||||
return interpolator;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isValid() {
|
|
||||||
return animateCondition == null || animateCondition.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final class Builder {
|
|
||||||
public <T> JFXKeyValueBuilder<T> setTarget(WritableValue<T> target) {
|
|
||||||
JFXKeyValueBuilder<T> builder = new JFXKeyValueBuilder<>();
|
|
||||||
builder.setTarget(target);
|
|
||||||
return builder;
|
|
||||||
}
|
|
||||||
|
|
||||||
public <T> JFXKeyValueBuilder<T> setTargetSupplier(Supplier<WritableValue<T>> targetSupplier) {
|
|
||||||
JFXKeyValueBuilder<T> builder = new JFXKeyValueBuilder<>();
|
|
||||||
builder.setTargetSupplier(targetSupplier);
|
|
||||||
return builder;
|
|
||||||
}
|
|
||||||
|
|
||||||
public <T> JFXKeyValueBuilder<T> setEndValueSupplier(Supplier<T> endValueSupplier) {
|
|
||||||
JFXKeyValueBuilder<T> builder = new JFXKeyValueBuilder<>();
|
|
||||||
builder.setEndValueSupplier(endValueSupplier);
|
|
||||||
return builder;
|
|
||||||
}
|
|
||||||
|
|
||||||
public <T> JFXKeyValueBuilder<T> setEndValue(T endValue) {
|
|
||||||
JFXKeyValueBuilder<T> builder = new JFXKeyValueBuilder<>();
|
|
||||||
builder.setEndValue(endValue);
|
|
||||||
return builder;
|
|
||||||
}
|
|
||||||
|
|
||||||
public <T> JFXKeyValueBuilder<T> setAnimateCondition(Supplier<Boolean> animateCondition) {
|
|
||||||
JFXKeyValueBuilder<T> builder = new JFXKeyValueBuilder<>();
|
|
||||||
builder.setAnimateCondition(animateCondition);
|
|
||||||
return builder;
|
|
||||||
}
|
|
||||||
|
|
||||||
public <T> JFXKeyValueBuilder<T> setInterpolator(Interpolator interpolator) {
|
|
||||||
JFXKeyValueBuilder<T> builder = new JFXKeyValueBuilder<>();
|
|
||||||
builder.setInterpolator(interpolator);
|
|
||||||
return builder;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final class JFXKeyValueBuilder<T> {
|
|
||||||
|
|
||||||
private WritableValue<T> target;
|
|
||||||
private Supplier<WritableValue<T>> targetSupplier;
|
|
||||||
private Supplier<T> endValueSupplier;
|
|
||||||
private T endValue;
|
|
||||||
private Supplier<Boolean> animateCondition = () -> true;
|
|
||||||
private Interpolator interpolator = Interpolator.EASE_BOTH;
|
|
||||||
|
|
||||||
private JFXKeyValueBuilder() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public JFXKeyValueBuilder<T> setTarget(WritableValue<T> target) {
|
|
||||||
this.target = target;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public JFXKeyValueBuilder<T> setTargetSupplier(Supplier<WritableValue<T>> targetSupplier) {
|
|
||||||
this.targetSupplier = targetSupplier;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public JFXKeyValueBuilder<T> setEndValueSupplier(Supplier<T> endValueSupplier) {
|
|
||||||
this.endValueSupplier = endValueSupplier;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public JFXKeyValueBuilder<T> setEndValue(T endValue) {
|
|
||||||
this.endValue = endValue;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public JFXKeyValueBuilder<T> setAnimateCondition(Supplier<Boolean> animateCondition) {
|
|
||||||
this.animateCondition = animateCondition;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public JFXKeyValueBuilder<T> setInterpolator(Interpolator interpolator) {
|
|
||||||
this.interpolator = interpolator;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public JFXKeyValue<T> build() {
|
|
||||||
JFXKeyValue<T> jFXKeyValue = new JFXKeyValue<>();
|
|
||||||
jFXKeyValue.target = this.target;
|
|
||||||
jFXKeyValue.interpolator = this.interpolator;
|
|
||||||
jFXKeyValue.targetSupplier = this.targetSupplier;
|
|
||||||
jFXKeyValue.endValue = this.endValue;
|
|
||||||
jFXKeyValue.endValueSupplier = this.endValueSupplier;
|
|
||||||
jFXKeyValue.animateCondition = this.animateCondition;
|
|
||||||
return jFXKeyValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.jfoenix.utils;
|
|
||||||
|
|
||||||
import javafx.beans.value.ObservableBooleanValue;
|
|
||||||
import javafx.scene.Node;
|
|
||||||
import javafx.scene.Scene;
|
|
||||||
import javafx.scene.layout.Background;
|
|
||||||
import javafx.scene.layout.BackgroundFill;
|
|
||||||
import javafx.scene.layout.Region;
|
|
||||||
import javafx.scene.paint.Color;
|
|
||||||
import javafx.scene.paint.Paint;
|
|
||||||
import javafx.stage.Window;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
import java.lang.invoke.MethodHandle;
|
|
||||||
import java.lang.invoke.MethodHandles;
|
|
||||||
import java.lang.invoke.MethodType;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.function.Function;
|
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
|
||||||
|
|
||||||
/// @author Shadi Shaheen
|
|
||||||
/// @version 1.0
|
|
||||||
/// @since 2017-02-11
|
|
||||||
public final class JFXNodeUtils {
|
|
||||||
|
|
||||||
public static void updateBackground(Background newBackground, Region nodeToUpdate) {
|
|
||||||
updateBackground(newBackground, nodeToUpdate, Color.BLACK);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void updateBackground(Background newBackground, Region nodeToUpdate, Paint fill) {
|
|
||||||
if (newBackground != null && !newBackground.getFills().isEmpty()) {
|
|
||||||
final BackgroundFill[] fills = new BackgroundFill[newBackground.getFills().size()];
|
|
||||||
for (int i = 0; i < newBackground.getFills().size(); i++) {
|
|
||||||
BackgroundFill bf = newBackground.getFills().get(i);
|
|
||||||
fills[i] = new BackgroundFill(fill, bf.getRadii(), bf.getInsets());
|
|
||||||
}
|
|
||||||
nodeToUpdate.setBackground(new Background(fills));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String colorToHex(Color c) {
|
|
||||||
if (c != null) {
|
|
||||||
return String.format((Locale) null, "#%02X%02X%02X",
|
|
||||||
Math.round(c.getRed() * 255),
|
|
||||||
Math.round(c.getGreen() * 255),
|
|
||||||
Math.round(c.getBlue() * 255));
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final @NotNull Function<Node, ObservableBooleanValue> treeVisiblePropertyGetter = initTreeVisiblePropertyGetter();
|
|
||||||
|
|
||||||
private static @NotNull Function<Node, ObservableBooleanValue> initTreeVisiblePropertyGetter() {
|
|
||||||
|
|
||||||
MethodHandles.Lookup lookup;
|
|
||||||
try {
|
|
||||||
lookup = MethodHandles.privateLookupIn(Node.class, MethodHandles.lookup());
|
|
||||||
} catch (IllegalAccessException e) {
|
|
||||||
LOG.warning("Failed to get private lookup for Node", e);
|
|
||||||
return JFXNodeUtils::defaultTreeVisibleProperty;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Method treeVisiblePropertyMethod = Node.class.getDeclaredMethod("treeVisibleProperty");
|
|
||||||
if (!ObservableBooleanValue.class.isAssignableFrom(treeVisiblePropertyMethod.getReturnType())) {
|
|
||||||
LOG.warning("Node.treeVisibleProperty() does not return ObservableBooleanValue: " + treeVisiblePropertyMethod.getReturnType());
|
|
||||||
return JFXNodeUtils::defaultTreeVisibleProperty;
|
|
||||||
}
|
|
||||||
|
|
||||||
MethodHandle handle = lookup.unreflect(treeVisiblePropertyMethod)
|
|
||||||
.asType(MethodType.methodType(ObservableBooleanValue.class, Node.class));
|
|
||||||
return item -> {
|
|
||||||
try {
|
|
||||||
return (ObservableBooleanValue) handle.invokeExact((Node) item);
|
|
||||||
} catch (RuntimeException | Error e) {
|
|
||||||
throw e;
|
|
||||||
} catch (Throwable e) {
|
|
||||||
throw new AssertionError("Unreachable", e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} catch (Exception e) {
|
|
||||||
LOG.warning("Failed to get method handle for Node.treeVisibleProperty()", e);
|
|
||||||
return JFXNodeUtils::defaultTreeVisibleProperty;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// If `Node.treeVisibleProperty()` does not exist, use `Node.visibleProperty()` as a fallback
|
|
||||||
private static @NotNull ObservableBooleanValue defaultTreeVisibleProperty(Node item) {
|
|
||||||
return item.visibleProperty();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static @NotNull ObservableBooleanValue treeVisibleProperty(Node item) {
|
|
||||||
return treeVisiblePropertyGetter.apply(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isTreeVisible(Node item) {
|
|
||||||
return treeVisibleProperty(item).getValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isTreeShowing(Node node) {
|
|
||||||
if (node == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
Scene scene = node.getScene();
|
|
||||||
if (scene == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
Window window = scene.getWindow();
|
|
||||||
if (window == null || !window.isShowing())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return isTreeVisible(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
private JFXNodeUtils() {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved.
|
|
||||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
|
||||||
*
|
|
||||||
* This code is free software; you can redistribute it and/or modify it
|
|
||||||
* under the terms of the GNU General Public License version 2 only, as
|
|
||||||
* published by the Free Software Foundation. Oracle designates this
|
|
||||||
* particular file as subject to the "Classpath" exception as provided
|
|
||||||
* by Oracle in the LICENSE file that accompanied this code.
|
|
||||||
*
|
|
||||||
* This code is distributed in the hope that it will be useful, but WITHOUT
|
|
||||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
|
||||||
* version 2 for more details (a copy is included in the LICENSE file that
|
|
||||||
* accompanied this code).
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License version
|
|
||||||
* 2 along with this work; if not, write to the Free Software Foundation,
|
|
||||||
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
||||||
*
|
|
||||||
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
|
||||||
* or visit www.oracle.com if you need additional information or have any
|
|
||||||
* questions.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.jfoenix.utils;
|
|
||||||
|
|
||||||
import javafx.beans.property.ReadOnlyBooleanPropertyBase;
|
|
||||||
import javafx.beans.value.ChangeListener;
|
|
||||||
import javafx.beans.value.ObservableBooleanValue;
|
|
||||||
import javafx.scene.Node;
|
|
||||||
import javafx.scene.Scene;
|
|
||||||
import javafx.stage.Window;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used to observe changes in tree showing status for a {@link Node}. For a Node's tree to be showing
|
|
||||||
* it must be visible, its ancestors must be visible, the node must be part of a {@link Scene} and
|
|
||||||
* the scene must have a {@link Window} which is currently showing.<p>
|
|
||||||
*
|
|
||||||
* This class provides the exact same functionality as {@link JFXNodeUtils#isTreeShowing(Node)} in
|
|
||||||
* an observable form.
|
|
||||||
*/
|
|
||||||
public class TreeShowingProperty extends ReadOnlyBooleanPropertyBase {
|
|
||||||
private final ChangeListener<Boolean> windowShowingChangedListener = (obs, old, current) -> updateTreeShowing();
|
|
||||||
private final ChangeListener<Window> sceneWindowChangedListener = (obs, old, current) -> windowChanged(old, current);
|
|
||||||
private final ChangeListener<Scene> nodeSceneChangedListener = (obs, old, current) -> sceneChanged(old, current);
|
|
||||||
|
|
||||||
private final Node node;
|
|
||||||
private final ObservableBooleanValue treeVisibleProperty;
|
|
||||||
|
|
||||||
private boolean valid;
|
|
||||||
private boolean treeShowing;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new instance.
|
|
||||||
*
|
|
||||||
* @param node a {@link Node} for which the tree showing status should be observed, cannot be null
|
|
||||||
*/
|
|
||||||
public TreeShowingProperty(Node node) {
|
|
||||||
this.node = node;
|
|
||||||
this.treeVisibleProperty = JFXNodeUtils.treeVisibleProperty(node);
|
|
||||||
|
|
||||||
this.node.sceneProperty().addListener(nodeSceneChangedListener);
|
|
||||||
this.treeVisibleProperty.addListener(windowShowingChangedListener);
|
|
||||||
|
|
||||||
sceneChanged(null, node.getScene());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Object getBean() {
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return "treeShowing";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cleans up any listeners that this class may have registered on the {@link Node}
|
|
||||||
* that was supplied at construction.
|
|
||||||
*/
|
|
||||||
public void dispose() {
|
|
||||||
node.sceneProperty().removeListener(nodeSceneChangedListener);
|
|
||||||
|
|
||||||
if (treeVisibleProperty != null)
|
|
||||||
treeVisibleProperty.removeListener(windowShowingChangedListener);
|
|
||||||
|
|
||||||
valid = false; // prevents unregistration from triggering an invalidation notification
|
|
||||||
sceneChanged(node.getScene(), null);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void invalidate() {
|
|
||||||
if (valid) {
|
|
||||||
valid = false;
|
|
||||||
fireValueChangedEvent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean get() {
|
|
||||||
if (!valid) {
|
|
||||||
updateTreeShowing();
|
|
||||||
valid = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return treeShowing;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sceneChanged(Scene oldScene, Scene newScene) {
|
|
||||||
if (oldScene != null) {
|
|
||||||
oldScene.windowProperty().removeListener(sceneWindowChangedListener);
|
|
||||||
}
|
|
||||||
if (newScene != null) {
|
|
||||||
newScene.windowProperty().addListener(sceneWindowChangedListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
windowChanged(
|
|
||||||
oldScene == null ? null : oldScene.getWindow(),
|
|
||||||
newScene == null ? null : newScene.getWindow()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void windowChanged(Window oldWindow, Window newWindow) {
|
|
||||||
if (oldWindow != null) {
|
|
||||||
oldWindow.showingProperty().removeListener(windowShowingChangedListener);
|
|
||||||
}
|
|
||||||
if (newWindow != null) {
|
|
||||||
newWindow.showingProperty().addListener(windowShowingChangedListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateTreeShowing();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateTreeShowing() {
|
|
||||||
boolean newValue = JFXNodeUtils.isTreeShowing(node);
|
|
||||||
|
|
||||||
if (newValue != treeShowing) {
|
|
||||||
treeShowing = newValue;
|
|
||||||
invalidate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,20 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
|
/*
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
* Hello Minecraft! Launcher
|
||||||
|
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
package org.jackhuang.hmcl;
|
package org.jackhuang.hmcl;
|
||||||
|
|
||||||
import org.jackhuang.hmcl.util.FileSaver;
|
import org.jackhuang.hmcl.util.FileSaver;
|
||||||
@@ -8,10 +23,8 @@ import org.jackhuang.hmcl.util.SwingUtils;
|
|||||||
import org.jackhuang.hmcl.java.JavaRuntime;
|
import org.jackhuang.hmcl.java.JavaRuntime;
|
||||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||||
import org.jackhuang.hmcl.util.io.TLSSecurity;
|
|
||||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||||
|
|
||||||
import javax.swing.JOptionPane;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.invoke.MethodHandle;
|
import java.lang.invoke.MethodHandle;
|
||||||
import java.lang.invoke.MethodHandles;
|
import java.lang.invoke.MethodHandles;
|
||||||
@@ -28,23 +41,19 @@ public final class EntryPoint {
|
|||||||
private EntryPoint() {
|
private EntryPoint() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initializes TLS protection before downloading dependencies and starting the launcher.
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
System.getProperties().putIfAbsent("java.net.useSystemProxies", "true");
|
System.getProperties().putIfAbsent("java.net.useSystemProxies", "true");
|
||||||
System.getProperties().putIfAbsent("javafx.autoproxy.disable", "true");
|
System.getProperties().putIfAbsent("javafx.autoproxy.disable", "true");
|
||||||
System.getProperties().putIfAbsent("http.agent", "HMCL/" + Metadata.VERSION);
|
System.getProperties().putIfAbsent("http.agent", "HMCL/" + Metadata.VERSION);
|
||||||
|
|
||||||
createHMCLDirectories();
|
createHMCLDirectories();
|
||||||
LOG.start(Metadata.HMCL_LOCAL_HOME.resolve("logs"));
|
LOG.start(Metadata.HMCL_CURRENT_DIRECTORY.resolve("logs"));
|
||||||
TLSSecurity.install();
|
|
||||||
|
|
||||||
checkWine();
|
setupJavaFXVMOptions();
|
||||||
|
checkDirectoryPath();
|
||||||
|
|
||||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
|
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS && !isInsideMacAppBundle())
|
||||||
System.getProperties().putIfAbsent("apple.awt.application.appearance", "system");
|
initIcon();
|
||||||
if (!isInsideMacAppBundle())
|
|
||||||
initIcon();
|
|
||||||
}
|
|
||||||
|
|
||||||
checkJavaFX();
|
checkJavaFX();
|
||||||
verifyJavaFX();
|
verifyJavaFX();
|
||||||
@@ -60,30 +69,95 @@ public final class EntryPoint {
|
|||||||
System.exit(exitCode);
|
System.exit(exitCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void createHMCLDirectories() {
|
private static void setupJavaFXVMOptions() {
|
||||||
if (!Files.isDirectory(Metadata.HMCL_LOCAL_HOME)) {
|
if ("true".equalsIgnoreCase(System.getenv("HMCL_FORCE_GPU"))) {
|
||||||
|
LOG.info("HMCL_FORCE_GPU: true");
|
||||||
|
System.getProperties().putIfAbsent("prism.forceGPU", "true");
|
||||||
|
}
|
||||||
|
|
||||||
|
String animationFrameRate = System.getenv("HMCL_ANIMATION_FRAME_RATE");
|
||||||
|
if (animationFrameRate != null) {
|
||||||
|
LOG.info("HMCL_ANIMATION_FRAME_RATE: " + animationFrameRate);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Files.createDirectories(Metadata.HMCL_LOCAL_HOME);
|
if (Integer.parseInt(animationFrameRate) <= 0)
|
||||||
|
throw new NumberFormatException(animationFrameRate);
|
||||||
|
|
||||||
|
System.getProperties().putIfAbsent("javafx.animation.pulse", animationFrameRate);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
LOG.warning("Invalid animation frame rate: " + animationFrameRate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String uiScale = System.getProperty("hmcl.uiScale", System.getenv("HMCL_UI_SCALE"));
|
||||||
|
if (uiScale != null) {
|
||||||
|
uiScale = uiScale.trim();
|
||||||
|
|
||||||
|
LOG.info("HMCL_UI_SCALE: " + uiScale);
|
||||||
|
|
||||||
|
try {
|
||||||
|
float scaleValue;
|
||||||
|
if (uiScale.endsWith("%")) {
|
||||||
|
scaleValue = Integer.parseInt(uiScale.substring(0, uiScale.length() - 1)) / 100.0f;
|
||||||
|
} else if (uiScale.endsWith("dpi") || uiScale.endsWith("DPI")) {
|
||||||
|
scaleValue = Integer.parseInt(uiScale.substring(0, uiScale.length() - 3)) / 96.0f;
|
||||||
|
} else {
|
||||||
|
scaleValue = Float.parseFloat(uiScale);
|
||||||
|
}
|
||||||
|
|
||||||
|
float lowerBound;
|
||||||
|
float upperBound;
|
||||||
|
|
||||||
|
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||||
|
// JavaFX behavior may be abnormal when the DPI scaling factor is too high
|
||||||
|
lowerBound = 0.25f;
|
||||||
|
upperBound = 4f;
|
||||||
|
} else {
|
||||||
|
lowerBound = 0.01f;
|
||||||
|
upperBound = 10f;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scaleValue >= lowerBound && scaleValue <= upperBound) {
|
||||||
|
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||||
|
System.getProperties().putIfAbsent("glass.win.uiScale", uiScale);
|
||||||
|
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
|
||||||
|
LOG.warning("macOS does not support setting UI scale, so it will be ignored");
|
||||||
|
} else {
|
||||||
|
System.getProperties().putIfAbsent("glass.gtk.uiScale", uiScale);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOG.warning("UI scale out of range: " + uiScale);
|
||||||
|
}
|
||||||
|
} catch (Throwable e) {
|
||||||
|
LOG.warning("Invalid UI scale: " + uiScale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void createHMCLDirectories() {
|
||||||
|
if (!Files.isDirectory(Metadata.HMCL_CURRENT_DIRECTORY)) {
|
||||||
|
try {
|
||||||
|
Files.createDirectories(Metadata.HMCL_CURRENT_DIRECTORY);
|
||||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||||
try {
|
try {
|
||||||
Files.setAttribute(Metadata.HMCL_LOCAL_HOME, "dos:hidden", true);
|
Files.setAttribute(Metadata.HMCL_CURRENT_DIRECTORY, "dos:hidden", true);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
LOG.warning("Failed to set hidden attribute of " + Metadata.HMCL_LOCAL_HOME, e);
|
LOG.warning("Failed to set hidden attribute of " + Metadata.HMCL_CURRENT_DIRECTORY, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
// Logger has not been started yet, so print directly to System.err
|
// Logger has not been started yet, so print directly to System.err
|
||||||
System.err.println("Failed to create HMCL directory: " + Metadata.HMCL_LOCAL_HOME);
|
System.err.println("Failed to create HMCL directory: " + Metadata.HMCL_CURRENT_DIRECTORY);
|
||||||
e.printStackTrace(System.err);
|
e.printStackTrace(System.err);
|
||||||
showErrorAndExit(i18n("fatal.create_hmcl_current_directory_failure", Metadata.HMCL_LOCAL_HOME));
|
showErrorAndExit(i18n("fatal.create_hmcl_current_directory_failure", Metadata.HMCL_CURRENT_DIRECTORY));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Files.isDirectory(Metadata.HMCL_USER_HOME)) {
|
if (!Files.isDirectory(Metadata.HMCL_GLOBAL_DIRECTORY)) {
|
||||||
try {
|
try {
|
||||||
Files.createDirectories(Metadata.HMCL_USER_HOME);
|
Files.createDirectories(Metadata.HMCL_GLOBAL_DIRECTORY);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
LOG.warning("Failed to create HMCL user home " + Metadata.HMCL_USER_HOME, e);
|
LOG.warning("Failed to create HMCL global directory " + Metadata.HMCL_GLOBAL_DIRECTORY, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,12 +192,25 @@ public final class EntryPoint {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void checkDirectoryPath() {
|
||||||
|
String currentDir = System.getProperty("user.dir", "");
|
||||||
|
if (currentDir.contains("!")) {
|
||||||
|
LOG.error("The current working path contains an exclamation mark: " + currentDir);
|
||||||
|
// No Chinese translation because both Swing and JavaFX cannot render Chinese character properly when exclamation mark exists in the path.
|
||||||
|
showErrorAndExit("Exclamation mark(!) is not allowed in the path where HMCL is in.\n"
|
||||||
|
+ "The path is " + currentDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void checkJavaFX() {
|
private static void checkJavaFX() {
|
||||||
try {
|
try {
|
||||||
SelfDependencyPatcher.patch();
|
SelfDependencyPatcher.patch();
|
||||||
} catch (SelfDependencyPatcher.PatchException e) {
|
} catch (SelfDependencyPatcher.PatchException e) {
|
||||||
LOG.error("Unable to patch JVM", e);
|
LOG.error("Unable to patch JVM", e);
|
||||||
showErrorAndExit(i18n("fatal.javafx.missing"));
|
showErrorAndExit(i18n("fatal.javafx.missing"));
|
||||||
|
} catch (SelfDependencyPatcher.IncompatibleVersionException e) {
|
||||||
|
LOG.error("Unable to patch JVM", e);
|
||||||
|
showErrorAndExit(i18n("fatal.javafx.incompatible"));
|
||||||
} catch (CancellationException e) {
|
} catch (CancellationException e) {
|
||||||
LOG.error("User cancels downloading JavaFX", e);
|
LOG.error("User cancels downloading JavaFX", e);
|
||||||
exit(0);
|
exit(0);
|
||||||
@@ -144,13 +231,6 @@ public final class EntryPoint {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void checkWine() {
|
|
||||||
if (OperatingSystem.isRunningUnderWine()) {
|
|
||||||
LOG.warning("HMCL is running under Wine or its distributions!");
|
|
||||||
showWarning(i18n("fatal.wine_warning"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void addEnableNativeAccess() {
|
private static void addEnableNativeAccess() {
|
||||||
if (JavaRuntime.CURRENT_VERSION > 21) {
|
if (JavaRuntime.CURRENT_VERSION > 21) {
|
||||||
try {
|
try {
|
||||||
@@ -187,21 +267,10 @@ public final class EntryPoint {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void showWarning(String message) {
|
|
||||||
SwingUtils.initLookAndFeel();
|
|
||||||
|
|
||||||
int result = JOptionPane.showOptionDialog(null, message, i18n("message.warning"), JOptionPane.OK_CANCEL_OPTION,
|
|
||||||
JOptionPane.WARNING_MESSAGE, null, null, null);
|
|
||||||
|
|
||||||
if (result == JOptionPane.CANCEL_OPTION || result == JOptionPane.CLOSED_OPTION) {
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates that a fatal error has occurred, and that the application cannot start.
|
* Indicates that a fatal error has occurred, and that the application cannot start.
|
||||||
*/
|
*/
|
||||||
static void showErrorAndExit(String message) {
|
private static void showErrorAndExit(String message) {
|
||||||
SwingUtils.showErrorDialog(message);
|
SwingUtils.showErrorDialog(message);
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,49 +1,52 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
|
/*
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
* Hello Minecraft! Launcher
|
||||||
|
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
package org.jackhuang.hmcl;
|
package org.jackhuang.hmcl;
|
||||||
|
|
||||||
import com.sun.jna.Pointer;
|
|
||||||
import com.sun.jna.WString;
|
|
||||||
import javafx.animation.KeyFrame;
|
|
||||||
import javafx.animation.KeyValue;
|
|
||||||
import javafx.animation.Timeline;
|
|
||||||
import javafx.application.Application;
|
import javafx.application.Application;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.beans.binding.Bindings;
|
|
||||||
import javafx.beans.value.ObservableBooleanValue;
|
import javafx.beans.value.ObservableBooleanValue;
|
||||||
import javafx.geometry.Rectangle2D;
|
|
||||||
import javafx.scene.control.Alert;
|
import javafx.scene.control.Alert;
|
||||||
import javafx.scene.control.Alert.AlertType;
|
import javafx.scene.control.Alert.AlertType;
|
||||||
import javafx.scene.control.Button;
|
|
||||||
import javafx.scene.control.ButtonType;
|
import javafx.scene.control.ButtonType;
|
||||||
import javafx.stage.Screen;
|
import javafx.scene.input.Clipboard;
|
||||||
|
import javafx.scene.input.DataFormat;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import javafx.util.Duration;
|
import org.jackhuang.hmcl.setting.ConfigHolder;
|
||||||
import org.jackhuang.hmcl.game.HMCLCacheRepository;
|
import org.jackhuang.hmcl.setting.SambaException;
|
||||||
import org.jackhuang.hmcl.setting.*;
|
import org.jackhuang.hmcl.ui.FXUtils;
|
||||||
|
import org.jackhuang.hmcl.util.FileSaver;
|
||||||
import org.jackhuang.hmcl.task.AsyncTaskExecutor;
|
import org.jackhuang.hmcl.task.AsyncTaskExecutor;
|
||||||
import org.jackhuang.hmcl.task.Schedulers;
|
import org.jackhuang.hmcl.task.Schedulers;
|
||||||
import org.jackhuang.hmcl.ui.Controllers;
|
import org.jackhuang.hmcl.ui.Controllers;
|
||||||
import org.jackhuang.hmcl.ui.FXUtils;
|
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
import org.jackhuang.hmcl.upgrade.UpdateHandler;
|
||||||
import org.jackhuang.hmcl.util.*;
|
import org.jackhuang.hmcl.util.CrashReporter;
|
||||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
import org.jackhuang.hmcl.util.Lang;
|
||||||
|
import org.jackhuang.hmcl.util.StringUtils;
|
||||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||||
import org.jackhuang.hmcl.util.platform.*;
|
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||||
import org.jackhuang.hmcl.util.platform.macos.LibSystem;
|
import org.jackhuang.hmcl.util.platform.CommandBuilder;
|
||||||
import org.jackhuang.hmcl.util.platform.windows.Gdi32;
|
import org.jackhuang.hmcl.util.platform.NativeUtils;
|
||||||
import org.jackhuang.hmcl.util.platform.windows.Shell32;
|
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||||
import org.jackhuang.hmcl.util.platform.windows.User32;
|
import org.jackhuang.hmcl.util.platform.SystemInfo;
|
||||||
import org.jackhuang.hmcl.util.platform.windows.WinReg;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
import javax.swing.*;
|
|
||||||
import java.awt.*;
|
|
||||||
import java.awt.datatransfer.StringSelection;
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.lang.management.ManagementFactory;
|
import java.lang.management.ManagementFactory;
|
||||||
import java.lang.management.MemoryPoolMXBean;
|
import java.lang.management.MemoryPoolMXBean;
|
||||||
import java.net.CookieHandler;
|
import java.net.CookieHandler;
|
||||||
@@ -51,21 +54,17 @@ import java.net.CookieManager;
|
|||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.text.DecimalFormat;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.Collections;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.setting.SettingsManager.settings;
|
|
||||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||||
import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
|
import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
|
||||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
|
||||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||||
|
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||||
|
|
||||||
/// The main JavaFX application entry point.
|
|
||||||
public final class Launcher extends Application {
|
public final class Launcher extends Application {
|
||||||
public static final CookieManager COOKIE_MANAGER = new CookieManager();
|
public static final CookieManager COOKIE_MANAGER = new CookieManager();
|
||||||
|
|
||||||
@@ -80,51 +79,50 @@ public final class Launcher extends Application {
|
|||||||
LOG.info("Dark Mode: " + Optional.ofNullable(FXUtils.DARK_MODE).map(ObservableBooleanValue::get).orElse(false));
|
LOG.info("Dark Mode: " + Optional.ofNullable(FXUtils.DARK_MODE).map(ObservableBooleanValue::get).orElse(false));
|
||||||
LOG.info("Reduced Motion: " + Objects.requireNonNullElse(FXUtils.REDUCED_MOTION, false));
|
LOG.info("Reduced Motion: " + Objects.requireNonNullElse(FXUtils.REDUCED_MOTION, false));
|
||||||
|
|
||||||
if (Screen.getScreens().isEmpty()) {
|
|
||||||
LOG.info("No screen");
|
|
||||||
} else {
|
|
||||||
StringBuilder builder = new StringBuilder("Screens:");
|
|
||||||
int count = 0;
|
|
||||||
for (Screen screen : Screen.getScreens()) {
|
|
||||||
builder.append("\n - Screen ").append(++count).append(": ");
|
|
||||||
appendScreen(builder, screen);
|
|
||||||
}
|
|
||||||
LOG.info(builder.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
initializeSettingsRuntime();
|
try {
|
||||||
|
ConfigHolder.init();
|
||||||
|
} catch (SambaException e) {
|
||||||
|
showAlert(AlertType.WARNING, i18n("fatal.samba"));
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOG.error("Failed to load config", e);
|
||||||
|
checkConfigInTempDir();
|
||||||
|
checkConfigOwner();
|
||||||
|
showAlert(AlertType.ERROR, i18n("fatal.config_loading_failure", ConfigHolder.configLocation().getParent()));
|
||||||
|
EntryPoint.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
// https://lapcatsoftware.com/articles/app-translocation.html
|
// https://lapcatsoftware.com/articles/app-translocation.html
|
||||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS
|
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS
|
||||||
&& SettingsManager.isNewlyCreated()
|
&& ConfigHolder.isNewlyCreated()
|
||||||
&& System.getProperty("user.dir").startsWith("/private/var/folders/")) {
|
&& System.getProperty("user.dir").startsWith("/private/var/folders/")) {
|
||||||
if (!confirmWithCountdown(AlertType.WARNING, i18n("fatal.mac_app_translocation"), 5))
|
if (showAlert(AlertType.WARNING, i18n("fatal.mac_app_translocation"), ButtonType.YES, ButtonType.NO) == ButtonType.NO)
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
checkConfigInTempDir();
|
checkConfigInTempDir();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SettingsManager.isOwnerChanged()) {
|
if (ConfigHolder.isOwnerChanged()) {
|
||||||
if (showAlert(AlertType.WARNING, i18n("fatal.config_change_owner_root"), ButtonType.YES, ButtonType.NO) == ButtonType.NO)
|
if (showAlert(AlertType.WARNING, i18n("fatal.config_change_owner_root"), ButtonType.YES, ButtonType.NO) == ButtonType.NO)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SettingsManager.hasReadOnlyCoreSettings()) {
|
if (ConfigHolder.isUnsupportedVersion()) {
|
||||||
showAlert(AlertType.WARNING, i18n("fatal.config_unsupported_version"));
|
showAlert(AlertType.WARNING, i18n("fatal.config_unsupported_version"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Metadata.HMCL_LOCAL_HOME.toString().indexOf('=') >= 0) {
|
if (Metadata.HMCL_CURRENT_DIRECTORY.toString().indexOf('=') >= 0) {
|
||||||
showAlert(AlertType.WARNING, i18n("fatal.illegal_char"));
|
showAlert(AlertType.WARNING, i18n("fatal.illegal_char"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// runLater to ensure SettingsManager.init() finished initialization
|
// runLater to ensure ConfigHolder.init() finished initialization
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
// When launcher visibility is set to "hide and reopen" without Platform.implicitExit = false,
|
// When launcher visibility is set to "hide and reopen" without Platform.implicitExit = false,
|
||||||
// Stage.show() cannot work again because JavaFX Toolkit have already shut down.
|
// Stage.show() cannot work again because JavaFX Toolkit have already shut down.
|
||||||
Platform.setImplicitExit(false);
|
Platform.setImplicitExit(false);
|
||||||
Controllers.initialize(primaryStage);
|
Controllers.initialize(primaryStage);
|
||||||
|
|
||||||
|
UpdateChecker.init();
|
||||||
|
|
||||||
primaryStage.show();
|
primaryStage.show();
|
||||||
});
|
});
|
||||||
@@ -133,85 +131,12 @@ public final class Launcher extends Application {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initializes modules and runtime services that depend on loaded settings.
|
|
||||||
private static void initializeSettingsRuntime() {
|
|
||||||
DownloadProviders.init();
|
|
||||||
ProxyManager.init();
|
|
||||||
Accounts.init();
|
|
||||||
GameDirectoryManager.init();
|
|
||||||
AuthlibInjectorServers.init();
|
|
||||||
AnimationUtils.init();
|
|
||||||
|
|
||||||
CacheRepository.setInstance(HMCLCacheRepository.REPOSITORY);
|
|
||||||
HMCLCacheRepository.REPOSITORY.directoryProperty().bind(Bindings.createStringBinding(() -> {
|
|
||||||
String commonDirectory = settings().getResolvedCommonDirectory();
|
|
||||||
if (commonDirectory != null && FileUtils.canCreateDirectory(commonDirectory)) {
|
|
||||||
return commonDirectory;
|
|
||||||
} else {
|
|
||||||
return LauncherSettings.getDefaultCommonDirectory();
|
|
||||||
}
|
|
||||||
}, settings().commonDirectoryProperty(), settings().commonDirectoryTypeProperty()));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void appendScreen(StringBuilder builder, Screen screen) {
|
|
||||||
Rectangle2D bounds = screen.getBounds();
|
|
||||||
double scale = screen.getOutputScaleX();
|
|
||||||
|
|
||||||
builder.append(Math.round(bounds.getWidth() * scale));
|
|
||||||
builder.append('x');
|
|
||||||
builder.append(Math.round(bounds.getHeight() * scale));
|
|
||||||
|
|
||||||
DecimalFormat decimalFormat = new DecimalFormat("#.##");
|
|
||||||
|
|
||||||
if (scale != 1.0) {
|
|
||||||
builder.append(" @ ");
|
|
||||||
builder.append(decimalFormat.format(scale));
|
|
||||||
builder.append('x');
|
|
||||||
}
|
|
||||||
|
|
||||||
double dpi = screen.getDpi();
|
|
||||||
builder.append(' ');
|
|
||||||
builder.append(decimalFormat.format(dpi));
|
|
||||||
builder.append("dpi");
|
|
||||||
|
|
||||||
builder.append(" in ")
|
|
||||||
.append(Math.round(Math.sqrt(bounds.getWidth() * bounds.getWidth() + bounds.getHeight() * bounds.getHeight()) / dpi))
|
|
||||||
.append('"');
|
|
||||||
|
|
||||||
builder.append(" (").append(decimalFormat.format(bounds.getMinX()))
|
|
||||||
.append(", ").append(decimalFormat.format(bounds.getMinY()))
|
|
||||||
.append(", ").append(decimalFormat.format(bounds.getMaxX()))
|
|
||||||
.append(", ").append(decimalFormat.format(bounds.getMaxY()))
|
|
||||||
.append(")");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ButtonType showAlert(AlertType alertType, String contentText, ButtonType... buttons) {
|
private static ButtonType showAlert(AlertType alertType, String contentText, ButtonType... buttons) {
|
||||||
return new Alert(alertType, contentText, buttons).showAndWait().orElse(null);
|
return new Alert(alertType, contentText, buttons).showAndWait().orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean confirmWithCountdown(Alert.AlertType alertType, String contentText, int seconds) {
|
|
||||||
Alert alert = new Alert(alertType, contentText, ButtonType.YES, ButtonType.NO);
|
|
||||||
Button okButton = (Button) alert.getDialogPane().lookupButton(ButtonType.YES);
|
|
||||||
|
|
||||||
okButton.setDisable(true);
|
|
||||||
|
|
||||||
KeyFrame[] keyFrames = new KeyFrame[seconds + 1];
|
|
||||||
for (int i = 0; i < seconds; i++) {
|
|
||||||
keyFrames[i] = new KeyFrame(Duration.seconds(i),
|
|
||||||
new KeyValue(okButton.textProperty(), i18n("button.ok.countdown", seconds - i)));
|
|
||||||
}
|
|
||||||
keyFrames[seconds] = new KeyFrame(Duration.seconds(seconds),
|
|
||||||
new KeyValue(okButton.textProperty(), i18n("button.ok")),
|
|
||||||
new KeyValue(okButton.disableProperty(), false));
|
|
||||||
|
|
||||||
Timeline timeline = new Timeline(keyFrames);
|
|
||||||
alert.setOnShown(e -> timeline.play());
|
|
||||||
alert.setOnCloseRequest(e -> timeline.stop());
|
|
||||||
return alert.showAndWait().orElse(null) == ButtonType.YES;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isConfigInTempDir() {
|
private static boolean isConfigInTempDir() {
|
||||||
String configPath = SettingsManager.localConfigDirectory().toString();
|
String configPath = ConfigHolder.configLocation().toString();
|
||||||
|
|
||||||
String tmpdir = System.getProperty("java.io.tmpdir");
|
String tmpdir = System.getProperty("java.io.tmpdir");
|
||||||
if (StringUtils.isNotBlank(tmpdir) && configPath.startsWith(tmpdir))
|
if (StringUtils.isNotBlank(tmpdir) && configPath.startsWith(tmpdir))
|
||||||
@@ -248,20 +173,64 @@ public final class Launcher extends Application {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void checkConfigInTempDir() {
|
private static void checkConfigInTempDir() {
|
||||||
if (SettingsManager.isNewlyCreated() && isConfigInTempDir()
|
if (ConfigHolder.isNewlyCreated() && isConfigInTempDir()
|
||||||
&& !confirmWithCountdown(AlertType.WARNING, i18n("fatal.config_in_temp_dir"), 5)) {
|
&& showAlert(AlertType.WARNING, i18n("fatal.config_in_temp_dir"), ButtonType.YES, ButtonType.NO) == ButtonType.NO) {
|
||||||
EntryPoint.exit(0);
|
EntryPoint.exit(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void checkConfigOwner() {
|
||||||
|
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
|
||||||
|
return;
|
||||||
|
|
||||||
|
String userName = System.getProperty("user.name");
|
||||||
|
String owner;
|
||||||
|
try {
|
||||||
|
owner = Files.getOwner(ConfigHolder.configLocation()).getName();
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
LOG.warning("Failed to get file owner", ioe);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Files.isWritable(ConfigHolder.configLocation()) || userName.equals("root") || userName.equals(owner))
|
||||||
|
return;
|
||||||
|
|
||||||
|
ArrayList<String> files = new ArrayList<>();
|
||||||
|
files.add(ConfigHolder.configLocation().toString());
|
||||||
|
if (Files.exists(Metadata.HMCL_GLOBAL_DIRECTORY))
|
||||||
|
files.add(Metadata.HMCL_GLOBAL_DIRECTORY.toString());
|
||||||
|
if (Files.exists(Metadata.HMCL_CURRENT_DIRECTORY))
|
||||||
|
files.add(Metadata.HMCL_CURRENT_DIRECTORY.toString());
|
||||||
|
|
||||||
|
Path mcDir = Paths.get(".minecraft").toAbsolutePath().normalize();
|
||||||
|
if (Files.exists(mcDir))
|
||||||
|
files.add(mcDir.toString());
|
||||||
|
|
||||||
|
String command = new CommandBuilder().add("sudo", "chown", "-R", userName).addAll(files).toString();
|
||||||
|
ButtonType copyAndExit = new ButtonType(i18n("button.copy_and_exit"));
|
||||||
|
|
||||||
|
if (showAlert(AlertType.ERROR,
|
||||||
|
i18n("fatal.config_loading_failure.unix", owner, command),
|
||||||
|
copyAndExit, ButtonType.CLOSE) == copyAndExit) {
|
||||||
|
Clipboard.getSystemClipboard()
|
||||||
|
.setContent(Collections.singletonMap(DataFormat.PLAIN_TEXT, command));
|
||||||
|
}
|
||||||
|
EntryPoint.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void stop() throws Exception {
|
public void stop() throws Exception {
|
||||||
Controllers.onApplicationStop();
|
Controllers.onApplicationStop();
|
||||||
SettingsManager.shutdown();
|
FileSaver.shutdown();
|
||||||
LOG.shutdown();
|
LOG.shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
|
if (UpdateHandler.processArguments(args)) {
|
||||||
|
LOG.shutdown();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Thread.setDefaultUncaughtExceptionHandler(CRASH_REPORTER);
|
Thread.setDefaultUncaughtExceptionHandler(CRASH_REPORTER);
|
||||||
AsyncTaskExecutor.setUncaughtExceptionHandler(new CrashReporter(false));
|
AsyncTaskExecutor.setUncaughtExceptionHandler(new CrashReporter(false));
|
||||||
|
|
||||||
@@ -274,10 +243,6 @@ public final class Launcher extends Application {
|
|||||||
LOG.info("Processor Identifier: " + System.getenv("PROCESSOR_IDENTIFIER"));
|
LOG.info("Processor Identifier: " + System.getenv("PROCESSOR_IDENTIFIER"));
|
||||||
}
|
}
|
||||||
LOG.info("System Architecture: " + Architecture.SYSTEM_ARCH.getDisplayName());
|
LOG.info("System Architecture: " + Architecture.SYSTEM_ARCH.getDisplayName());
|
||||||
try {
|
|
||||||
LOG.info("Process PID: " + ProcessHandle.current().pid());
|
|
||||||
} catch (UnsupportedOperationException ignored) {
|
|
||||||
}
|
|
||||||
LOG.info("Native Encoding: " + OperatingSystem.NATIVE_CHARSET);
|
LOG.info("Native Encoding: " + OperatingSystem.NATIVE_CHARSET);
|
||||||
LOG.info("JNU Encoding: " + System.getProperty("sun.jnu.encoding"));
|
LOG.info("JNU Encoding: " + System.getProperty("sun.jnu.encoding"));
|
||||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||||
@@ -288,10 +253,10 @@ public final class Launcher extends Application {
|
|||||||
LOG.info("Java VM Version: " + System.getProperty("java.vm.name") + " (" + System.getProperty("java.vm.info") + "), " + System.getProperty("java.vm.vendor"));
|
LOG.info("Java VM Version: " + System.getProperty("java.vm.name") + " (" + System.getProperty("java.vm.info") + "), " + System.getProperty("java.vm.vendor"));
|
||||||
LOG.info("Java Home: " + System.getProperty("java.home"));
|
LOG.info("Java Home: " + System.getProperty("java.home"));
|
||||||
LOG.info("Current Directory: " + Metadata.CURRENT_DIRECTORY);
|
LOG.info("Current Directory: " + Metadata.CURRENT_DIRECTORY);
|
||||||
LOG.info("HMCL User Home: " + Metadata.HMCL_USER_HOME);
|
LOG.info("HMCL Global Directory: " + Metadata.HMCL_GLOBAL_DIRECTORY);
|
||||||
LOG.info("HMCL Local Home: " + Metadata.HMCL_LOCAL_HOME);
|
LOG.info("HMCL Current Directory: " + Metadata.HMCL_CURRENT_DIRECTORY);
|
||||||
LOG.info("HMCL Jar Path: " + Objects.requireNonNullElse(JarUtils.thisJarPath(), "Not Found"));
|
LOG.info("HMCL Jar Path: " + Lang.requireNonNullElse(JarUtils.thisJarPath(), "Not Found"));
|
||||||
LOG.info("HMCL Log File: " + Objects.requireNonNullElse(LOG.getLogFile(), "In Memory"));
|
LOG.info("HMCL Log File: " + Lang.requireNonNullElse(LOG.getLogFile(), "In Memory"));
|
||||||
LOG.info("JVM Max Memory: " + MEGABYTES.formatBytes(Runtime.getRuntime().maxMemory()));
|
LOG.info("JVM Max Memory: " + MEGABYTES.formatBytes(Runtime.getRuntime().maxMemory()));
|
||||||
try {
|
try {
|
||||||
for (MemoryPoolMXBean bean : ManagementFactory.getMemoryPoolMXBeans()) {
|
for (MemoryPoolMXBean bean : ManagementFactory.getMemoryPoolMXBeans()) {
|
||||||
@@ -309,27 +274,8 @@ public final class Launcher extends Application {
|
|||||||
LOG.info("XDG Current Desktop: " + System.getenv("XDG_CURRENT_DESKTOP"));
|
LOG.info("XDG Current Desktop: " + System.getenv("XDG_CURRENT_DESKTOP"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable Boolean isRoot = isRootUser();
|
|
||||||
LOG.info("Administrator User: " + (isRoot == null ? "Unknown" : isRoot.toString()));
|
|
||||||
|
|
||||||
LOG.info("Zlib Compatible: " + ZlibUtils.IS_ZLIB_COMPATIBLE);
|
|
||||||
|
|
||||||
Lang.thread(SystemInfo::initialize, "Detection System Information", true);
|
Lang.thread(SystemInfo::initialize, "Detection System Information", true);
|
||||||
|
|
||||||
try {
|
|
||||||
SettingsManager.init();
|
|
||||||
} catch (SambaException e) {
|
|
||||||
EntryPoint.showWarning(i18n("fatal.samba"));
|
|
||||||
} catch (IOException e) {
|
|
||||||
LOG.error("Failed to load config", e);
|
|
||||||
checkConfigOwner();
|
|
||||||
SwingUtils.showErrorDialog(i18n("fatal.config_loading_failure", SettingsManager.localConfigDirectory()));
|
|
||||||
EntryPoint.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
setupJavaFXVMOptions();
|
|
||||||
setupWindowsAppUserModelID();
|
|
||||||
|
|
||||||
launch(Launcher.class, args);
|
launch(Launcher.class, args);
|
||||||
} catch (Throwable e) { // Fucking JavaFX will suppress the exception and will break our crash reporter.
|
} catch (Throwable e) { // Fucking JavaFX will suppress the exception and will break our crash reporter.
|
||||||
CRASH_REPORTER.uncaughtException(Thread.currentThread(), e);
|
CRASH_REPORTER.uncaughtException(Thread.currentThread(), e);
|
||||||
@@ -362,283 +308,5 @@ public final class Launcher extends Application {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the process-level AppUserModelID on Windows so the launcher groups correctly on the taskbar.
|
|
||||||
private static void setupWindowsAppUserModelID() {
|
|
||||||
if (OperatingSystem.CURRENT_OS != OperatingSystem.WINDOWS)
|
|
||||||
return;
|
|
||||||
if (!NativeUtils.USE_JNA || Shell32.INSTANCE == null)
|
|
||||||
return;
|
|
||||||
@Nullable Path thisJar = JarUtils.thisJarPath();
|
|
||||||
if (thisJar == null || !Files.isRegularFile(thisJar) || !"exe".equalsIgnoreCase(FileUtils.getExtension(thisJar)))
|
|
||||||
return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
int hr = Shell32.INSTANCE.SetCurrentProcessExplicitAppUserModelID(new WString(Metadata.WINDOWS_APP_USER_MODEL_ID));
|
|
||||||
if (hr < 0) {
|
|
||||||
LOG.warning("Failed to set AppUserModelID, HRESULT=0x" + Integer.toHexString(hr));
|
|
||||||
} else {
|
|
||||||
LOG.info("Set AppUserModelID: " + Metadata.WINDOWS_APP_USER_MODEL_ID);
|
|
||||||
}
|
|
||||||
} catch (Throwable e) {
|
|
||||||
LOG.warning("Failed to set AppUserModelID", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets JavaFX VM options before the toolkit starts.
|
|
||||||
/// On Wayland, the UI scale is auto-detected when the user did not configure one explicitly.
|
|
||||||
private static void setupJavaFXVMOptions() {
|
|
||||||
if ("true".equalsIgnoreCase(System.getenv("HMCL_FORCE_GPU"))) {
|
|
||||||
LOG.info("HMCL_FORCE_GPU: true");
|
|
||||||
System.getProperties().putIfAbsent("prism.forceGPU", "true");
|
|
||||||
}
|
|
||||||
|
|
||||||
setUpAnimationFrameRate:
|
|
||||||
{
|
|
||||||
if (System.getProperty("javafx.animation.pulse") != null) {
|
|
||||||
break setUpAnimationFrameRate;
|
|
||||||
}
|
|
||||||
|
|
||||||
String animationFrameRate = System.getenv("HMCL_ANIMATION_FRAME_RATE");
|
|
||||||
if (animationFrameRate != null) {
|
|
||||||
LOG.info("HMCL_ANIMATION_FRAME_RATE: " + animationFrameRate);
|
|
||||||
|
|
||||||
try {
|
|
||||||
int value = Integer.parseInt(animationFrameRate);
|
|
||||||
if (value <= 0)
|
|
||||||
throw new NumberFormatException(animationFrameRate);
|
|
||||||
|
|
||||||
if (value != 60)
|
|
||||||
System.setProperty("javafx.animation.pulse", animationFrameRate);
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
LOG.warning("Invalid animation frame rate: " + animationFrameRate);
|
|
||||||
}
|
|
||||||
break setUpAnimationFrameRate;
|
|
||||||
}
|
|
||||||
|
|
||||||
// To avoid prematurely loading FXUtils, we only check if animationDisabled has been explicitly set to true
|
|
||||||
if (Boolean.TRUE.equals(settings().animationDisabledProperty().get()))
|
|
||||||
break setUpAnimationFrameRate;
|
|
||||||
|
|
||||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
|
||||||
if (NativeUtils.USE_JNA && Gdi32.INSTANCE != null && User32.INSTANCE != null) {
|
|
||||||
@Nullable Pointer pointer = User32.INSTANCE.GetDC(Pointer.NULL);
|
|
||||||
if (pointer != null) {
|
|
||||||
try {
|
|
||||||
int refreshRate = Gdi32.INSTANCE.GetDeviceCaps(pointer, Gdi32.VREFRESH);
|
|
||||||
|
|
||||||
if (refreshRate > 0) {
|
|
||||||
LOG.info("Detected refresh rate: " + refreshRate + "Hz");
|
|
||||||
|
|
||||||
if (refreshRate >= 90) {
|
|
||||||
System.getProperties().putIfAbsent("javafx.animation.pulse", String.valueOf(refreshRate));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
User32.INSTANCE.ReleaseDC(Pointer.NULL, pointer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
float uiScaleLowerBound;
|
|
||||||
float uiScaleUpperBound;
|
|
||||||
|
|
||||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
|
||||||
// JavaFX behavior may be abnormal when the DPI scaling factor is too high
|
|
||||||
uiScaleLowerBound = 0.25f;
|
|
||||||
uiScaleUpperBound = 4f;
|
|
||||||
} else {
|
|
||||||
uiScaleLowerBound = 0.01f;
|
|
||||||
uiScaleUpperBound = 10f;
|
|
||||||
}
|
|
||||||
|
|
||||||
String uiScale = System.getProperty("hmcl.uiScale", System.getenv("HMCL_UI_SCALE"));
|
|
||||||
|
|
||||||
float scaleValue = Float.NaN;
|
|
||||||
if (uiScale != null && !(uiScale = uiScale.trim()).isEmpty()) {
|
|
||||||
LOG.info("HMCL_UI_SCALE: " + uiScale);
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (uiScale.endsWith("%")) {
|
|
||||||
scaleValue = Integer.parseInt(uiScale.substring(0, uiScale.length() - 1)) / 100.0f;
|
|
||||||
} else if (uiScale.endsWith("dpi") || uiScale.endsWith("DPI")) {
|
|
||||||
scaleValue = Integer.parseInt(uiScale.substring(0, uiScale.length() - 3)) / 96.0f;
|
|
||||||
} else {
|
|
||||||
scaleValue = Float.parseFloat(uiScale);
|
|
||||||
}
|
|
||||||
} catch (Throwable e) {
|
|
||||||
LOG.warning("Invalid UI scale: " + uiScale);
|
|
||||||
}
|
|
||||||
} else if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
|
|
||||||
String xdgSessionType = Objects.requireNonNullElse(System.getenv("XDG_SESSION_TYPE"), "");
|
|
||||||
String xdgCurrentDesktop = Objects.requireNonNullElse(System.getenv("XDG_CURRENT_DESKTOP"), "");
|
|
||||||
|
|
||||||
if ("wayland".equals(xdgSessionType)
|
|
||||||
&& StringUtils.startsWithIgnoreCase(xdgCurrentDesktop, "KDE")) {
|
|
||||||
try {
|
|
||||||
@Nullable Path xrdb = SystemUtils.which("xrdb");
|
|
||||||
@Nullable String xftDpi = xrdb != null ? SystemUtils.run(List.of(FileUtils.getAbsolutePath(xrdb), "-query"), inputStream -> {
|
|
||||||
@Nullable String dpiLine;
|
|
||||||
|
|
||||||
try (var reader = new InputStreamReader(inputStream, OperatingSystem.NATIVE_CHARSET);
|
|
||||||
BufferedReader bufferedReader = new BufferedReader(reader);
|
|
||||||
Stream<String> lines = bufferedReader.lines()) {
|
|
||||||
dpiLine = lines.filter(line -> line.trim().startsWith("Xft.dpi:"))
|
|
||||||
.findFirst().orElse(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
return dpiLine != null
|
|
||||||
? dpiLine.substring("Xft.dpi:".length()).trim()
|
|
||||||
: null;
|
|
||||||
}, java.time.Duration.ofSeconds(1)) : null;
|
|
||||||
|
|
||||||
if (xftDpi != null) {
|
|
||||||
float dpiValue = Float.parseFloat(xftDpi);
|
|
||||||
float scale = dpiValue / 96f;
|
|
||||||
|
|
||||||
if (scale > 1 && scale <= 10) {
|
|
||||||
LOG.info("Detected Xft.dpi: %s (%.1fx)".formatted(dpiValue, scale));
|
|
||||||
scaleValue = scale;
|
|
||||||
uiScale = Float.toString(scale);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
LOG.warning("Failed to read Xft.dpi from xrdb", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Float.isFinite(scaleValue)) {
|
|
||||||
if (scaleValue >= uiScaleLowerBound && scaleValue <= uiScaleUpperBound) {
|
|
||||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
|
||||||
System.getProperties().putIfAbsent("glass.win.uiScale", uiScale);
|
|
||||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
|
|
||||||
LOG.warning("macOS does not support setting UI scale, so it will be ignored");
|
|
||||||
} else {
|
|
||||||
System.getProperties().putIfAbsent("glass.gtk.uiScale", uiScale);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LOG.warning("UI scale out of range: " + uiScale);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void checkConfigOwner() {
|
|
||||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
|
|
||||||
return;
|
|
||||||
|
|
||||||
String userName = System.getProperty("user.name");
|
|
||||||
Path configDirectory = SettingsManager.localConfigDirectory();
|
|
||||||
if (!Files.exists(configDirectory)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String owner;
|
|
||||||
try {
|
|
||||||
owner = Files.getOwner(configDirectory).getName();
|
|
||||||
} catch (IOException ioe) {
|
|
||||||
LOG.warning("Failed to get file owner", ioe);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Files.isWritable(configDirectory) || userName.equals("root") || userName.equals(owner))
|
|
||||||
return;
|
|
||||||
|
|
||||||
ArrayList<String> files = new ArrayList<>();
|
|
||||||
files.add(configDirectory.toString());
|
|
||||||
if (Files.exists(Metadata.HMCL_USER_HOME))
|
|
||||||
files.add(Metadata.HMCL_USER_HOME.toString());
|
|
||||||
|
|
||||||
Path mcDir = Paths.get(".minecraft").toAbsolutePath().normalize();
|
|
||||||
if (Files.exists(mcDir))
|
|
||||||
files.add(mcDir.toString());
|
|
||||||
|
|
||||||
String command = new CommandBuilder().addAll("sudo", "chown", "-R", userName).addAll(files).toString();
|
|
||||||
SwingUtils.initLookAndFeel();
|
|
||||||
|
|
||||||
Object[] options = {i18n("button.copy_and_exit"), i18n("button.cancel")};
|
|
||||||
int result = JOptionPane.showOptionDialog(null,
|
|
||||||
i18n("fatal.config_loading_failure.unix", owner, command),
|
|
||||||
i18n("message.error"),
|
|
||||||
JOptionPane.DEFAULT_OPTION,
|
|
||||||
JOptionPane.ERROR_MESSAGE,
|
|
||||||
null,
|
|
||||||
options,
|
|
||||||
options[0]);
|
|
||||||
|
|
||||||
if (result == 0) {
|
|
||||||
try {
|
|
||||||
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(command), null);
|
|
||||||
} catch (Throwable e) {
|
|
||||||
LOG.warning("Failed to copy command to clipboard", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EntryPoint.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static @Nullable Boolean isRootUser() {
|
|
||||||
switch (OperatingSystem.CURRENT_OS) {
|
|
||||||
case WINDOWS -> {
|
|
||||||
WinReg reg = WinReg.INSTANCE;
|
|
||||||
|
|
||||||
if (reg == null)
|
|
||||||
return null;
|
|
||||||
try {
|
|
||||||
return reg.exists(WinReg.HKEY.HKEY_USERS, "S-1-5-19");
|
|
||||||
} catch (Exception e) {
|
|
||||||
LOG.warning("Failed to check Windows administrator privileges", e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case LINUX -> {
|
|
||||||
Path selfStatus = Paths.get("/proc/self/status");
|
|
||||||
if (Files.exists(selfStatus)) {
|
|
||||||
try (var reader = Files.newBufferedReader(selfStatus)) {
|
|
||||||
@Nullable String line;
|
|
||||||
while ((line = reader.readLine()) != null) {
|
|
||||||
if (line.startsWith("Uid:")) {
|
|
||||||
String[] parts = line.split("\t");
|
|
||||||
if (parts.length >= 3) {
|
|
||||||
String uid = parts[2].trim();
|
|
||||||
return "0".equals(uid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
LOG.warning("Failed to read " + selfStatus, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case MACOS -> {
|
|
||||||
if (NativeUtils.USE_JNA) {
|
|
||||||
LibSystem libSystem = LibSystem.INSTANCE;
|
|
||||||
if (libSystem != null) {
|
|
||||||
try {
|
|
||||||
return libSystem.geteuid() == 0;
|
|
||||||
} catch (Throwable e) {
|
|
||||||
LOG.warning("Failed to check macOS root privileges", e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (OperatingSystem.CURRENT_OS.isUnixLike()) {
|
|
||||||
@Nullable Path id = SystemUtils.which("id");
|
|
||||||
if (id != null) {
|
|
||||||
try {
|
|
||||||
return "0".equals(SystemUtils.run(id.toString(), "-u").trim());
|
|
||||||
} catch (Exception e) {
|
|
||||||
LOG.warning("Failed to run id -u", e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final CrashReporter CRASH_REPORTER = new CrashReporter(true);
|
public static final CrashReporter CRASH_REPORTER = new CrashReporter(true);
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,20 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors
|
/*
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
* Hello Minecraft! Launcher
|
||||||
|
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
package org.jackhuang.hmcl;
|
package org.jackhuang.hmcl;
|
||||||
|
|
||||||
import org.jackhuang.hmcl.util.StringUtils;
|
import org.jackhuang.hmcl.util.StringUtils;
|
||||||
@@ -8,8 +23,8 @@ import org.jackhuang.hmcl.util.platform.Architecture;
|
|||||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,56 +38,55 @@ public final class Metadata {
|
|||||||
public static final String FULL_NAME = "Hello Minecraft! Launcher";
|
public static final String FULL_NAME = "Hello Minecraft! Launcher";
|
||||||
public static final String VERSION = System.getProperty("hmcl.version.override", JarUtils.getAttribute("hmcl.version", "@develop@"));
|
public static final String VERSION = System.getProperty("hmcl.version.override", JarUtils.getAttribute("hmcl.version", "@develop@"));
|
||||||
|
|
||||||
/// Explicit Application User Model ID used for Windows taskbar grouping and pinning.
|
|
||||||
public static final String WINDOWS_APP_USER_MODEL_ID = "org.jackhuang.hmcl";
|
|
||||||
|
|
||||||
public static final String TITLE = NAME + " " + VERSION;
|
public static final String TITLE = NAME + " " + VERSION;
|
||||||
|
public static final String FULL_TITLE = FULL_NAME + " v" + VERSION;
|
||||||
/// Whether the build metadata identifies this launcher as an official build.
|
|
||||||
public static final boolean OFFICIAL_BUILD = Boolean.parseBoolean(JarUtils.getAttribute("hmcl.official", "false"));
|
|
||||||
|
|
||||||
public static final int MINIMUM_REQUIRED_JAVA_VERSION = 17;
|
public static final int MINIMUM_REQUIRED_JAVA_VERSION = 17;
|
||||||
public static final int MINIMUM_SUPPORTED_JAVA_VERSION = 17;
|
public static final int MINIMUM_SUPPORTED_JAVA_VERSION = 17;
|
||||||
public static final int RECOMMENDED_JAVA_VERSION = 25;
|
public static final int RECOMMENDED_JAVA_VERSION = 21;
|
||||||
|
|
||||||
|
public static final String PUBLISH_URL = "https://hmcl.huangyuhui.net";
|
||||||
|
public static final String ABOUT_URL = PUBLISH_URL + "/about";
|
||||||
|
public static final String DOWNLOAD_URL = PUBLISH_URL + "/download";
|
||||||
|
public static final String HMCL_UPDATE_URL = System.getProperty("hmcl.update_source.override", PUBLISH_URL + "/api/update_link");
|
||||||
|
|
||||||
public static final String DOCS_URL = "https://docs.hmcl.net";
|
public static final String DOCS_URL = "https://docs.hmcl.net";
|
||||||
|
public static final String CONTACT_URL = DOCS_URL + "/help.html";
|
||||||
|
public static final String CHANGELOG_URL = DOCS_URL + "/changelog/";
|
||||||
public static final String EULA_URL = DOCS_URL + "/eula/hmcl.html";
|
public static final String EULA_URL = DOCS_URL + "/eula/hmcl.html";
|
||||||
|
public static final String GROUPS_URL = "https://www.bilibili.com/opus/905435541874409529";
|
||||||
|
|
||||||
public static final String BUILD_CHANNEL = JarUtils.getAttribute("hmcl.version.type", "nightly");
|
public static final String BUILD_CHANNEL = JarUtils.getAttribute("hmcl.version.type", "nightly");
|
||||||
public static final String GITHUB_SHA = JarUtils.getAttribute("hmcl.version.hash", null);
|
public static final String GITHUB_SHA = JarUtils.getAttribute("hmcl.version.hash", null);
|
||||||
|
|
||||||
public static final Path CURRENT_DIRECTORY = Path.of(System.getProperty("user.dir")).toAbsolutePath().normalize();
|
public static final Path CURRENT_DIRECTORY = Paths.get(System.getProperty("user.dir")).toAbsolutePath().normalize();
|
||||||
public static final Path MINECRAFT_DIRECTORY = OperatingSystem.getWorkingDirectory("minecraft");
|
public static final Path MINECRAFT_DIRECTORY = OperatingSystem.getWorkingDirectory("minecraft");
|
||||||
public static final Path HMCL_USER_HOME;
|
public static final Path HMCL_GLOBAL_DIRECTORY;
|
||||||
public static final Path HMCL_LOCAL_HOME;
|
public static final Path HMCL_CURRENT_DIRECTORY;
|
||||||
public static final Path DEPENDENCIES_DIRECTORY;
|
public static final Path DEPENDENCIES_DIRECTORY;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
String hmclHome = System.getProperty("hmcl.home", System.getenv("HMCL_USER_HOME"));
|
String hmclHome = System.getProperty("hmcl.home");
|
||||||
if (StringUtils.isBlank(hmclHome)) {
|
if (hmclHome == null) {
|
||||||
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
|
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
|
||||||
String xdgData = System.getenv("XDG_DATA_HOME");
|
String xdgData = System.getenv("XDG_DATA_HOME");
|
||||||
if (StringUtils.isNotBlank(xdgData)) {
|
if (StringUtils.isNotBlank(xdgData)) {
|
||||||
HMCL_USER_HOME = Path.of(xdgData, "hmcl").toAbsolutePath().normalize();
|
HMCL_GLOBAL_DIRECTORY = Paths.get(xdgData, "hmcl").toAbsolutePath().normalize();
|
||||||
} else {
|
} else {
|
||||||
HMCL_USER_HOME = Path.of(System.getProperty("user.home"), ".local", "share", "hmcl").toAbsolutePath().normalize();
|
HMCL_GLOBAL_DIRECTORY = Paths.get(System.getProperty("user.home"), ".local", "share", "hmcl").toAbsolutePath().normalize();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
HMCL_USER_HOME = OperatingSystem.getWorkingDirectory("hmcl");
|
HMCL_GLOBAL_DIRECTORY = OperatingSystem.getWorkingDirectory("hmcl");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
HMCL_USER_HOME = Path.of(hmclHome).toAbsolutePath().normalize();
|
HMCL_GLOBAL_DIRECTORY = Paths.get(hmclHome).toAbsolutePath().normalize();
|
||||||
}
|
}
|
||||||
|
|
||||||
String hmclCurrentDir = System.getProperty("hmcl.dir", System.getenv("HMCL_LOCAL_HOME"));
|
String hmclCurrentDir = System.getProperty("hmcl.dir");
|
||||||
HMCL_LOCAL_HOME = StringUtils.isNotBlank(hmclCurrentDir)
|
HMCL_CURRENT_DIRECTORY = hmclCurrentDir != null
|
||||||
? Path.of(hmclCurrentDir).toAbsolutePath().normalize()
|
? Paths.get(hmclCurrentDir).toAbsolutePath().normalize()
|
||||||
: CURRENT_DIRECTORY.resolve(".hmcl");
|
: CURRENT_DIRECTORY.resolve(".hmcl");
|
||||||
|
DEPENDENCIES_DIRECTORY = HMCL_CURRENT_DIRECTORY.resolve("dependencies");
|
||||||
String hmclDependencies = System.getProperty("hmcl.dependencies.dir", System.getenv("HMCL_DEPENDENCIES_DIR"));
|
|
||||||
DEPENDENCIES_DIRECTORY = StringUtils.isNotBlank(hmclDependencies)
|
|
||||||
? Path.of(hmclDependencies).toAbsolutePath().normalize()
|
|
||||||
: HMCL_LOCAL_HOME.resolve("dependencies");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isStable() {
|
public static boolean isStable() {
|
||||||
@@ -89,7 +103,7 @@ public final class Metadata {
|
|||||||
|
|
||||||
public static @Nullable String getSuggestedJavaDownloadLink() {
|
public static @Nullable String getSuggestedJavaDownloadLink() {
|
||||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX && Architecture.SYSTEM_ARCH == Architecture.LOONGARCH64_OW)
|
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX && Architecture.SYSTEM_ARCH == Architecture.LOONGARCH64_OW)
|
||||||
return "https://www.loongnix.cn/zh/api/java/downloads-jdk25/index.html";
|
return "https://www.loongnix.cn/zh/api/java/downloads-jdk21/index.html";
|
||||||
else {
|
else {
|
||||||
EnumSet<Architecture> supportedArchitectures;
|
EnumSet<Architecture> supportedArchitectures;
|
||||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
|
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
|
||||||
@@ -113,46 +127,4 @@ public final class Metadata {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Directory under [HMCL_LOCAL_HOME] that holds a launcher-bundled modpack for automatic install.
|
|
||||||
public static final String BUNDLED_MODPACK_DIRECTORY_NAME = "modpack";
|
|
||||||
|
|
||||||
/// Returns the directory for a launcher-bundled modpack (`[HMCL_LOCAL_HOME]/modpack`).
|
|
||||||
public static Path getBundledModpackDirectory() {
|
|
||||||
return HMCL_LOCAL_HOME.resolve(BUNDLED_MODPACK_DIRECTORY_NAME);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the bundled modpack package under [getBundledModpackDirectory], if present.
|
|
||||||
///
|
|
||||||
/// Prefers `modpack.zip` over `modpack.mrpack` when both exist. Presence of the package is the
|
|
||||||
/// signal to offer automatic install; the file is removed after a successful install.
|
|
||||||
///
|
|
||||||
/// @return the modpack path, or `null` when no package is present
|
|
||||||
public static @Nullable Path findBundledModpackFile() {
|
|
||||||
Path directory = getBundledModpackDirectory();
|
|
||||||
Path zipModpack = directory.resolve("modpack.zip");
|
|
||||||
if (Files.isRegularFile(zipModpack)) {
|
|
||||||
return zipModpack;
|
|
||||||
}
|
|
||||||
Path mrpackModpack = directory.resolve("modpack.mrpack");
|
|
||||||
if (Files.isRegularFile(mrpackModpack)) {
|
|
||||||
return mrpackModpack;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Server address used when the installed modpack manifest does not provide one.
|
|
||||||
///
|
|
||||||
/// Can be set at build or run time with the `hmcl.server.address` system property; empty by default.
|
|
||||||
public static String getDefaultServerAddress() {
|
|
||||||
return System.getProperty("hmcl.server.address", "").trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The server name shown in the title bar.
|
|
||||||
///
|
|
||||||
/// Can be set with the `hmcl.server.name` system property; empty by default.
|
|
||||||
public static String getServerName() {
|
|
||||||
return System.getProperty("hmcl.server.name", "").trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,473 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
|
||||||
package org.jackhuang.hmcl.addon;
|
|
||||||
|
|
||||||
import org.jackhuang.hmcl.addon.mod.ModProjectInfo;
|
|
||||||
import org.jackhuang.hmcl.modpack.ModpackProjectInfo;
|
|
||||||
import org.jackhuang.hmcl.util.Pair;
|
|
||||||
import org.jackhuang.hmcl.util.StringUtils;
|
|
||||||
import org.jetbrains.annotations.NotNullByDefault;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
import org.jetbrains.annotations.Unmodifiable;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.lang.ref.SoftReference;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.*;
|
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
|
||||||
|
|
||||||
/// @author Glavo
|
|
||||||
@NotNullByDefault
|
|
||||||
public abstract class AddonProjectInfoRegistry<I extends AddonProjectInfo> {
|
|
||||||
public static final AddonProjectInfoRegistry<ModProjectInfo> MOD = new AddonProjectInfoRegistry<>(
|
|
||||||
"/assets/addon/mod.csv",
|
|
||||||
32768
|
|
||||||
) {
|
|
||||||
@Override
|
|
||||||
protected int getFieldsCount() {
|
|
||||||
return MOD_IDS_INDEX + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected ModProjectInfo parseAddonProjectInfo(String[] fields) throws Exception {
|
|
||||||
return new ModProjectInfo(
|
|
||||||
Integer.parseInt(fields[MCMOD_ID_INDEX]),
|
|
||||||
fields[CURSEFORGE_ID_INDEX],
|
|
||||||
fields[MODRINTH_ID_INDEX],
|
|
||||||
fields[MAIN_NAME_INDEX],
|
|
||||||
fields[SUB_NAME_INDEX],
|
|
||||||
fields[ABBR_NAME_INDEX],
|
|
||||||
fields[MOD_IDS_INDEX]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Provides metadata for modpacks.
|
|
||||||
public static final AddonProjectInfoRegistry<ModpackProjectInfo> MODPACK = new AddonProjectInfoRegistry<>(
|
|
||||||
"/assets/addon/modpack.csv", 2048
|
|
||||||
) {
|
|
||||||
@Override
|
|
||||||
protected int getFieldsCount() {
|
|
||||||
return ABBR_NAME_INDEX + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected ModpackProjectInfo parseAddonProjectInfo(String[] fields) {
|
|
||||||
return new ModpackProjectInfo(Integer.parseInt(fields[MCMOD_ID_INDEX]),
|
|
||||||
fields[CURSEFORGE_ID_INDEX], fields[MODRINTH_ID_INDEX],
|
|
||||||
fields[MAIN_NAME_INDEX], fields[SUB_NAME_INDEX], fields[ABBR_NAME_INDEX]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Returns the registry for the addon type, or `null` for null or unsupported types.
|
|
||||||
public static @Nullable AddonProjectInfoRegistry<?> getByAddonType(@Nullable RemoteAddon.Type type) {
|
|
||||||
if (type == null) return null;
|
|
||||||
return switch (type) {
|
|
||||||
case MOD -> MOD;
|
|
||||||
case MODPACK -> MODPACK;
|
|
||||||
default -> null;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static final int MCMOD_ID_INDEX = 0;
|
|
||||||
protected static final int CURSEFORGE_ID_INDEX = 1;
|
|
||||||
protected static final int MODRINTH_ID_INDEX = 2;
|
|
||||||
protected static final int MAIN_NAME_INDEX = 3;
|
|
||||||
protected static final int SUB_NAME_INDEX = 4;
|
|
||||||
protected static final int ABBR_NAME_INDEX = 5;
|
|
||||||
protected static final int MOD_IDS_INDEX = 6;
|
|
||||||
|
|
||||||
private final ReentrantLock lock = new ReentrantLock();
|
|
||||||
private final String assetsPath;
|
|
||||||
private final int initialCapacity;
|
|
||||||
|
|
||||||
private volatile @Nullable SoftReference<@Unmodifiable List<I>> infosRef;
|
|
||||||
|
|
||||||
/// Softly caches projects indexed by mod ID.
|
|
||||||
private volatile @Nullable SoftReference<@Unmodifiable Map<String, I>> modIdMapRef;
|
|
||||||
|
|
||||||
/// Softly caches projects indexed by cleaned secondary name.
|
|
||||||
private volatile @Nullable SoftReference<@Unmodifiable Map<String, I>> subnameMapRef;
|
|
||||||
|
|
||||||
/// Softly caches projects indexed by CurseForge identifier.
|
|
||||||
private volatile @Nullable SoftReference<@Unmodifiable Map<String, I>> curseForgeMapRef;
|
|
||||||
|
|
||||||
/// Softly caches searchable names in resource and field order.
|
|
||||||
private volatile @Nullable SoftReference<@Unmodifiable List<Pair<String, I>>> keywordsRef;
|
|
||||||
|
|
||||||
/// Creates a registry reading records from the given classpath resource.
|
|
||||||
AddonProjectInfoRegistry(String assetsPath, int initialCapacity) {
|
|
||||||
this.assetsPath = assetsPath;
|
|
||||||
this.initialCapacity = initialCapacity;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract int getFieldsCount();
|
|
||||||
|
|
||||||
protected abstract I parseAddonProjectInfo(String[] fields) throws Exception;
|
|
||||||
|
|
||||||
private @Unmodifiable List<I> loadInfos() throws IOException {
|
|
||||||
@Nullable InputStream inputStream = AddonProjectInfoRegistry.class.getResourceAsStream(assetsPath);
|
|
||||||
if (inputStream == null) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
|
|
||||||
try (var reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
|
|
||||||
ArrayList<I> infos = new ArrayList<>(initialCapacity);
|
|
||||||
@Unmodifiable List<I> result = Collections.unmodifiableList(infos);
|
|
||||||
|
|
||||||
int fieldsCount = getFieldsCount();
|
|
||||||
String[] fields = new String[fieldsCount];
|
|
||||||
|
|
||||||
@Nullable StringBuilder fieldBuilder = null;
|
|
||||||
|
|
||||||
@Nullable String line;
|
|
||||||
loop:
|
|
||||||
while ((line = reader.readLine()) != null) {
|
|
||||||
if (line.isEmpty() || line.startsWith("#")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
int offset = 0;
|
|
||||||
for (int fieldIndex = 0; fieldIndex < fieldsCount; fieldIndex++) {
|
|
||||||
if (offset > line.length()) {
|
|
||||||
LOG.warning("Invalid line in " + assetsPath + ": " + line);
|
|
||||||
continue loop;
|
|
||||||
}
|
|
||||||
|
|
||||||
int end = line.indexOf(',', offset);
|
|
||||||
if (end < 0)
|
|
||||||
end = line.length();
|
|
||||||
|
|
||||||
|
|
||||||
String field;
|
|
||||||
if (offset == end) {
|
|
||||||
field = "";
|
|
||||||
} else {
|
|
||||||
int cursor = offset;
|
|
||||||
while (cursor < end) {
|
|
||||||
if (line.charAt(cursor) == '\\') {
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
cursor++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cursor == end) {
|
|
||||||
field = line.substring(offset, end);
|
|
||||||
} else {
|
|
||||||
if (fieldBuilder == null) {
|
|
||||||
fieldBuilder = new StringBuilder(32);
|
|
||||||
} else {
|
|
||||||
fieldBuilder.setLength(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
fieldBuilder.append(line, offset, cursor);
|
|
||||||
while (cursor < end) {
|
|
||||||
char c = line.charAt(cursor);
|
|
||||||
if (c == '\\') {
|
|
||||||
if (cursor < end - 1) {
|
|
||||||
char next = line.charAt(cursor + 1);
|
|
||||||
switch (next) {
|
|
||||||
case '0' -> {
|
|
||||||
fieldBuilder.append('\0');
|
|
||||||
cursor += 2;
|
|
||||||
}
|
|
||||||
case 'n' -> {
|
|
||||||
fieldBuilder.append('\n');
|
|
||||||
cursor += 2;
|
|
||||||
}
|
|
||||||
case 'r' -> {
|
|
||||||
fieldBuilder.append('\r');
|
|
||||||
cursor += 2;
|
|
||||||
}
|
|
||||||
case 't' -> {
|
|
||||||
fieldBuilder.append('\t');
|
|
||||||
cursor += 2;
|
|
||||||
}
|
|
||||||
case '\\' -> {
|
|
||||||
fieldBuilder.append('\\');
|
|
||||||
cursor += 2;
|
|
||||||
}
|
|
||||||
case 'c' -> {
|
|
||||||
fieldBuilder.append(',');
|
|
||||||
cursor += 2;
|
|
||||||
}
|
|
||||||
case 'u' -> {
|
|
||||||
if (cursor < end - 5) {
|
|
||||||
try {
|
|
||||||
int codePoint = Integer.parseInt(line, cursor + 2, cursor + 6, 16);
|
|
||||||
fieldBuilder.append((char) codePoint);
|
|
||||||
cursor += 6;
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
LOG.warning("Invalid line in " + assetsPath + ": " + line);
|
|
||||||
continue loop;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fieldBuilder.append(c);
|
|
||||||
cursor++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default -> {
|
|
||||||
LOG.warning("Invalid line in " + assetsPath + ": " + line);
|
|
||||||
continue loop;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fieldBuilder.append(c);
|
|
||||||
cursor++;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
} else {
|
|
||||||
fieldBuilder.append(c);
|
|
||||||
cursor++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
field = fieldBuilder.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fields[fieldIndex] = field;
|
|
||||||
offset = end + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
I info = parseAddonProjectInfo(fields);
|
|
||||||
info.setKeepAlive(result);
|
|
||||||
infos.add(info);
|
|
||||||
} catch (Exception e) {
|
|
||||||
LOG.warning("Failed to parse line in " + assetsPath + ": " + line, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
infos.trimToSize();
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns an unmodifiable index, rebuilding it after its soft reference is cleared.
|
|
||||||
/// Duplicate keys retain the first project in resource order.
|
|
||||||
private @Unmodifiable Map<String, I> getModIdMap() {
|
|
||||||
@Nullable SoftReference<@Unmodifiable Map<String, I>> reference = this.modIdMapRef;
|
|
||||||
@Unmodifiable Map<String, I> cached = reference != null ? reference.get() : null;
|
|
||||||
if (cached != null)
|
|
||||||
return cached;
|
|
||||||
lock.lock();
|
|
||||||
try {
|
|
||||||
reference = this.modIdMapRef;
|
|
||||||
cached = reference != null ? reference.get() : null;
|
|
||||||
if (cached != null)
|
|
||||||
return cached;
|
|
||||||
Map<String, I> map = new HashMap<>();
|
|
||||||
for (I info : getInfos()) {
|
|
||||||
if (info instanceof ModProjectInfo mod) {
|
|
||||||
for (String id : mod.getModIds()) {
|
|
||||||
if (!id.isEmpty())
|
|
||||||
map.putIfAbsent(id, info);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cached = Collections.unmodifiableMap(map);
|
|
||||||
this.modIdMapRef = new SoftReference<>(cached);
|
|
||||||
return cached;
|
|
||||||
} finally {
|
|
||||||
lock.unlock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns an unmodifiable index, rebuilding it after its soft reference is cleared.
|
|
||||||
/// Duplicate keys retain the first project in resource order.
|
|
||||||
private @Unmodifiable Map<String, I> getSubnameMap() {
|
|
||||||
@Nullable SoftReference<@Unmodifiable Map<String, I>> reference = this.subnameMapRef;
|
|
||||||
@Unmodifiable Map<String, I> cached = reference != null ? reference.get() : null;
|
|
||||||
if (cached != null)
|
|
||||||
return cached;
|
|
||||||
lock.lock();
|
|
||||||
try {
|
|
||||||
reference = this.subnameMapRef;
|
|
||||||
cached = reference != null ? reference.get() : null;
|
|
||||||
if (cached != null)
|
|
||||||
return cached;
|
|
||||||
Map<String, I> map = new HashMap<>();
|
|
||||||
for (I info : getInfos()) {
|
|
||||||
String name = cleanSubname(info.subName());
|
|
||||||
if (!name.isEmpty())
|
|
||||||
map.putIfAbsent(name, info);
|
|
||||||
}
|
|
||||||
cached = Collections.unmodifiableMap(map);
|
|
||||||
this.subnameMapRef = new SoftReference<>(cached);
|
|
||||||
return cached;
|
|
||||||
} finally {
|
|
||||||
lock.unlock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns an unmodifiable index, rebuilding it after its soft reference is cleared.
|
|
||||||
/// Duplicate keys retain the first project in resource order.
|
|
||||||
private @Unmodifiable Map<String, I> getCurseForgeMap() {
|
|
||||||
@Nullable SoftReference<@Unmodifiable Map<String, I>> reference = this.curseForgeMapRef;
|
|
||||||
@Unmodifiable Map<String, I> cached = reference != null ? reference.get() : null;
|
|
||||||
if (cached != null)
|
|
||||||
return cached;
|
|
||||||
lock.lock();
|
|
||||||
try {
|
|
||||||
reference = this.curseForgeMapRef;
|
|
||||||
cached = reference != null ? reference.get() : null;
|
|
||||||
if (cached != null)
|
|
||||||
return cached;
|
|
||||||
Map<String, I> map = new HashMap<>();
|
|
||||||
for (I info : getInfos()) {
|
|
||||||
String id = info.curseforgeId();
|
|
||||||
if (StringUtils.isNotBlank(id))
|
|
||||||
map.putIfAbsent(id, info);
|
|
||||||
}
|
|
||||||
cached = Collections.unmodifiableMap(map);
|
|
||||||
this.curseForgeMapRef = new SoftReference<>(cached);
|
|
||||||
return cached;
|
|
||||||
} finally {
|
|
||||||
lock.unlock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the first project with the exact CurseForge identifier, or `null` if the
|
|
||||||
/// identifier is null, blank or unknown.
|
|
||||||
public @Nullable I getInfoByCurseForgeId(@Nullable String id) {
|
|
||||||
return StringUtils.isBlank(id) ? null : getCurseForgeMap().get(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Finds a project by its cleaned secondary name, falling back to its exact mod ID.
|
|
||||||
/// A name match is accepted only when the ID is blank or belongs to that mod project.
|
|
||||||
/// Null and blank inputs are absent; returns `null` if neither lookup matches.
|
|
||||||
/// Duplicate keys resolve to the first project in resource order.
|
|
||||||
public @Nullable I getInfo(@Nullable String id, @Nullable String subname) {
|
|
||||||
String name = cleanSubname(subname);
|
|
||||||
if (StringUtils.isNotBlank(name)) {
|
|
||||||
@Nullable I info = getSubnameMap().get(name);
|
|
||||||
if (info != null && (StringUtils.isBlank(id)
|
|
||||||
|| info instanceof ModProjectInfo mod && mod.getModIds().contains(id)))
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
return StringUtils.isBlank(id) ? null : getModIdMap().get(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isSupportedSubnameChar(int ch) {
|
|
||||||
return (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9')
|
|
||||||
|| ".+\\".indexOf(ch) >= 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Removes supported separators from a secondary name.
|
|
||||||
/// Returns an empty string for null, blank or unsupported names; matching is case-sensitive.
|
|
||||||
private static String cleanSubname(@Nullable String subname) {
|
|
||||||
if (StringUtils.isBlank(subname))
|
|
||||||
return "";
|
|
||||||
|
|
||||||
int offset = 0;
|
|
||||||
while (offset < subname.length()) {
|
|
||||||
int ch = subname.charAt(offset);
|
|
||||||
if (!isSupportedSubnameChar(ch))
|
|
||||||
break;
|
|
||||||
offset++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (offset == subname.length())
|
|
||||||
return subname;
|
|
||||||
|
|
||||||
StringBuilder builder = new StringBuilder(subname.length());
|
|
||||||
builder.append(subname, 0, offset);
|
|
||||||
|
|
||||||
while (offset < subname.length()) {
|
|
||||||
int ch = subname.codePointAt(offset);
|
|
||||||
if (isSupportedSubnameChar(ch)) {
|
|
||||||
builder.appendCodePoint(ch);
|
|
||||||
} else if (!(Character.isWhitespace(ch)
|
|
||||||
|| "':_-/&()[]{}|,!?~•".indexOf(ch) >= 0)) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
offset += Character.charCount(ch);
|
|
||||||
}
|
|
||||||
return builder.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns searchable names, rebuilding the index after its soft reference is cleared.
|
|
||||||
private @Unmodifiable List<Pair<String, I>> getKeywords() {
|
|
||||||
@Nullable SoftReference<@Unmodifiable List<Pair<String, I>>> reference = keywordsRef;
|
|
||||||
@Unmodifiable List<Pair<String, I>> cached = reference != null ? reference.get() : null;
|
|
||||||
if (cached != null)
|
|
||||||
return cached;
|
|
||||||
lock.lock();
|
|
||||||
try {
|
|
||||||
reference = keywordsRef;
|
|
||||||
cached = reference != null ? reference.get() : null;
|
|
||||||
if (cached != null)
|
|
||||||
return cached;
|
|
||||||
List<Pair<String, I>> keywords = new ArrayList<>();
|
|
||||||
for (I info : getInfos()) {
|
|
||||||
if (!info.mainName().isEmpty())
|
|
||||||
keywords.add(Pair.pair(info.mainName(), info));
|
|
||||||
if (!info.subName().isEmpty())
|
|
||||||
keywords.add(Pair.pair(info.subName(), info));
|
|
||||||
if (!info.abbrName().isEmpty())
|
|
||||||
keywords.add(Pair.pair(info.abbrName(), info));
|
|
||||||
}
|
|
||||||
cached = List.copyOf(keywords);
|
|
||||||
keywordsRef = new SoftReference<>(cached);
|
|
||||||
return cached;
|
|
||||||
} finally {
|
|
||||||
lock.unlock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns projects ranked by descending longest common subsequence length after removing
|
|
||||||
/// space characters from the query. Comparison is case-sensitive and uses UTF-16 units.
|
|
||||||
/// Matches require at least one unit and at least query length minus three units.
|
|
||||||
/// Each matching name contributes a result, so projects may occur more than once.
|
|
||||||
/// Ties preserve resource order, then primary name, secondary name and abbreviation order.
|
|
||||||
public @Unmodifiable List<I> search(String query) {
|
|
||||||
query = query.chars().filter(ch -> !Character.isSpaceChar(ch))
|
|
||||||
.collect(StringBuilder::new, (sb, ch) -> sb.append((char) ch), StringBuilder::append).toString();
|
|
||||||
@Unmodifiable List<Pair<String, I>> keywords = getKeywords();
|
|
||||||
int maxLength = keywords.stream().mapToInt(keyword -> keyword.getKey().length()).max().orElse(0);
|
|
||||||
StringUtils.LongestCommonSubsequence lcs = new StringUtils.LongestCommonSubsequence(query.length(), maxLength);
|
|
||||||
List<Pair<Integer, I>> matches = new ArrayList<>();
|
|
||||||
for (Pair<String, I> keyword : keywords) {
|
|
||||||
int score = lcs.calc(query, keyword.getKey());
|
|
||||||
if (score >= Math.max(1, query.length() - 3))
|
|
||||||
matches.add(Pair.pair(score, keyword.getValue()));
|
|
||||||
}
|
|
||||||
return matches.stream().sorted((a, b) -> Integer.compare(b.getKey(), a.getKey()))
|
|
||||||
.map(Pair::getValue).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns cached project metadata, loading it on demand. Missing resources and I/O failures
|
|
||||||
/// produce an empty list; malformed records are skipped. The cache is softly referenced.
|
|
||||||
public @Unmodifiable List<I> getInfos() {
|
|
||||||
@Nullable SoftReference<@Unmodifiable List<I>> infosRef = this.infosRef;
|
|
||||||
@Unmodifiable List<I> infos = infosRef != null ? infosRef.get() : null;
|
|
||||||
if (infos != null)
|
|
||||||
return infos;
|
|
||||||
|
|
||||||
lock.lock();
|
|
||||||
try {
|
|
||||||
infosRef = this.infosRef;
|
|
||||||
infos = infosRef != null ? infosRef.get() : null;
|
|
||||||
if (infos != null)
|
|
||||||
return infos;
|
|
||||||
|
|
||||||
try {
|
|
||||||
infos = loadInfos();
|
|
||||||
} catch (IOException e) {
|
|
||||||
LOG.warning("Failed to load addon project info from " + assetsPath, e);
|
|
||||||
infos = List.of();
|
|
||||||
}
|
|
||||||
this.infosRef = new SoftReference<>(infos);
|
|
||||||
return infos;
|
|
||||||
} finally {
|
|
||||||
lock.unlock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
|
||||||
package org.jackhuang.hmcl.addon.update;
|
|
||||||
|
|
||||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
|
||||||
import org.jackhuang.hmcl.addon.LocalAddonFile;
|
|
||||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
|
||||||
import org.jackhuang.hmcl.task.Schedulers;
|
|
||||||
import org.jackhuang.hmcl.task.Task;
|
|
||||||
import org.jackhuang.hmcl.util.Pair;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
|
||||||
|
|
||||||
public class AddonCheckUpdatesTask extends Task<AddonUpdates> {
|
|
||||||
private final List<Task<Pair<AddonUpdate, @Nullable AddonUpdate>>> dependents;
|
|
||||||
|
|
||||||
public AddonCheckUpdatesTask(DownloadProvider downloadProvider, String gameVersion, Collection<? extends LocalAddonFile<?>> addons) {
|
|
||||||
dependents = addons.stream().map(addon ->
|
|
||||||
Task.supplyAsync(Schedulers.io(), () -> {
|
|
||||||
AddonUpdate candidate = null;
|
|
||||||
AddonUpdate candidateRelease = null;
|
|
||||||
for (RemoteAddon.Source source : RemoteAddon.Source.values()) {
|
|
||||||
AddonUpdate update = null;
|
|
||||||
AddonUpdate updateRelease = null;
|
|
||||||
try {
|
|
||||||
var u = addon.checkUpdates(downloadProvider, gameVersion, source);
|
|
||||||
if (u != null) {
|
|
||||||
update = u.key();
|
|
||||||
updateRelease = u.value();
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
LOG.warning(String.format("Cannot check update for addon %s.", addon.getFileName()), e);
|
|
||||||
}
|
|
||||||
if (update == null) continue;
|
|
||||||
if (candidate == null || candidate.targetVersion().datePublished().isBefore(update.targetVersion().datePublished()))
|
|
||||||
candidate = update;
|
|
||||||
|
|
||||||
if (updateRelease == null) continue;
|
|
||||||
if (candidateRelease == null || candidateRelease.targetVersion().datePublished().isBefore(updateRelease.targetVersion().datePublished()))
|
|
||||||
candidateRelease = updateRelease;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (candidate == null) return null; // If there's no candidate for all channels, then no candidate for release channel
|
|
||||||
return Pair.pair(candidate, candidateRelease);
|
|
||||||
}).setName(addon.getFileName()).setSignificance(TaskSignificance.MAJOR).withCounter("update.checking")
|
|
||||||
).toList();
|
|
||||||
|
|
||||||
setStage("update.checking");
|
|
||||||
getProperties().put("total", dependents.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean doPreExecute() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void preExecute() {
|
|
||||||
notifyPropertiesChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Collection<? extends Task<?>> getDependents() {
|
|
||||||
return dependents;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isRelyingOnDependents() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute() throws Exception {
|
|
||||||
List<AddonUpdate> commonUpdates = new ArrayList<>(), releaseUpdates = new ArrayList<>();
|
|
||||||
dependents.stream().map(Task::getResult).filter(Objects::nonNull)
|
|
||||||
.forEachOrdered(pair -> {
|
|
||||||
commonUpdates.add(pair.key());
|
|
||||||
if (pair.value() != null) releaseUpdates.add(pair.value());
|
|
||||||
});
|
|
||||||
setResult(new AddonUpdates(commonUpdates, releaseUpdates));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
|
||||||
package org.jackhuang.hmcl.addon.update;
|
|
||||||
|
|
||||||
import org.jackhuang.hmcl.addon.LocalAddonFile;
|
|
||||||
import org.jackhuang.hmcl.addon.LocalAddonManager;
|
|
||||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
|
||||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
|
||||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
|
||||||
import org.jackhuang.hmcl.task.Schedulers;
|
|
||||||
import org.jackhuang.hmcl.task.Task;
|
|
||||||
import org.jackhuang.hmcl.util.StringUtils;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
|
||||||
|
|
||||||
public class AddonUpdateTask extends Task<Void> {
|
|
||||||
private final Collection<Task<?>> dependents = new ArrayList<>();
|
|
||||||
private final List<LocalAddonFile<?>> failedAddons = new ArrayList<>();
|
|
||||||
|
|
||||||
public AddonUpdateTask(DownloadProvider downloadProvider, Path addonDirectory, List<AddonUpdate> addons) {
|
|
||||||
setStage("addon.check_update.confirm");
|
|
||||||
getProperties().put("total", addons.size());
|
|
||||||
|
|
||||||
for (AddonUpdate addon : addons) {
|
|
||||||
LocalAddonFile<?> local = addon.localAddonFile();
|
|
||||||
RemoteAddon.Version remote = addon.targetVersion();
|
|
||||||
boolean isDisabled = local.isDisabled();
|
|
||||||
String fileName = remote.file().filename();
|
|
||||||
if (isDisabled)
|
|
||||||
fileName = StringUtils.addSuffix(fileName, LocalAddonManager.DISABLED_EXTENSION);
|
|
||||||
String newFileName = fileName;
|
|
||||||
Path targetFile = addonDirectory.resolve(newFileName);
|
|
||||||
|
|
||||||
dependents.add(Task
|
|
||||||
.supplyAsync(Schedulers.io(), () -> Files.createTempFile("hmcl-addon-update-", ".tmp"))
|
|
||||||
.thenComposeAsync(tempFile ->
|
|
||||||
new FileDownloadTask(downloadProvider.getDownloadCandidates(remote.file().url()), tempFile)
|
|
||||||
.setName(remote.name())
|
|
||||||
.thenRunAsync(Schedulers.javafx(), () -> local.setOld(true))
|
|
||||||
.thenRunAsync(Schedulers.io(), () -> Files.move(tempFile, targetFile))
|
|
||||||
.whenComplete(Schedulers.javafx(), exception -> {
|
|
||||||
try {
|
|
||||||
Files.deleteIfExists(tempFile);
|
|
||||||
} catch (Exception e) {
|
|
||||||
LOG.warning("Failed to delete temp file " + tempFile, e);
|
|
||||||
}
|
|
||||||
if (exception != null) {
|
|
||||||
// restore state if failed
|
|
||||||
local.setOld(false);
|
|
||||||
if (isDisabled)
|
|
||||||
local.markDisabled();
|
|
||||||
} else {
|
|
||||||
local.onUpdated(newFileName);
|
|
||||||
if (!local.keepOldFiles()) {
|
|
||||||
try {
|
|
||||||
local.delete();
|
|
||||||
} catch (IOException e) {
|
|
||||||
LOG.warning("Failed to delete outdated addon: " + local.getFile(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
).whenComplete(Schedulers.javafx(), exception -> {
|
|
||||||
if (exception != null) {
|
|
||||||
LOG.warning("Failed to update addon", exception);
|
|
||||||
failedAddons.add(local);
|
|
||||||
}
|
|
||||||
}).withCounter("addon.check_update.confirm"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<LocalAddonFile<?>> getFailedAddons() {
|
|
||||||
return failedAddons;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Collection<Task<?>> getDependents() {
|
|
||||||
return dependents;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean doPreExecute() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void preExecute() {
|
|
||||||
notifyPropertiesChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isRelyingOnDependents() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute() throws Exception {
|
|
||||||
if (!isDependentsSucceeded())
|
|
||||||
throw getException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+2
-3
@@ -1,8 +1,7 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
package org.jackhuang.hmcl.countly;
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
|
||||||
package org.jackhuang.hmcl.util;
|
|
||||||
|
|
||||||
import org.jackhuang.hmcl.Metadata;
|
import org.jackhuang.hmcl.Metadata;
|
||||||
|
import org.jackhuang.hmcl.util.StringUtils;
|
||||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||||
|
|
||||||
@@ -1,413 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
|
||||||
package org.jackhuang.hmcl.download;
|
|
||||||
|
|
||||||
import org.glavo.url.WebURL;
|
|
||||||
import org.jackhuang.hmcl.download.cleanroom.CleanroomRemoteVersion;
|
|
||||||
import org.jackhuang.hmcl.download.fabric.FabricRemoteVersion;
|
|
||||||
import org.jackhuang.hmcl.download.forge.ForgeRemoteVersion;
|
|
||||||
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
|
|
||||||
import org.jackhuang.hmcl.download.neoforge.NeoForgeRemoteVersion;
|
|
||||||
import org.jackhuang.hmcl.download.optifine.OptiFineRemoteVersion;
|
|
||||||
import org.jackhuang.hmcl.game.AssetObject;
|
|
||||||
import org.jackhuang.hmcl.game.GameComponentType;
|
|
||||||
import org.jackhuang.hmcl.game.HMCLCacheRepository;
|
|
||||||
import org.jackhuang.hmcl.setting.DownloadSource;
|
|
||||||
import org.jackhuang.hmcl.task.Task;
|
|
||||||
import org.jackhuang.hmcl.util.RegionUtils;
|
|
||||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
|
||||||
import org.jetbrains.annotations.NotNullByDefault;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
import org.jetbrains.annotations.Unmodifiable;
|
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
import java.util.function.Function;
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
/// @author Glavo
|
|
||||||
@NotNullByDefault
|
|
||||||
public final class HMCLDownloadProvider extends DownloadProvider {
|
|
||||||
private volatile DownloadSource versionListSource = DownloadSource.MIRROR;
|
|
||||||
private volatile DownloadSource fileSource = DownloadSource.MIRROR;
|
|
||||||
|
|
||||||
private static boolean preferMirror(DownloadSource source) {
|
|
||||||
if ("CN".equals(RegionUtils.MACHINE_REGION)) {
|
|
||||||
return source != DownloadSource.OFFICIAL;
|
|
||||||
}
|
|
||||||
return source == DownloadSource.MIRROR;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static DownloadCandidates getCandidates(
|
|
||||||
DownloadSource source,
|
|
||||||
WebURL officialUrl,
|
|
||||||
List<WebURL> mirrorUrls
|
|
||||||
) {
|
|
||||||
if (preferMirror(source)) {
|
|
||||||
var candidates = new ArrayList<DownloadCandidate>(mirrorUrls.size() + 1);
|
|
||||||
candidates.addAll(spreadPrimary(mirrorUrls.stream().map(DownloadCandidate::of).toList(), officialUrl.href()));
|
|
||||||
candidates.add(DownloadCandidate.of(officialUrl));
|
|
||||||
return DownloadCandidates.of(List.copyOf(candidates));
|
|
||||||
} else {
|
|
||||||
var candidates = new ArrayList<DownloadCandidate>(mirrorUrls.size() + 1);
|
|
||||||
candidates.add(DownloadCandidate.of(officialUrl));
|
|
||||||
for (WebURL mirrorUrl : mirrorUrls) {
|
|
||||||
candidates.add(DownloadCandidate.of(mirrorUrl));
|
|
||||||
}
|
|
||||||
return DownloadCandidates.of(List.copyOf(candidates));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Rotates the first mirror among the top entries using a stable hash of the URL, so concurrent
|
|
||||||
/// file downloads spread across healthy mirrors instead of stampeding one entry point.
|
|
||||||
private static List<DownloadCandidate> spreadPrimary(List<DownloadCandidate> mirrors, String key) {
|
|
||||||
if (mirrors.size() <= 1)
|
|
||||||
return mirrors;
|
|
||||||
|
|
||||||
int top = Math.min(2, mirrors.size());
|
|
||||||
int primary = Math.floorMod(key.hashCode(), top);
|
|
||||||
if (primary == 0)
|
|
||||||
return mirrors;
|
|
||||||
|
|
||||||
var rotated = new ArrayList<DownloadCandidate>(mirrors.size());
|
|
||||||
rotated.add(mirrors.get(primary));
|
|
||||||
for (int i = 0; i < mirrors.size(); i++) {
|
|
||||||
if (i != primary)
|
|
||||||
rotated.add(mirrors.get(i));
|
|
||||||
}
|
|
||||||
return rotated;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static DownloadCandidates getCandidates(DownloadSource source, List<MirrorRule> rules, List<String> urls) {
|
|
||||||
if (urls.isEmpty()) {
|
|
||||||
throw new IllegalArgumentException("urls cannot be empty");
|
|
||||||
}
|
|
||||||
|
|
||||||
// true means prefer mirror, false means prefer official, null means do not use mirror
|
|
||||||
@Nullable Boolean mirrorFirst;
|
|
||||||
if ("CN".equals(RegionUtils.MACHINE_REGION)) {
|
|
||||||
mirrorFirst = switch (source) {
|
|
||||||
case DEFAULT, MIRROR -> true;
|
|
||||||
case OFFICIAL -> false;
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
mirrorFirst = switch (source) {
|
|
||||||
case DEFAULT, OFFICIAL -> null;
|
|
||||||
case MIRROR -> true;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mirrorFirst == null) {
|
|
||||||
return DownloadCandidates.of(urls.stream().map(DownloadCandidate::of).toArray(DownloadCandidate[]::new));
|
|
||||||
}
|
|
||||||
|
|
||||||
var candidates = new ArrayList<DownloadCandidate>(urls.size() * 3);
|
|
||||||
for (String url : urls) {
|
|
||||||
DownloadCandidate candidate = DownloadCandidate.of(url);
|
|
||||||
if (candidate.url() == null) {
|
|
||||||
// Invalid URL, just add it to the list and let the download task handle it.
|
|
||||||
candidates.add(candidate);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean added = false;
|
|
||||||
for (MirrorRule rule : rules) {
|
|
||||||
@Nullable List<DownloadCandidate> list = rule.getCandidates(candidate, mirrorFirst);
|
|
||||||
if (list != null) {
|
|
||||||
candidates.addAll(list);
|
|
||||||
added = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!added)
|
|
||||||
candidates.add(candidate);
|
|
||||||
}
|
|
||||||
|
|
||||||
return DownloadCandidates.of(List.copyOf(candidates));
|
|
||||||
}
|
|
||||||
|
|
||||||
private final Supplier<@Unmodifiable List<String>> mirrorRoots;
|
|
||||||
private final List<MirrorRule> rules;
|
|
||||||
|
|
||||||
public HMCLDownloadProvider(Supplier<@Unmodifiable List<String>> mirrorRoots) {
|
|
||||||
this.mirrorRoots = Objects.requireNonNull(mirrorRoots);
|
|
||||||
this.rules = List.of(
|
|
||||||
new MultiRootRule("https://bmclapi2.bangbang93.com", Function.identity()),
|
|
||||||
new MultiRootRule("https://bmclapi.bangbang93.com", Function.identity()),
|
|
||||||
new MultiRootRule("https://launchermeta.mojang.com", Function.identity()),
|
|
||||||
new MultiRootRule("https://piston-meta.mojang.com", Function.identity()),
|
|
||||||
new MultiRootRule("https://piston-data.mojang.com", Function.identity()),
|
|
||||||
new MultiRootRule("https://launcher.mojang.com", Function.identity()),
|
|
||||||
new MultiRootRule("https://libraries.minecraft.net", root -> root + "/libraries"),
|
|
||||||
new MultiRootRule("http://files.minecraftforge.net/maven", root -> root + "/maven"),
|
|
||||||
new MultiRootRule("https://files.minecraftforge.net/maven", root -> root + "/maven"),
|
|
||||||
new MultiRootRule("https://maven.minecraftforge.net", root -> root + "/maven"),
|
|
||||||
(source, mirrorFirst) -> {
|
|
||||||
String neoforgedMaven = "https://maven.neoforged.net/";
|
|
||||||
String neoforgedMavenRelease = "https://maven.neoforged.net/releases/";
|
|
||||||
|
|
||||||
if (source.url() == null || !source.url().href().startsWith(neoforgedMaven))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var candidates = new ArrayList<DownloadCandidate>();
|
|
||||||
List<String> roots = mirrorRoots.get();
|
|
||||||
|
|
||||||
if (source.url().href().startsWith(neoforgedMavenRelease)) {
|
|
||||||
String suffix = source.url().href().substring(neoforgedMavenRelease.length());
|
|
||||||
List<DownloadCandidate> mirrors = roots.stream()
|
|
||||||
.map(root -> DownloadCandidate.of(root + "/maven/" + suffix))
|
|
||||||
.toList();
|
|
||||||
if (mirrorFirst) {
|
|
||||||
candidates.addAll(spreadPrimary(mirrors, source.url().href()));
|
|
||||||
candidates.add(source);
|
|
||||||
} else {
|
|
||||||
candidates.add(source);
|
|
||||||
candidates.addAll(mirrors);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
candidates.add(source);
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://neoforged.forgecdn.net
|
|
||||||
candidates.add(DownloadCandidate.of("https://neoforged.forgecdn.net/" + source.url().href().substring(neoforgedMaven.length())));
|
|
||||||
return candidates;
|
|
||||||
},
|
|
||||||
new MultiRootRule("http://dl.liteloader.com/versions", root -> root + "/maven"),
|
|
||||||
new MultiRootRule("https://dl.liteloader.com/versions", root -> root + "/maven"),
|
|
||||||
new MultiRootRule("https://meta.fabricmc.net", root -> root + "/fabric-meta"),
|
|
||||||
new MultiRootRule("https://maven.fabricmc.net", root -> root + "/maven"),
|
|
||||||
new MultiRootRule("https://authlib-injector.yushi.moe", root -> root + "/mirrors/authlib-injector"),
|
|
||||||
new SimpleRule("https://repo1.maven.org/maven2", "https://mirrors.cloud.tencent.com/nexus/repository/maven-public"),
|
|
||||||
new SimpleRule("https://repo.maven.apache.org/maven2", "https://mirrors.cloud.tencent.com/nexus/repository/maven-public"),
|
|
||||||
new SimpleRule("https://hmcl.glavo.site/metadata/cleanroom", "https://alist.8mi.tech/d/mirror/HMCL-Metadata/Auto/cleanroom"),
|
|
||||||
new SimpleRule("https://hmcl.glavo.site/metadata/fmllibs", "https://alist.8mi.tech/d/mirror/HMCL-Metadata/Auto/fmllibs"),
|
|
||||||
new SimpleRule("https://zkitefly.github.io/unlisted-versions-of-minecraft", "https://alist.8mi.tech/d/mirror/unlisted-versions-of-minecraft/Auto"),
|
|
||||||
|
|
||||||
// https://github.com/mcmod-info-mirror/mcim-rust-api
|
|
||||||
new SimpleRule("https://api.modrinth.com", null, "https://mod.mcimirror.top/modrinth"),
|
|
||||||
new SimpleRule("https://cdn.modrinth.com", null, "https://mod.mcimirror.top"),
|
|
||||||
new SimpleRule("https://api.curseforge.com", null, "https://mod.mcimirror.top/curseforge"),
|
|
||||||
new SimpleRule("https://edge.forgecdn.net", null, "https://mod.mcimirror.top")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String primaryMirrorRoot() {
|
|
||||||
List<String> roots = mirrorRoots.get();
|
|
||||||
return roots.isEmpty() ? "https://bmclapi2.bangbang93.com" : roots.get(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<WebURL> mirrorUrls(String suffix) {
|
|
||||||
return mirrorRoots.get().stream()
|
|
||||||
.map(root -> WebURL.parse(root + suffix))
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Task<? extends ComponentRemoteVersionList<?>> fetchVersionsAsync(GameComponentType type, @Nullable GameVersionNumber gameVersion) {
|
|
||||||
assert (gameVersion == null) == (type == GameComponentType.GAME);
|
|
||||||
String bmclRoot = primaryMirrorRoot();
|
|
||||||
switch (type) {
|
|
||||||
case FORGE -> {
|
|
||||||
Task<ComponentRemoteVersionList<ForgeRemoteVersion>> fetchOfficial = ForgeRemoteVersion.fetchAsync(this, gameVersion);
|
|
||||||
|
|
||||||
DownloadSource source = versionListSource;
|
|
||||||
if (source == DownloadSource.OFFICIAL) {
|
|
||||||
return fetchOfficial;
|
|
||||||
}
|
|
||||||
|
|
||||||
Task<ComponentRemoteVersionList<ForgeRemoteVersion>> fetchBMCL = ForgeRemoteVersion.fetchBMCLAsync(this, bmclRoot, gameVersion);
|
|
||||||
if (preferMirror(source)) {
|
|
||||||
return new FallbackTask<>(fetchBMCL, fetchOfficial);
|
|
||||||
} else {
|
|
||||||
return new FallbackTask<>(fetchOfficial, fetchBMCL);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case NEO_FORGE -> {
|
|
||||||
Task<ComponentRemoteVersionList<NeoForgeRemoteVersion>> fetchOfficial = NeoForgeRemoteVersion.fetchAsync(this, gameVersion);
|
|
||||||
|
|
||||||
DownloadSource source = versionListSource;
|
|
||||||
if (source == DownloadSource.OFFICIAL) {
|
|
||||||
return fetchOfficial;
|
|
||||||
}
|
|
||||||
Task<ComponentRemoteVersionList<NeoForgeRemoteVersion>> fetchBMCL = NeoForgeRemoteVersion.fetchBMCLAsync(bmclRoot, gameVersion);
|
|
||||||
if (preferMirror(source)) {
|
|
||||||
return new FallbackTask<>(fetchBMCL, fetchOfficial);
|
|
||||||
} else {
|
|
||||||
return new FallbackTask<>(fetchOfficial, fetchBMCL);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case OPTIFINE -> {
|
|
||||||
return OptiFineRemoteVersion.fetchBMCLAsync(this, bmclRoot, gameVersion);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.fetchVersionsAsync(type, gameVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setFileSource(DownloadSource fileSource) {
|
|
||||||
this.fileSource = Objects.requireNonNull(fileSource);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setVersionListSource(DownloadSource versionListSource) {
|
|
||||||
this.versionListSource = Objects.requireNonNull(versionListSource);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public HMCLCacheRepository getCacheRepository() {
|
|
||||||
return HMCLCacheRepository.REPOSITORY;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DownloadCandidates getGameVersionListCandidates() {
|
|
||||||
return getCandidates(
|
|
||||||
versionListSource,
|
|
||||||
GameRemoteVersion.VERSION_MANIFEST_URL,
|
|
||||||
mirrorUrls("/mc/game/version_manifest.json")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public FabricLikeVersionListCandidates getFabricVersionListCandidates() {
|
|
||||||
DownloadSource source = versionListSource;
|
|
||||||
return new FabricLikeVersionListCandidates(
|
|
||||||
getCandidates(source, FabricRemoteVersion.LOADER_META_URL, mirrorUrls("/fabric-meta/v2/versions/loader")),
|
|
||||||
getCandidates(source, FabricRemoteVersion.GAME_META_URL, mirrorUrls("/fabric-meta/v2/versions/game"))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DownloadCandidates getCleanroomVersionListCandidates() {
|
|
||||||
return "CN".equals(RegionUtils.MACHINE_REGION)
|
|
||||||
? DownloadCandidates.of(CleanroomRemoteVersion.LOADER_LIST_URL, WebURL.parse("https://alist.8mi.tech/d/mirror/HMCL-Metadata/Auto/cleanroom/index.json"))
|
|
||||||
: DownloadCandidates.of(CleanroomRemoteVersion.LOADER_LIST_URL);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DownloadCandidates getDownloadCandidates(List<String> urls) {
|
|
||||||
return getCandidates(fileSource, rules, urls);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DownloadCandidates getVersionListCandidates(List<String> urls) {
|
|
||||||
return getCandidates(versionListSource, rules, urls);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DownloadCandidates getAssetObjectCandidates(AssetObject assetObject) {
|
|
||||||
String location = assetObject.getLocation();
|
|
||||||
return getCandidates(
|
|
||||||
fileSource,
|
|
||||||
WebURL.parse("https://resources.download.minecraft.net/" + location),
|
|
||||||
mirrorUrls("/assets/" + location)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNullByDefault
|
|
||||||
private static final class FallbackTask<T> extends Task<T> {
|
|
||||||
|
|
||||||
private final Task<? extends T> task;
|
|
||||||
private final Task<? extends T> fallback;
|
|
||||||
|
|
||||||
private List<Task<?>> dependencies = List.of();
|
|
||||||
|
|
||||||
public FallbackTask(Task<? extends T> task, Task<? extends T> fallback) {
|
|
||||||
this.task = task;
|
|
||||||
this.fallback = fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Collection<? extends Task<?>> getDependents() {
|
|
||||||
return List.of(task);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isRelyingOnDependents() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute() throws Exception {
|
|
||||||
if (isDependentsSucceeded()) {
|
|
||||||
setResult(task.getResult());
|
|
||||||
} else {
|
|
||||||
dependencies = List.of(fallback);
|
|
||||||
fallback.storeTo(this::setResult);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Task<?>> getDependencies() {
|
|
||||||
return dependencies;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNullByDefault
|
|
||||||
@FunctionalInterface
|
|
||||||
private interface MirrorRule {
|
|
||||||
@Nullable List<DownloadCandidate> getCandidates(DownloadCandidate source, boolean mirrorFirst);
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNullByDefault
|
|
||||||
private final class MultiRootRule implements MirrorRule {
|
|
||||||
private final String source;
|
|
||||||
private final Function<String, String> rootMapper;
|
|
||||||
|
|
||||||
private MultiRootRule(String source, Function<String, String> rootMapper) {
|
|
||||||
this.source = source;
|
|
||||||
this.rootMapper = rootMapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @Nullable List<DownloadCandidate> getCandidates(DownloadCandidate source, boolean mirrorFirst) {
|
|
||||||
if (source.url() == null || !source.url().href().startsWith(this.source))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
String urlWithoutPrefix = source.url().href().substring(this.source.length());
|
|
||||||
List<DownloadCandidate> mirrors = mirrorRoots.get().stream()
|
|
||||||
.map(root -> DownloadCandidate.of(rootMapper.apply(root) + urlWithoutPrefix))
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
var candidates = new ArrayList<DownloadCandidate>(mirrors.size() + 1);
|
|
||||||
if (mirrorFirst) {
|
|
||||||
candidates.addAll(spreadPrimary(mirrors, source.url().href()));
|
|
||||||
candidates.add(source);
|
|
||||||
} else {
|
|
||||||
candidates.add(source);
|
|
||||||
candidates.addAll(mirrors);
|
|
||||||
}
|
|
||||||
return candidates;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNullByDefault
|
|
||||||
private record SimpleRule(String source, @Nullable String first, @Nullable String fallback) implements MirrorRule {
|
|
||||||
public SimpleRule(String source, String target) {
|
|
||||||
this(source, target, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @Nullable List<DownloadCandidate> getCandidates(DownloadCandidate source, boolean mirrorFirst) {
|
|
||||||
if (source.url() == null || !source.url().href().startsWith(this.source))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
boolean addSourceFirst = !mirrorFirst || first == null;
|
|
||||||
String urlWithoutPrefix = source.url().href().substring(this.source.length());
|
|
||||||
|
|
||||||
var candidates = new ArrayList<DownloadCandidate>();
|
|
||||||
|
|
||||||
if (addSourceFirst)
|
|
||||||
candidates.add(source);
|
|
||||||
|
|
||||||
if (first != null)
|
|
||||||
candidates.add(DownloadCandidate.of(first + urlWithoutPrefix));
|
|
||||||
|
|
||||||
if (!addSourceFirst)
|
|
||||||
candidates.add(source);
|
|
||||||
|
|
||||||
if (fallback != null)
|
|
||||||
candidates.add(DownloadCandidate.of(fallback + urlWithoutPrefix));
|
|
||||||
|
|
||||||
return candidates;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,20 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
|
/*
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
* Hello Minecraft! Launcher
|
||||||
|
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
package org.jackhuang.hmcl.game;
|
package org.jackhuang.hmcl.game;
|
||||||
|
|
||||||
import javafx.beans.property.SimpleStringProperty;
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
|
||||||
package org.jackhuang.hmcl.game;
|
|
||||||
|
|
||||||
import org.jackhuang.hmcl.download.DefaultCacheRepository;
|
|
||||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
|
||||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
|
||||||
import org.jetbrains.annotations.NotNullByDefault;
|
|
||||||
|
|
||||||
/// Provides HMCL-specific game builders for an HMCL game repository.
|
|
||||||
@NotNullByDefault
|
|
||||||
public class HMCLDependencyManager extends DefaultDependencyManager {
|
|
||||||
/// Creates a dependency manager for a repository and download context.
|
|
||||||
///
|
|
||||||
/// @param repository the associated game repository
|
|
||||||
/// @param downloadProvider the remote download provider
|
|
||||||
/// @param cacheRepository the artifact cache
|
|
||||||
public HMCLDependencyManager(
|
|
||||||
HMCLGameRepository repository,
|
|
||||||
DownloadProvider downloadProvider,
|
|
||||||
DefaultCacheRepository cacheRepository) {
|
|
||||||
super(repository, downloadProvider, cacheRepository);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// {@inheritDoc}
|
|
||||||
@Override
|
|
||||||
public HMCLGameRepository getGameRepository() {
|
|
||||||
return (HMCLGameRepository) super.getGameRepository();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// {@inheritDoc}
|
|
||||||
@Override
|
|
||||||
public HMCLGameBuilder newGameBuilder(GameInstanceID instanceId) {
|
|
||||||
GameInstanceManifest initialManifest = new GameInstanceManifest(instanceId);
|
|
||||||
DefaultGameRepositoryDraft draft = openGameBuilderDraft(initialManifest, null);
|
|
||||||
return new HMCLGameBuilder(this, instanceId, null, draft, initialManifest);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// {@inheritDoc}
|
|
||||||
@Override
|
|
||||||
public HMCLGameBuilder newGameBuilder(GameInstance instance) {
|
|
||||||
validateGameInstance(instance);
|
|
||||||
HMCLGameInstance updateTarget = (HMCLGameInstance) instance;
|
|
||||||
|
|
||||||
GameInstanceManifest initialManifest = new GameInstanceManifest(updateTarget.getId());
|
|
||||||
DefaultGameRepositoryDraft draft = openGameBuilderDraft(initialManifest, updateTarget);
|
|
||||||
return new HMCLGameBuilder(
|
|
||||||
this, updateTarget.getId(), updateTarget, draft, initialManifest);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
|
||||||
package org.jackhuang.hmcl.game;
|
|
||||||
|
|
||||||
import org.jackhuang.hmcl.download.DefaultGameBuilder;
|
|
||||||
import org.jetbrains.annotations.NotNullByDefault;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
/// Builds game instances and applies HMCL-specific post-installation settings.
|
|
||||||
@NotNullByDefault
|
|
||||||
public class HMCLGameBuilder extends DefaultGameBuilder {
|
|
||||||
/// Creates a builder around an HMCL target already reserved by its dependency manager.
|
|
||||||
///
|
|
||||||
/// @param dependencyManager the dependency manager for the target repository
|
|
||||||
/// @param instanceId the id of the reserved instance
|
|
||||||
/// @param updateTarget the exact update target retained by the draft, or `null` for install
|
|
||||||
/// @param draft the open draft containing `initialManifest`
|
|
||||||
/// @param initialManifest the empty target manifest retained by `draft`
|
|
||||||
HMCLGameBuilder(
|
|
||||||
HMCLDependencyManager dependencyManager,
|
|
||||||
GameInstanceID instanceId,
|
|
||||||
@Nullable HMCLGameInstance updateTarget,
|
|
||||||
DefaultGameRepositoryDraft draft,
|
|
||||||
GameInstanceManifest initialManifest) {
|
|
||||||
super(dependencyManager, instanceId, updateTarget, draft, initialManifest);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// {@inheritDoc}
|
|
||||||
///
|
|
||||||
/// When isolation was enabled, persists the corresponding HMCL instance setting so subsequent
|
|
||||||
/// launches use the instance root. A read-only instance retains its existing setting.
|
|
||||||
@Override
|
|
||||||
protected void onInstanceCommitted(DefaultGameInstance instance) {
|
|
||||||
if (isolationEnabled) {
|
|
||||||
((HMCLGameInstance) instance).enableIsolation();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,848 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
|
||||||
package org.jackhuang.hmcl.game;
|
|
||||||
|
|
||||||
import com.google.gson.JsonObject;
|
|
||||||
import com.google.gson.JsonParseException;
|
|
||||||
import com.google.gson.reflect.TypeToken;
|
|
||||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
|
||||||
import javafx.beans.property.ReadOnlyObjectPropertyBase;
|
|
||||||
import javafx.scene.image.Image;
|
|
||||||
import org.jackhuang.hmcl.Metadata;
|
|
||||||
import org.jackhuang.hmcl.addon.mod.ModLoaderType;
|
|
||||||
import org.jackhuang.hmcl.java.JavaRuntime;
|
|
||||||
import org.jackhuang.hmcl.modpack.ModpackConfiguration;
|
|
||||||
import org.jackhuang.hmcl.modpack.ModpackProvider;
|
|
||||||
import org.jackhuang.hmcl.setting.*;
|
|
||||||
import org.jackhuang.hmcl.ui.FXUtils;
|
|
||||||
import org.jackhuang.hmcl.util.FileSaver;
|
|
||||||
import org.jackhuang.hmcl.util.Lang;
|
|
||||||
import org.jackhuang.hmcl.util.StringUtils;
|
|
||||||
import org.jackhuang.hmcl.util.gson.JsonSchema;
|
|
||||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
|
||||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
|
||||||
import org.jackhuang.hmcl.util.platform.SystemInfo;
|
|
||||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
|
||||||
import org.jetbrains.annotations.Contract;
|
|
||||||
import org.jetbrains.annotations.NotNullByDefault;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.lang.ref.WeakReference;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.setting.SettingsManager.settings;
|
|
||||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
|
||||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
|
||||||
|
|
||||||
/// HMCL-specific game instance that owns instance-local settings and run-directory policy.
|
|
||||||
@NotNullByDefault
|
|
||||||
public class HMCLGameInstance extends DefaultGameInstance {
|
|
||||||
|
|
||||||
/// Whether the instance-local game settings file has already been inspected.
|
|
||||||
private boolean gameSettingsLoaded;
|
|
||||||
|
|
||||||
/// Whether the instance-local game settings file cannot be overwritten safely.
|
|
||||||
private boolean gameSettingsReadOnly;
|
|
||||||
|
|
||||||
/// Cached instance-local game settings, or `null` when none exist after loading.
|
|
||||||
private GameSettings.@Nullable Instance gameSettings;
|
|
||||||
|
|
||||||
/// Creates a registered instance bound to the given repository snapshot.
|
|
||||||
///
|
|
||||||
/// @param snapshot the repository snapshot that owns this instance
|
|
||||||
/// @param id the instance id
|
|
||||||
/// @param manifest the stored instance manifest
|
|
||||||
protected HMCLGameInstance(DefaultGameRepositorySnapshot snapshot, GameInstanceID id, GameInstanceManifest manifest) {
|
|
||||||
this(snapshot, id, manifest, (Path) null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a registered instance with an optional non-conventional manifest path.
|
|
||||||
///
|
|
||||||
/// @param snapshot the repository snapshot that owns this instance
|
|
||||||
/// @param id the instance id
|
|
||||||
/// @param manifest the stored instance manifest
|
|
||||||
/// @param manifestFile the actual manifest JSON path, or `null` for the layout default
|
|
||||||
protected HMCLGameInstance(
|
|
||||||
DefaultGameRepositorySnapshot snapshot,
|
|
||||||
GameInstanceID id,
|
|
||||||
GameInstanceManifest manifest,
|
|
||||||
@Nullable Path manifestFile) {
|
|
||||||
super(snapshot, id, manifest, manifestFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates an instance that shares mutable instance-local state with another instance.
|
|
||||||
///
|
|
||||||
/// Used when the repository clones a snapshot so that settings and the icon property remain
|
|
||||||
/// available on the new wrapper.
|
|
||||||
private HMCLGameInstance(
|
|
||||||
DefaultGameRepositorySnapshot snapshot,
|
|
||||||
GameInstanceID id,
|
|
||||||
GameInstanceManifest manifest,
|
|
||||||
HMCLGameInstance shareState) {
|
|
||||||
super(snapshot, id, manifest, shareState);
|
|
||||||
this.gameSettingsLoaded = shareState.gameSettingsLoaded;
|
|
||||||
this.gameSettingsReadOnly = shareState.gameSettingsReadOnly;
|
|
||||||
this.gameSettings = shareState.gameSettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected HMCLGameInstance withNewSnapshot(DefaultGameRepositorySnapshot newSnapshot) {
|
|
||||||
return new HMCLGameInstance(newSnapshot, id, manifest, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected HMCLGameInstance withManifest(DefaultGameRepositorySnapshot newSnapshot, GameInstanceManifest manifest) {
|
|
||||||
return new HMCLGameInstance(newSnapshot, id, manifest, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public HMCLGameRepository getRepository() {
|
|
||||||
return (HMCLGameRepository) super.getRepository();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public HMCLGameRepositoryLayout getLayout() {
|
|
||||||
return (HMCLGameRepositoryLayout) super.getLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the HMCL modpack configuration file for this instance.
|
|
||||||
///
|
|
||||||
/// @return the `modpack.cfg` path in the instance root
|
|
||||||
@Override
|
|
||||||
public Path getModpackConfigurationFile() {
|
|
||||||
return getLayout().getModpackConfigurationFile(getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns whether this instance has an HMCL modpack configuration file.
|
|
||||||
///
|
|
||||||
/// @return whether [#getModpackConfigurationFile()] exists
|
|
||||||
public boolean isModpack() {
|
|
||||||
return Files.exists(getModpackConfigurationFile());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reads this instance's HMCL modpack configuration.
|
|
||||||
///
|
|
||||||
/// @return the parsed configuration, or `null` when the file does not exist
|
|
||||||
/// @throws IOException if the configuration cannot be read
|
|
||||||
public @Nullable ModpackConfiguration<?> readModpackConfiguration() throws IOException {
|
|
||||||
Path file = getModpackConfigurationFile();
|
|
||||||
if (Files.notExists(file)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return JsonUtils.fromJsonFile(file, ModpackConfiguration.class);
|
|
||||||
} catch (JsonParseException e) {
|
|
||||||
throw new IOException("Malformed modpack configuration: " + file, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Path getRunDirectory() {
|
|
||||||
return getRepository().computeRunDirectory(getId(), isModpack(), getSettings());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the loaded instance-local game settings, loading them while this instance is current.
|
|
||||||
///
|
|
||||||
/// @return the settings, or `null` when no local settings exist after loading
|
|
||||||
public @Nullable GameSettings.Instance getSettings() {
|
|
||||||
ensureGameSettingsLoaded();
|
|
||||||
return gameSettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the instance-local game settings, creating an empty settings object when absent.
|
|
||||||
///
|
|
||||||
/// @return the settings, or `null` when the settings file is read-only and no settings are loaded
|
|
||||||
public @Nullable GameSettings.Instance getSettingsOrCreate() {
|
|
||||||
GameSettings.Instance setting = getSettings();
|
|
||||||
if (setting == null) {
|
|
||||||
setting = createSettings();
|
|
||||||
}
|
|
||||||
return setting;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resolves this instance's effective settings against its selected parent preset.
|
|
||||||
///
|
|
||||||
/// @return the effective settings
|
|
||||||
public GameSettings.Effective getEffectiveSettings() {
|
|
||||||
@Nullable GameSettings.Instance setting = getSettings();
|
|
||||||
return GameSettings.resolve(getRepository().getParentGameSettings(setting), setting);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates empty instance-local game settings when none are loaded.
|
|
||||||
///
|
|
||||||
/// @return the settings, or `null` when settings are read-only or already present in a non-creatable state
|
|
||||||
public @Nullable GameSettings.Instance createSettings() {
|
|
||||||
ensureGameSettingsLoaded();
|
|
||||||
if (gameSettingsReadOnly) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (gameSettings != null) {
|
|
||||||
return gameSettings;
|
|
||||||
}
|
|
||||||
return initSettings(new GameSettings.Instance(), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns whether the instance-local game settings file cannot be overwritten safely.
|
|
||||||
///
|
|
||||||
/// @return whether the settings are loaded in read-only mode
|
|
||||||
public boolean isSettingsReadOnly() {
|
|
||||||
ensureGameSettingsLoaded();
|
|
||||||
return gameSettingsReadOnly;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Enables instance-local running-directory selection for this instance.
|
|
||||||
///
|
|
||||||
/// A blank local running directory resolves to the instance root. This operation is idempotent
|
|
||||||
/// and schedules a settings save only when it adds the override. It leaves the instance
|
|
||||||
/// unchanged when its local settings cannot be written safely.
|
|
||||||
public void enableIsolation() {
|
|
||||||
if (isSettingsReadOnly()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable GameSettings.Instance setting = getSettingsOrCreate();
|
|
||||||
if (setting != null
|
|
||||||
&& setting.getOverrideProperties().add(GameSettings.PROPERTY_RUNNING_DIRECTORY)) {
|
|
||||||
saveSettings();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Backs up and overwrites the settings file when this instance still owns its settings.
|
|
||||||
public void forceOverwriteSettings() {
|
|
||||||
ensureGameSettingsLoaded();
|
|
||||||
if (!ownsGameSettings()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
GameSettings.Instance setting = gameSettings;
|
|
||||||
if (setting == null) {
|
|
||||||
setting = new GameSettings.Instance();
|
|
||||||
gameSettings = setting;
|
|
||||||
gameSettingsLoaded = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean installAutoSave = !setting.isSavable();
|
|
||||||
Path file = getGameSettingsFile().toAbsolutePath().normalize();
|
|
||||||
SettingFileUtils.backupInvalidConfig(file);
|
|
||||||
setting.setSchema(GameSettings.Instance.CURRENT_SCHEMA);
|
|
||||||
setting.setSavable(true);
|
|
||||||
setting.setBackupOnNextSave(false);
|
|
||||||
gameSettingsReadOnly = false;
|
|
||||||
saveSettings();
|
|
||||||
if (installAutoSave) {
|
|
||||||
setting.addListener(a -> saveSettings());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Saves the settings asynchronously when writable and still owned by the current instance.
|
|
||||||
public void saveSettings() {
|
|
||||||
if (gameSettings == null || gameSettingsReadOnly || !ownsGameSettings()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
GameSettings.Instance setting = gameSettings;
|
|
||||||
Path file = getGameSettingsFile().toAbsolutePath().normalize();
|
|
||||||
try {
|
|
||||||
Files.createDirectories(file.getParent());
|
|
||||||
} catch (IOException e) {
|
|
||||||
LOG.warning("Failed to create directory: " + file.getParent(), e);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (setting.isBackupOnNextSave()) {
|
|
||||||
setting.setBackupOnNextSave(false);
|
|
||||||
SettingFileUtils.backupInvalidConfig(file);
|
|
||||||
}
|
|
||||||
FileSaver.save(file, LauncherSettings.SETTINGS_GSON.toJson(setting));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Saves the settings synchronously when writable and still owned by the current instance.
|
|
||||||
///
|
|
||||||
/// @throws IOException if saving the file fails
|
|
||||||
public void saveSettingsSync() throws IOException {
|
|
||||||
if (gameSettings == null || gameSettingsReadOnly || !ownsGameSettings()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
GameSettings.Instance setting = gameSettings;
|
|
||||||
Path file = getGameSettingsFile().toAbsolutePath().normalize();
|
|
||||||
Files.createDirectories(file.getParent());
|
|
||||||
if (setting.isBackupOnNextSave()) {
|
|
||||||
setting.setBackupOnNextSave(false);
|
|
||||||
SettingFileUtils.backupInvalidConfig(file);
|
|
||||||
}
|
|
||||||
FileUtils.saveSafely(file, LauncherSettings.SETTINGS_GSON.toJson(setting));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Initializes this instance with the given settings object.
|
|
||||||
///
|
|
||||||
/// @param setting the settings to install
|
|
||||||
/// @param allowSave whether the settings may be written back to disk
|
|
||||||
/// @return the installed settings
|
|
||||||
public GameSettings.Instance initSettings(GameSettings.Instance setting, boolean allowSave) {
|
|
||||||
normalizeRunningDirectoryOverride(setting);
|
|
||||||
setting.setSavable(allowSave);
|
|
||||||
gameSettingsLoaded = true;
|
|
||||||
gameSettings = setting;
|
|
||||||
setting.iconProperty().addListener(observable -> invalidateIconImage());
|
|
||||||
if (allowSave) {
|
|
||||||
gameSettingsReadOnly = false;
|
|
||||||
setting.addListener(a -> saveSettings());
|
|
||||||
} else {
|
|
||||||
gameSettingsReadOnly = true;
|
|
||||||
}
|
|
||||||
return setting;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a deep copy of the currently loaded settings, or a new settings object that inherits
|
|
||||||
/// the effective parent preset when no local settings exist.
|
|
||||||
///
|
|
||||||
/// @return a detached copy suitable for installing into another instance
|
|
||||||
public GameSettings.Instance copySettings() {
|
|
||||||
@Nullable GameSettings.Instance setting = getSettings();
|
|
||||||
if (setting != null) {
|
|
||||||
return JsonUtils.clone(LauncherSettings.SETTINGS_GSON, setting, TypeToken.get(GameSettings.Instance.class));
|
|
||||||
}
|
|
||||||
|
|
||||||
GameSettings.Instance copied = new GameSettings.Instance();
|
|
||||||
copied.parentProperty().setValue(
|
|
||||||
getEffectiveSettings().getPreset().idProperty().getValue());
|
|
||||||
return copied;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the first custom icon file found in this instance's root directory.
|
|
||||||
///
|
|
||||||
/// @return the icon file, or empty when no supported icon file exists
|
|
||||||
public @Nullable Path getIconFile() {
|
|
||||||
for (String extension : FXUtils.IMAGE_EXTENSIONS) {
|
|
||||||
Path file = getInstanceRoot().resolve("icon." + extension);
|
|
||||||
if (Files.exists(file)) {
|
|
||||||
return file;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Replaces this instance's custom icon file.
|
|
||||||
///
|
|
||||||
/// Existing supported icon files are removed before `iconFile` is copied.
|
|
||||||
///
|
|
||||||
/// @param iconFile the source icon file
|
|
||||||
/// @throws IOException if the icon cannot be copied
|
|
||||||
/// @throws IllegalArgumentException if the file extension is unsupported
|
|
||||||
public void setIconFile(Path iconFile) throws IOException {
|
|
||||||
String extension = FileUtils.getExtension(iconFile).toLowerCase(Locale.ROOT);
|
|
||||||
if (!FXUtils.IMAGE_EXTENSIONS.contains(extension)) {
|
|
||||||
throw new IllegalArgumentException("Unsupported icon file: " + extension);
|
|
||||||
}
|
|
||||||
|
|
||||||
clearIconFiles();
|
|
||||||
FileUtils.copyFile(iconFile, getInstanceRoot().resolve("icon." + extension));
|
|
||||||
invalidateIconImage();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Deletes all supported custom icon files for this instance.
|
|
||||||
///
|
|
||||||
/// Individual deletion failures are logged and do not stop later files from being attempted.
|
|
||||||
public void deleteIconFile() {
|
|
||||||
clearIconFiles();
|
|
||||||
invalidateIconImage();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void clearIconFiles() {
|
|
||||||
for (String extension : FXUtils.IMAGE_EXTENSIONS) {
|
|
||||||
Path file = getInstanceRoot().resolve("icon." + extension);
|
|
||||||
try {
|
|
||||||
Files.deleteIfExists(file);
|
|
||||||
} catch (IOException e) {
|
|
||||||
LOG.warning("Failed to delete instance icon file: " + file, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Soft-cached icon image for this instance id.
|
|
||||||
///
|
|
||||||
/// Shared across COW snapshot wrappers. The computed [Image] is retained only via a
|
|
||||||
/// [WeakReference], so it can be reclaimed under memory pressure when nothing else holds it.
|
|
||||||
private @Nullable WeakCachedIconImageProperty iconImage;
|
|
||||||
|
|
||||||
/// Returns the observable icon image for this instance.
|
|
||||||
///
|
|
||||||
/// The image is stored in a [WeakReference] cache: when nothing else strongly references it
|
|
||||||
/// (for example no UI node is displaying it), the JVM may reclaim the [Image] under memory
|
|
||||||
/// pressure. The next [#getIconImage] reloads it.
|
|
||||||
///
|
|
||||||
/// @return the icon image property
|
|
||||||
public ReadOnlyObjectProperty<Image> iconImageProperty() {
|
|
||||||
if (iconImage == null) {
|
|
||||||
iconImage = new WeakCachedIconImageProperty();
|
|
||||||
}
|
|
||||||
return iconImage;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the icon image selected for this instance.
|
|
||||||
///
|
|
||||||
/// Equivalent to [ReadOnlyObjectProperty#get()] on [#iconImageProperty].
|
|
||||||
///
|
|
||||||
/// @return the selected or derived icon image
|
|
||||||
public Image getIconImage() {
|
|
||||||
return iconImageProperty().get();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Drops the soft-cached icon image and notifies observers.
|
|
||||||
public void invalidateIconImage() {
|
|
||||||
((WeakCachedIconImageProperty) iconImageProperty()).invalidate();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Soft-cached read-only icon property compatible with JavaFX versions before 19.
|
|
||||||
private final class WeakCachedIconImageProperty extends ReadOnlyObjectPropertyBase<Image> {
|
|
||||||
private @Nullable WeakReference<Image> cache;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Object getBean() {
|
|
||||||
return HMCLGameInstance.this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return "iconImage";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Image get() {
|
|
||||||
WeakReference<Image> current = cache;
|
|
||||||
Image image = current != null ? current.get() : null;
|
|
||||||
if (image != null) {
|
|
||||||
return image;
|
|
||||||
}
|
|
||||||
|
|
||||||
image = computeIconImage();
|
|
||||||
cache = new WeakReference<>(image);
|
|
||||||
return image;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Computes the icon image from settings, custom files, and the launch manifest.
|
|
||||||
///
|
|
||||||
/// @return the selected or derived icon image
|
|
||||||
private Image computeIconImage() {
|
|
||||||
@Nullable GameSettings.Instance setting = getSettings();
|
|
||||||
GameInstanceIconType iconType = setting != null
|
|
||||||
? Lang.requireNonNullElse(setting.iconProperty().getValue(), GameInstanceIconType.DEFAULT)
|
|
||||||
: GameInstanceIconType.DEFAULT;
|
|
||||||
if (iconType != GameInstanceIconType.DEFAULT) {
|
|
||||||
return iconType.getIcon();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable Path iconFile = getIconFile();
|
|
||||||
if (iconFile != null) {
|
|
||||||
try {
|
|
||||||
return FXUtils.loadImage(iconFile, 64, 64, true, true);
|
|
||||||
} catch (Exception e) {
|
|
||||||
LOG.warning("Failed to load instance icon for " + getId(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (ModLoaderType modLoader : getModLoaders()) {
|
|
||||||
return GameInstanceIconType.getIconType(modLoader).getIcon();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasComponent(GameComponentType.OPTIFINE))
|
|
||||||
return GameInstanceIconType.OPTIFINE.getIcon();
|
|
||||||
|
|
||||||
GameVersionNumber version = getVersion();
|
|
||||||
if (version instanceof GameVersionNumber.LegacySnapshot
|
|
||||||
|| version.isAprilFools()
|
|
||||||
|| version instanceof GameVersionNumber.Release release && release.getEaType() != GameVersionNumber.Release.ReleaseType.GA)
|
|
||||||
return GameInstanceIconType.COMMAND.getIcon();
|
|
||||||
else if (version instanceof GameVersionNumber.Old)
|
|
||||||
return GameInstanceIconType.CRAFT_TABLE.getIcon();
|
|
||||||
else
|
|
||||||
return GameInstanceIconType.GRASS.getIcon();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clears the weak cache and notifies listeners.
|
|
||||||
void invalidate() {
|
|
||||||
cache = null;
|
|
||||||
fireValueChangedEvent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates the marker indicating that the most recent launch ended abnormally.
|
|
||||||
public void markLaunchedAbnormally() {
|
|
||||||
try {
|
|
||||||
Files.createFile(getInstanceRoot().resolve(".abnormal"));
|
|
||||||
} catch (IOException ignored) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Deletes the abnormal-launch marker when present.
|
|
||||||
///
|
|
||||||
/// @return whether a regular marker file was present
|
|
||||||
public boolean unmarkLaunchedAbnormally() {
|
|
||||||
Path file = getInstanceRoot().resolve(".abnormal");
|
|
||||||
if (!Files.isRegularFile(file)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Files.delete(file);
|
|
||||||
} catch (IOException e) {
|
|
||||||
LOG.warning("Failed to delete abnormal launch marker: " + file, e);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Loads settings once while this object is the repository's current instance.
|
|
||||||
private void ensureGameSettingsLoaded() {
|
|
||||||
if (!gameSettingsLoaded && getRepository().findInstance(id) == this) {
|
|
||||||
loadGameSettings();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns whether this instance still owns the settings object used by the current snapshot.
|
|
||||||
private boolean ownsGameSettings() {
|
|
||||||
@Nullable HMCLGameInstance current = getRepository().findInstance(id);
|
|
||||||
boolean owns = current == this
|
|
||||||
|| current != null && gameSettings != null && current.gameSettings == gameSettings;
|
|
||||||
if (!owns && gameSettings != null) {
|
|
||||||
gameSettings.setSavable(false);
|
|
||||||
}
|
|
||||||
return owns;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadGameSettings() {
|
|
||||||
gameSettingsLoaded = true;
|
|
||||||
LoadResult result = loadGameSettingsFile(getGameSettingsFile());
|
|
||||||
if (result.setting() != null) {
|
|
||||||
initSettings(result.setting(), result.allowSave());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!result.allowSave()) {
|
|
||||||
gameSettingsReadOnly = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable GameSettingsPresetID legacyParent = getRepository().getGameDirectory().getLegacyGameSettings();
|
|
||||||
if (SettingsManager.getGameSettings(legacyParent) == null) {
|
|
||||||
legacyParent = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
LegacyGameSettingsMigrator.InstanceMigrationResult migrationResult =
|
|
||||||
LegacyGameSettingsMigrator.migrateInstanceGameSettings(
|
|
||||||
getRepository(), id, legacyParent);
|
|
||||||
if (migrationResult != null) {
|
|
||||||
initSettings(migrationResult.setting(), true);
|
|
||||||
try {
|
|
||||||
saveSettingsSync();
|
|
||||||
migrationResult.saveReceipt();
|
|
||||||
} catch (IOException e) {
|
|
||||||
LOG.warning("Failed to save migrated instance game settings for " + id, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Path getGameSettingsFile() {
|
|
||||||
return getLayout().getInstanceGameSettingsFile(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public LaunchOptions.Builder getLaunchOptions(
|
|
||||||
JavaRuntime javaRuntime,
|
|
||||||
Path javaExecutable,
|
|
||||||
Path gameDir,
|
|
||||||
List<String> javaAgents,
|
|
||||||
List<String> javaArguments,
|
|
||||||
boolean makeLaunchScript) {
|
|
||||||
GameSettings.Effective vs = getEffectiveSettings();
|
|
||||||
boolean noJVMOptions = vs.getInheritable(GameSettings::noJVMOptionsProperty);
|
|
||||||
boolean autoMemory = vs.getInheritable(GameSettings::autoMemoryProperty);
|
|
||||||
GameVersionNumber gameVersionNumber = getVersion();
|
|
||||||
|
|
||||||
@Nullable Integer maxMemory;
|
|
||||||
if (autoMemory) {
|
|
||||||
maxMemory = noJVMOptions
|
|
||||||
? null
|
|
||||||
: Math.toIntExact(HMCLGameRepository.getAutoAllocatedMemory(
|
|
||||||
SystemInfo.getPhysicalMemoryStatus(),
|
|
||||||
javaRuntime.getPlatform()) / 1024L / 1024L);
|
|
||||||
} else {
|
|
||||||
maxMemory = vs.getMaxMemory();
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchOptions.Builder builder = new LaunchOptions.Builder()
|
|
||||||
.setInstanceId(getId())
|
|
||||||
.setGameDir(gameDir)
|
|
||||||
.setJava(javaRuntime)
|
|
||||||
.setJavaExecutable(javaExecutable)
|
|
||||||
.setVersionType(Metadata.TITLE)
|
|
||||||
.setVersionName(getId().id())
|
|
||||||
.setProfileName(Metadata.TITLE)
|
|
||||||
.setGameArguments(StringUtils.tokenize(vs.getInheritable(GameSettings::gameArgumentsProperty)))
|
|
||||||
.setOverrideJavaArguments(StringUtils.tokenize(vs.getInheritable(GameSettings::jvmOptionsProperty)))
|
|
||||||
.setMaxMemory(maxMemory)
|
|
||||||
.setMinMemory(vs.getInheritable(GameSettings::minMemoryProperty))
|
|
||||||
.setMetaspace(Lang.toIntOrNull(vs.getInheritable(GameSettings::permSizeProperty)))
|
|
||||||
.setEnvironmentVariables(
|
|
||||||
Lang.mapOf(StringUtils.tokenize(vs.getInheritable(GameSettings::environmentVariablesProperty))
|
|
||||||
.stream()
|
|
||||||
.map(it -> {
|
|
||||||
int idx = it.indexOf('=');
|
|
||||||
return idx >= 0 ? pair(it.substring(0, idx), it.substring(idx + 1)) : pair(it, "");
|
|
||||||
})
|
|
||||||
.collect(Collectors.toList())
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.setWidth(vs.getWidth())
|
|
||||||
.setHeight(vs.getHeight())
|
|
||||||
.setFullscreen(vs.getInheritable(GameSettings::windowTypeProperty) == GameWindowType.FULLSCREEN)
|
|
||||||
.setWrapper(vs.getInheritable(GameSettings::commandWrapperProperty))
|
|
||||||
.setProxyOption(getProxyOption())
|
|
||||||
.setPreLaunchCommand(vs.getInheritable(GameSettings::preLaunchCommandProperty))
|
|
||||||
.setPostExitCommand(vs.getInheritable(GameSettings::postExitCommandProperty))
|
|
||||||
.setNoGeneratedJVMArgs(noJVMOptions)
|
|
||||||
.setNoGeneratedOptimizingJVMArgs(vs.getInheritable(GameSettings::noOptimizingJVMOptionsProperty))
|
|
||||||
.setUseCustomNatives(vs.getInheritable(GameSettings::useCustomNativesProperty))
|
|
||||||
.setNativesDir(vs.getInheritable(GameSettings::nativesDirectoryProperty))
|
|
||||||
.setProcessPriority(vs.getInheritable(GameSettings::processPriorityProperty))
|
|
||||||
.setGraphicsBackend(vs.getInheritable(GameSettings::graphicsBackendProperty))
|
|
||||||
.setRenderer(vs.getRenderer(gameVersionNumber))
|
|
||||||
.setEnableDebugLogOutput(vs.getInheritable(GameSettings::enableDebugLogOutputProperty))
|
|
||||||
.setAllowAutoAgent(vs.getInheritable(GameSettings::allowAutoAgentProperty))
|
|
||||||
.setDisableAutoGameOptions(vs.getInheritable(GameSettings::disableAutoGameOptionsProperty))
|
|
||||||
.setUseNativeGLFWorSDL(vs.getInheritable(GameSettings::useNativeGLFWorSDLProperty))
|
|
||||||
.setUseNativeOpenAL(vs.getInheritable(GameSettings::useNativeOpenALProperty))
|
|
||||||
.setUseHighPerformanceGPU(vs.getInheritable(GameSettings::highPerformanceProperty))
|
|
||||||
.setDaemon(!makeLaunchScript && vs.getInheritable(GameSettings::launcherVisibilityProperty).isDaemon())
|
|
||||||
.setJavaAgents(javaAgents)
|
|
||||||
.setJavaArguments(javaArguments);
|
|
||||||
|
|
||||||
QuickPlayOption quickPlayOption = vs.getQuickPlayOption();
|
|
||||||
if (quickPlayOption != null) {
|
|
||||||
builder.setQuickPlayOption(quickPlayOption);
|
|
||||||
}
|
|
||||||
|
|
||||||
Path json = getModpackConfigurationFile();
|
|
||||||
if (Files.exists(json)) {
|
|
||||||
try {
|
|
||||||
String jsonText = Files.readString(json);
|
|
||||||
ModpackConfiguration<?> modpackConfiguration = JsonUtils.GSON.fromJson(jsonText, ModpackConfiguration.class);
|
|
||||||
ModpackProvider provider = ModpackHelper.getProviderByType(modpackConfiguration.getType());
|
|
||||||
if (provider != null) provider.injectLaunchOptions(jsonText, builder);
|
|
||||||
} catch (IOException | JsonParseException e) {
|
|
||||||
LOG.warning("Failed to parse modpack configuration file " + json, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (autoMemory && builder.getJavaArguments().stream().anyMatch(it -> it.startsWith("-Xmx")))
|
|
||||||
builder.setMaxMemory(null);
|
|
||||||
|
|
||||||
return builder;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ProxyOption getProxyOption() {
|
|
||||||
return switch (settings().proxyTypeProperty().get()) {
|
|
||||||
case SYSTEM -> ProxyOption.Default.INSTANCE;
|
|
||||||
case DIRECT -> ProxyOption.Direct.INSTANCE;
|
|
||||||
case HTTP, SOCKS -> {
|
|
||||||
String proxyHost = settings().proxyHostProperty().get();
|
|
||||||
int proxyPort = settings().proxyPortProperty().get();
|
|
||||||
|
|
||||||
if (StringUtils.isBlank(proxyHost) || proxyPort < 0 || proxyPort > 0xFFFF) {
|
|
||||||
yield ProxyOption.Default.INSTANCE;
|
|
||||||
}
|
|
||||||
|
|
||||||
String proxyUser = settings().proxyUserProperty().get();
|
|
||||||
String proxyPass = settings().proxyPasswordProperty().get();
|
|
||||||
|
|
||||||
if (StringUtils.isBlank(proxyUser)) {
|
|
||||||
proxyUser = null;
|
|
||||||
proxyPass = null;
|
|
||||||
} else if (proxyPass == null) {
|
|
||||||
proxyPass = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (settings().proxyTypeProperty().get() == ProxyType.HTTP) {
|
|
||||||
yield new ProxyOption.Http(proxyHost, proxyPort, proxyUser, proxyPass);
|
|
||||||
} else {
|
|
||||||
yield new ProxyOption.Socks(proxyHost, proxyPort, proxyUser, proxyPass);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Loads a new-format instance game settings file.
|
|
||||||
private static LoadResult loadGameSettingsFile(Path file) {
|
|
||||||
if (!Files.exists(file)) {
|
|
||||||
return new LoadResult(null, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
JsonObject jsonObject = JsonUtils.fromJsonFile(LauncherSettings.SETTINGS_GSON, file, JsonObject.class);
|
|
||||||
if (jsonObject == null) {
|
|
||||||
LOG.warning("Instance game settings are empty: " + file);
|
|
||||||
GameSettings.Instance fallback = new GameSettings.Instance();
|
|
||||||
return new LoadResult(fallback, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
JsonSchema.CompatibilityResult schemaResult =
|
|
||||||
JsonSchema.check(jsonObject, GameSettings.Instance.CURRENT_SCHEMA);
|
|
||||||
switch (schemaResult.status()) {
|
|
||||||
case MISSING -> LOG.warning("Missing schema in instance game settings: " + file);
|
|
||||||
case INVALID -> LOG.warning("Invalid schema in instance game settings: %s, Actual: %s".formatted(file, schemaResult.invalidValue()));
|
|
||||||
case UNPARSEABLE -> LOG.warning("Unparseable schema in instance game settings: %s, Actual: %s".formatted(file, schemaResult.actual()));
|
|
||||||
case UNEXPECTED_ID -> LOG.warning("Unexpected instance game settings schema. Expected: %s, Actual: %s".formatted(GameSettings.Instance.CURRENT_SCHEMA, schemaResult.actual()));
|
|
||||||
case UNSUPPORTED_MAJOR, READ_ONLY_PRESERVE_SCHEMA -> LOG.warning("Unsupported instance game settings schema. Expected: %s, Actual: %s".formatted(GameSettings.Instance.CURRENT_SCHEMA, schemaResult.actual()));
|
|
||||||
case READ_WRITE, READ_WRITE_PRESERVE_SCHEMA -> {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!schemaResult.readable()) {
|
|
||||||
GameSettings.Instance fallback = new GameSettings.Instance();
|
|
||||||
fallback.setSavable(false);
|
|
||||||
return new LoadResult(fallback, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
GameSettings.@Nullable Instance setting =
|
|
||||||
LauncherSettings.SETTINGS_GSON.fromJson(jsonObject, GameSettings.Instance.class);
|
|
||||||
if (setting == null) {
|
|
||||||
LOG.warning("Instance game settings deserialized to null: " + file);
|
|
||||||
GameSettings.Instance fallback = new GameSettings.Instance();
|
|
||||||
fallback.setBackupOnNextSave(true);
|
|
||||||
return new LoadResult(fallback, true);
|
|
||||||
}
|
|
||||||
if (!schemaResult.preserveSchema() && !GameSettings.Instance.CURRENT_SCHEMA.equals(setting.getSchema())) {
|
|
||||||
setting.setSchema(GameSettings.Instance.CURRENT_SCHEMA);
|
|
||||||
}
|
|
||||||
return new LoadResult(setting, schemaResult.allowSave());
|
|
||||||
} catch (JsonParseException ex) {
|
|
||||||
LOG.warning("Failed to parse game setting " + file, ex);
|
|
||||||
GameSettings.Instance fallback = new GameSettings.Instance();
|
|
||||||
fallback.setBackupOnNextSave(true);
|
|
||||||
return new LoadResult(fallback, true);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
LOG.warning("Failed to load game setting " + file, ex);
|
|
||||||
return new LoadResult(null, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Keeps old local custom running directories effective under the new source-selection model.
|
|
||||||
private static void normalizeRunningDirectoryOverride(GameSettings.Instance setting) {
|
|
||||||
if (StringUtils.isNotBlank(setting.runningDirectoryProperty().getValue())) {
|
|
||||||
setting.getOverrideProperties().add(GameSettings.PROPERTY_RUNNING_DIRECTORY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Result of loading an instance-specific game settings file.
|
|
||||||
///
|
|
||||||
/// @param setting the loaded instance settings, or `null` when unavailable
|
|
||||||
/// @param allowSave whether the file may be overwritten
|
|
||||||
private record LoadResult(@Nullable GameSettings.Instance setting, boolean allowSave) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Optional reference to an HMCL game instance bound to a repository.
|
|
||||||
///
|
|
||||||
/// Replaces the former `(repository, instanceId)` pair for UI and service context that may or
|
|
||||||
/// may not have a selected instance. When present, [#instance()] is a snapshot member and may
|
|
||||||
/// become stale after the repository publishes a new snapshot; call [#refreshed()] to re-resolve
|
|
||||||
/// from the current snapshot while preserving repository context.
|
|
||||||
@NotNullByDefault
|
|
||||||
public static final class Optional {
|
|
||||||
private final HMCLGameRepository repository;
|
|
||||||
private final @Nullable HMCLGameInstance instance;
|
|
||||||
|
|
||||||
/// Creates an empty optional bound only to a repository.
|
|
||||||
///
|
|
||||||
/// @param repository the repository
|
|
||||||
public Optional(HMCLGameRepository repository) {
|
|
||||||
this.repository = Objects.requireNonNull(repository);
|
|
||||||
this.instance = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates an optional that holds the given instance.
|
|
||||||
///
|
|
||||||
/// @param instance the instance
|
|
||||||
public Optional(HMCLGameInstance instance) {
|
|
||||||
this.repository = instance.getRepository();
|
|
||||||
this.instance = instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates an optional by resolving `instanceId` from the repository's current snapshot.
|
|
||||||
///
|
|
||||||
/// @param repository the repository
|
|
||||||
/// @param instanceId the instance id, or `null` for an empty optional
|
|
||||||
/// @return an optional that is empty when `instanceId` is null or not registered
|
|
||||||
public static Optional of(HMCLGameRepository repository, @Nullable GameInstanceID instanceId) {
|
|
||||||
if (instanceId == null) {
|
|
||||||
return new Optional(repository);
|
|
||||||
}
|
|
||||||
HMCLGameInstance instance = repository.findInstance(instanceId);
|
|
||||||
return instance != null ? new Optional(instance) : new Optional(repository);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates an optional that holds the given instance.
|
|
||||||
///
|
|
||||||
/// @param instance the instance
|
|
||||||
/// @return the optional
|
|
||||||
public static Optional of(HMCLGameInstance instance) {
|
|
||||||
return new Optional(instance);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates an empty optional bound only to a repository.
|
|
||||||
///
|
|
||||||
/// @param repository the repository
|
|
||||||
/// @return the empty optional
|
|
||||||
public static Optional empty(HMCLGameRepository repository) {
|
|
||||||
return new Optional(repository);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the repository associated with this optional.
|
|
||||||
///
|
|
||||||
/// @return the repository
|
|
||||||
public HMCLGameRepository repository() {
|
|
||||||
return repository;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the held instance, if any.
|
|
||||||
///
|
|
||||||
/// @return the instance, or `null` when empty
|
|
||||||
@Contract(pure = true)
|
|
||||||
public @Nullable HMCLGameInstance instance() {
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the held instance id, if any.
|
|
||||||
///
|
|
||||||
/// @return the instance id, or `null` when empty
|
|
||||||
@Contract(pure = true)
|
|
||||||
public @Nullable GameInstanceID instanceId() {
|
|
||||||
return instance != null ? instance.getId() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns whether an instance is present.
|
|
||||||
///
|
|
||||||
/// @return whether [#instance()] is non-null
|
|
||||||
public boolean isPresent() {
|
|
||||||
return instance != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns whether no instance is present.
|
|
||||||
///
|
|
||||||
/// @return whether [#instance()] is null
|
|
||||||
public boolean isEmpty() {
|
|
||||||
return instance == null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Re-resolves the held instance id from the repository's current snapshot.
|
|
||||||
///
|
|
||||||
/// @return this optional when empty; otherwise a fresh optional for the same id
|
|
||||||
public Optional refreshed() {
|
|
||||||
if (instance == null) {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
return of(repository, instance.getId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +1,37 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
|
/*
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
* Hello Minecraft! Launcher
|
||||||
|
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
package org.jackhuang.hmcl.game;
|
package org.jackhuang.hmcl.game;
|
||||||
|
|
||||||
import org.jackhuang.hmcl.Metadata;
|
import org.jackhuang.hmcl.Metadata;
|
||||||
import org.jackhuang.hmcl.auth.AuthInfo;
|
import org.jackhuang.hmcl.auth.AuthInfo;
|
||||||
import org.jackhuang.hmcl.launch.DefaultLauncher;
|
import org.jackhuang.hmcl.launch.DefaultLauncher;
|
||||||
import org.jackhuang.hmcl.launch.ProcessListener;
|
import org.jackhuang.hmcl.launch.ProcessListener;
|
||||||
import org.jackhuang.hmcl.util.NativePatcher;
|
|
||||||
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
|
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
|
||||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
|
||||||
import org.jackhuang.hmcl.util.platform.CommandBuilder;
|
|
||||||
import org.jackhuang.hmcl.util.platform.ManagedProcess;
|
import org.jackhuang.hmcl.util.platform.ManagedProcess;
|
||||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
|
||||||
import java.nio.file.*;
|
import java.nio.file.*;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -27,27 +39,16 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
|||||||
*/
|
*/
|
||||||
public final class HMCLGameLauncher extends DefaultLauncher {
|
public final class HMCLGameLauncher extends DefaultLauncher {
|
||||||
|
|
||||||
/// Creates a launcher with daemon process monitors.
|
public HMCLGameLauncher(GameRepository repository, Version version, AuthInfo authInfo, LaunchOptions options) {
|
||||||
///
|
this(repository, version, authInfo, options, null);
|
||||||
/// @param instance the instance being launched
|
|
||||||
/// @param manifest the effective launch-time manifest
|
|
||||||
/// @param authInfo authentication information for the game process
|
|
||||||
/// @param options launch options
|
|
||||||
/// @param listener process listener, or `null` to inherit IO
|
|
||||||
public HMCLGameLauncher(GameInstance instance, GameInstanceManifest manifest, AuthInfo authInfo, LaunchOptions options, ProcessListener listener) {
|
|
||||||
this(instance, manifest, authInfo, options, listener, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a launcher for the given instance and launch plan.
|
public HMCLGameLauncher(GameRepository repository, Version version, AuthInfo authInfo, LaunchOptions options, ProcessListener listener) {
|
||||||
///
|
this(repository, version, authInfo, options, listener, true);
|
||||||
/// @param instance the instance being launched
|
}
|
||||||
/// @param manifest the effective launch-time manifest
|
|
||||||
/// @param authInfo authentication information for the game process
|
public HMCLGameLauncher(GameRepository repository, Version version, AuthInfo authInfo, LaunchOptions options, ProcessListener listener, boolean daemon) {
|
||||||
/// @param options launch options
|
super(repository, version, authInfo, options, listener, daemon);
|
||||||
/// @param listener process listener, or `null` to inherit IO
|
|
||||||
/// @param daemon whether monitors should be daemon threads
|
|
||||||
public HMCLGameLauncher(GameInstance instance, GameInstanceManifest manifest, AuthInfo authInfo, LaunchOptions options, ProcessListener listener, boolean daemon) {
|
|
||||||
super(instance, manifest, authInfo, options, listener, daemon);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -59,10 +60,10 @@ public final class HMCLGameLauncher extends DefaultLauncher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void generateOptionsTxt() {
|
private void generateOptionsTxt() {
|
||||||
if (options.isDisableAutoGameOptions())
|
if (config().isDisableAutoGameOptions())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Path runDir = instance.getRunDirectory();
|
Path runDir = repository.getRunDirectory(version.getId());
|
||||||
Path optionsFile = runDir.resolve("options.txt");
|
Path optionsFile = runDir.resolve("options.txt");
|
||||||
Path configFolder = runDir.resolve("config");
|
Path configFolder = runDir.resolve("config");
|
||||||
|
|
||||||
@@ -87,14 +88,8 @@ public final class HMCLGameLauncher extends DefaultLauncher {
|
|||||||
* 1.11 ~ 1.12 : zh_cn works fine, zh_CN will display Chinese but the language setting will incorrectly show English as selected
|
* 1.11 ~ 1.12 : zh_cn works fine, zh_CN will display Chinese but the language setting will incorrectly show English as selected
|
||||||
* 1.13+ : zh_cn works fine, zh_CN will automatically switch to English
|
* 1.13+ : zh_cn works fine, zh_CN will automatically switch to English
|
||||||
*/
|
*/
|
||||||
GameVersionNumber gameVersion = instance.getVersion();
|
GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(repository.getGameVersion(version));
|
||||||
if (gameVersion == GameVersionNumber.unknown() || gameVersion.compareTo("1.1") < 0)
|
if (gameVersion.compareTo("1.1") < 0)
|
||||||
return;
|
|
||||||
|
|
||||||
// These snapshots lack the external language resources needed for non-English locales.
|
|
||||||
// Selecting an unavailable language causes a NullPointerException during startup.
|
|
||||||
if (gameVersion instanceof GameVersionNumber.LegacySnapshot
|
|
||||||
&& gameVersion.compareTo("13w16a") >= 0 && gameVersion.compareTo("13w23b") <= 0)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
String lang = normalizedLanguageTag(locale, gameVersion);
|
String lang = normalizedLanguageTag(locale, gameVersion);
|
||||||
@@ -116,7 +111,6 @@ public final class HMCLGameLauncher extends DefaultLauncher {
|
|||||||
String region = locale.getCountry();
|
String region = locale.getCountry();
|
||||||
|
|
||||||
return switch (LocaleUtils.getRootLanguage(locale)) {
|
return switch (LocaleUtils.getRootLanguage(locale)) {
|
||||||
case "ar" -> "ar_SA";
|
|
||||||
case "es" -> "es_ES";
|
case "es" -> "es_ES";
|
||||||
case "ja" -> "ja_JP";
|
case "ja" -> "ja_JP";
|
||||||
case "ru" -> "ru_RU";
|
case "ru" -> "ru_RU";
|
||||||
@@ -155,60 +149,4 @@ public final class HMCLGameLauncher extends DefaultLauncher {
|
|||||||
generateOptionsTxt();
|
generateOptionsTxt();
|
||||||
super.makeLaunchScript(scriptFile);
|
super.makeLaunchScript(scriptFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void appendJvmArgs(CommandBuilder result) {
|
|
||||||
super.appendJvmArgs(result);
|
|
||||||
|
|
||||||
if (options.isAllowAutoAgent()
|
|
||||||
&& !options.isNoGeneratedJVMArgs()
|
|
||||||
&& !options.isNoGeneratedOptimizingJVMArgs()
|
|
||||||
&& NativePatcher.needPatchMemoryUtil(manifest, options.getJava().getParsedVersion())) {
|
|
||||||
LOG.info("Attempting to patch game with lwjgl-unsafe-agent");
|
|
||||||
try {
|
|
||||||
result.add("-javaagent:" + extractLwjglUnsafeAgent());
|
|
||||||
} catch (Exception e) {
|
|
||||||
LOG.warning("Failed to extract lwjgl-unsafe-agent", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Path extractLwjglUnsafeAgent() throws IOException {
|
|
||||||
String agentVersion = JarUtils.getAttribute("hmcl.lwjgl-unsafe-agent.version", null);
|
|
||||||
if (agentVersion == null) {
|
|
||||||
throw new IOException("Missing hmcl.lwjgl-unsafe-agent.version attribute");
|
|
||||||
}
|
|
||||||
|
|
||||||
Library library = new Library(new Artifact("org.glavo", "lwjgl-unsafe-agent", agentVersion));
|
|
||||||
String fileName = library.artifact().getFileName();
|
|
||||||
|
|
||||||
Path agentPath = instance.getLayout().getLibraryFile(instance.getId(), library).toAbsolutePath().normalize();
|
|
||||||
if (agentPath.toString().contains("=")) {
|
|
||||||
throw new IOException("Invalid library path: " + agentPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] bytes;
|
|
||||||
try (InputStream input = DefaultLauncher.class.getResourceAsStream("/assets/" + fileName)) {
|
|
||||||
if (input == null) {
|
|
||||||
throw new IOException("/assets/" + fileName + " not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
bytes = input.readAllBytes();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Files.isRegularFile(agentPath)) {
|
|
||||||
try {
|
|
||||||
if (Files.size(agentPath) == bytes.length) {
|
|
||||||
return agentPath;
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
LOG.warning("Failed to check size of " + agentPath, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Files.createDirectories(agentPath.getParent());
|
|
||||||
FileUtils.saveSafely(agentPath, output -> output.write(bytes));
|
|
||||||
return agentPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -1,48 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
|
||||||
package org.jackhuang.hmcl.game;
|
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNullByDefault;
|
|
||||||
|
|
||||||
import java.nio.file.Path;
|
|
||||||
|
|
||||||
@NotNullByDefault
|
|
||||||
public final class HMCLGameRepositoryLayout extends DefaultGameRepositoryLayout {
|
|
||||||
/// Directory under the instance root that stores HMCL-managed instance metadata.
|
|
||||||
private static final String INSTANCE_METADATA_DIRECTORY = ".hmcl";
|
|
||||||
|
|
||||||
/// Directory under the instance metadata directory that stores instance configuration files.
|
|
||||||
private static final String INSTANCE_CONFIG_DIRECTORY = "config";
|
|
||||||
|
|
||||||
/// Directory under the instance metadata directory that stores instance state files.
|
|
||||||
private static final String INSTANCE_STATE_DIRECTORY = "state";
|
|
||||||
|
|
||||||
/// Current file name for instance-specific game settings.
|
|
||||||
private static final String INSTANCE_GAME_SETTINGS_FILENAME = "instance-game-settings.json";
|
|
||||||
|
|
||||||
public HMCLGameRepositoryLayout(Path baseDirectory) {
|
|
||||||
super(baseDirectory);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the HMCL-managed metadata directory under the instance root.
|
|
||||||
///
|
|
||||||
/// This directory stores instance-scoped files owned by HMCL.
|
|
||||||
public Path getInstanceMetadataDirectory(GameInstanceID instanceId) {
|
|
||||||
return getInstanceRoot(instanceId).resolve(INSTANCE_METADATA_DIRECTORY);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the HMCL-managed configuration directory under the instance metadata directory.
|
|
||||||
public Path getInstanceConfigDirectory(GameInstanceID instanceId) {
|
|
||||||
return getInstanceMetadataDirectory(instanceId).resolve(INSTANCE_CONFIG_DIRECTORY);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the HMCL-managed state directory under the instance metadata directory.
|
|
||||||
public Path getInstanceStateDirectory(GameInstanceID instanceId) {
|
|
||||||
return getInstanceMetadataDirectory(instanceId).resolve(INSTANCE_STATE_DIRECTORY);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the current local game settings path under the instance configuration directory.
|
|
||||||
public Path getInstanceGameSettingsFile(GameInstanceID instanceId) {
|
|
||||||
return getInstanceConfigDirectory(instanceId).resolve(INSTANCE_GAME_SETTINGS_FILENAME);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
|
||||||
package org.jackhuang.hmcl.game;
|
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNullByDefault;
|
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
|
|
||||||
/// HMCL repository snapshot, parallel to [HMCLGameInstance] in the instance hierarchy.
|
|
||||||
@NotNullByDefault
|
|
||||||
public class HMCLGameRepositorySnapshot extends DefaultGameRepositorySnapshot {
|
|
||||||
/// Creates an empty unsealed HMCL snapshot.
|
|
||||||
///
|
|
||||||
/// @param repository the owning repository
|
|
||||||
/// @param layout the HMCL layout for this snapshot
|
|
||||||
public HMCLGameRepositorySnapshot(HMCLGameRepository repository, HMCLGameRepositoryLayout layout) {
|
|
||||||
super(repository, layout);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public HMCLGameRepository getRepository() {
|
|
||||||
return (HMCLGameRepository) super.getRepository();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public HMCLGameRepositoryLayout getLayout() {
|
|
||||||
return (HMCLGameRepositoryLayout) super.getLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected HMCLGameRepositorySnapshot newEmpty() {
|
|
||||||
return new HMCLGameRepositorySnapshot(getRepository(), getLayout());
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
@Override
|
|
||||||
public Collection<HMCLGameInstance> getInstances() {
|
|
||||||
return (Collection<HMCLGameInstance>) super.getInstances();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,121 +1,80 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
|
/*
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
* Hello Minecraft! Launcher
|
||||||
|
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
package org.jackhuang.hmcl.game;
|
package org.jackhuang.hmcl.game;
|
||||||
|
|
||||||
import com.google.gson.JsonParseException;
|
import com.google.gson.JsonParseException;
|
||||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||||
import org.jackhuang.hmcl.download.GameBuilder;
|
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||||
import org.jackhuang.hmcl.modpack.MinecraftInstanceTask;
|
import org.jackhuang.hmcl.mod.MinecraftInstanceTask;
|
||||||
import org.jackhuang.hmcl.modpack.Modpack;
|
import org.jackhuang.hmcl.mod.Modpack;
|
||||||
import org.jackhuang.hmcl.modpack.ModpackConfiguration;
|
import org.jackhuang.hmcl.mod.ModpackConfiguration;
|
||||||
import org.jackhuang.hmcl.modpack.ModpackInstallTask;
|
import org.jackhuang.hmcl.mod.ModpackInstallTask;
|
||||||
|
import org.jackhuang.hmcl.setting.Profile;
|
||||||
import org.jackhuang.hmcl.task.Task;
|
import org.jackhuang.hmcl.task.Task;
|
||||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||||
import org.jetbrains.annotations.NotNullByDefault;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.regex.Matcher;
|
|
||||||
|
|
||||||
/// Installs or updates an HMCL modpack by rebuilding its recognized game components.
|
|
||||||
@NotNullByDefault
|
|
||||||
public final class HMCLModpackInstallTask extends Task<Void> {
|
public final class HMCLModpackInstallTask extends Task<Void> {
|
||||||
private final Path zipFile;
|
private final Path zipFile;
|
||||||
private final GameInstanceID instanceId;
|
private final String name;
|
||||||
|
|
||||||
/// Existing instance selecting update mode, or `null` for a new installation.
|
|
||||||
private final @Nullable DefaultGameInstance updateTarget;
|
|
||||||
|
|
||||||
private final HMCLGameRepository repository;
|
private final HMCLGameRepository repository;
|
||||||
private final DefaultDependencyManager dependency;
|
private final DefaultDependencyManager dependency;
|
||||||
private final Modpack modpack;
|
private final Modpack modpack;
|
||||||
private final List<Task<?>> dependencies = new ArrayList<>(1);
|
private final List<Task<?>> dependencies = new ArrayList<>(1);
|
||||||
private final List<Task<?>> dependents = new ArrayList<>(4);
|
private final List<Task<?>> dependents = new ArrayList<>(4);
|
||||||
|
|
||||||
/// Creates a task that installs a new HMCL modpack instance.
|
public HMCLModpackInstallTask(Profile profile, Path zipFile, Modpack modpack, String name) {
|
||||||
///
|
dependency = profile.getDependency();
|
||||||
/// @param repository the target HMCL game repository
|
repository = profile.getRepository();
|
||||||
/// @param zipFile the HMCL modpack archive
|
|
||||||
/// @param modpack the parsed modpack metadata
|
|
||||||
/// @param instanceId the id of the new instance
|
|
||||||
public HMCLModpackInstallTask(
|
|
||||||
HMCLGameRepository repository,
|
|
||||||
Path zipFile,
|
|
||||||
Modpack modpack,
|
|
||||||
GameInstanceID instanceId) {
|
|
||||||
this(repository, zipFile, modpack, instanceId, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a task that updates an existing HMCL modpack instance.
|
|
||||||
///
|
|
||||||
/// @param repository the target HMCL game repository
|
|
||||||
/// @param zipFile the HMCL modpack archive
|
|
||||||
/// @param modpack the parsed modpack metadata
|
|
||||||
/// @param instance the existing instance to update
|
|
||||||
/// @throws IllegalArgumentException if `instance` belongs to another repository, has no
|
|
||||||
/// modpack configuration, or records another provider type
|
|
||||||
public HMCLModpackInstallTask(
|
|
||||||
HMCLGameRepository repository,
|
|
||||||
Path zipFile,
|
|
||||||
Modpack modpack,
|
|
||||||
DefaultGameInstance instance) {
|
|
||||||
this(repository, zipFile, modpack, instance.getId(), instance);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates an HMCL modpack task in the mode selected by `updateTarget`.
|
|
||||||
///
|
|
||||||
/// @param repository the target HMCL game repository
|
|
||||||
/// @param zipFile the HMCL modpack archive
|
|
||||||
/// @param modpack the parsed modpack metadata
|
|
||||||
/// @param instanceId the target instance id
|
|
||||||
/// @param updateTarget the existing instance selecting update mode, or `null` for install
|
|
||||||
/// @throws IllegalArgumentException if an update target belongs to another repository or has
|
|
||||||
/// no compatible configuration
|
|
||||||
private HMCLModpackInstallTask(
|
|
||||||
HMCLGameRepository repository,
|
|
||||||
Path zipFile,
|
|
||||||
Modpack modpack,
|
|
||||||
GameInstanceID instanceId,
|
|
||||||
@Nullable DefaultGameInstance updateTarget) {
|
|
||||||
this.repository = repository;
|
|
||||||
this.dependency = repository.getDependency();
|
|
||||||
this.zipFile = zipFile;
|
this.zipFile = zipFile;
|
||||||
this.instanceId = instanceId;
|
this.name = name;
|
||||||
this.updateTarget = updateTarget;
|
|
||||||
this.modpack = modpack;
|
this.modpack = modpack;
|
||||||
if (this.updateTarget != null) {
|
|
||||||
dependency.validateGameInstance(this.updateTarget);
|
|
||||||
}
|
|
||||||
|
|
||||||
Path run = repository.getLayout().getInstanceRoot(this.instanceId);
|
Path run = repository.getRunDirectory(name);
|
||||||
Path json = repository.getLayout().getModpackConfigurationFile(this.instanceId);
|
Path json = repository.getModpackConfiguration(name);
|
||||||
if (this.updateTarget != null && Files.notExists(json))
|
if (repository.hasVersion(name) && Files.notExists(json))
|
||||||
throw new IllegalArgumentException("Instance " + instanceId + " is not a HMCL modpack. Cannot update this instance.");
|
throw new IllegalArgumentException("Version " + name + " already exists");
|
||||||
|
|
||||||
@Nullable ModpackConfiguration<Modpack> config = null;
|
dependents.add(dependency.gameBuilder().name(name).gameVersion(modpack.getGameVersion()).buildAsync());
|
||||||
|
|
||||||
|
onDone().register(event -> {
|
||||||
|
if (event.isFailed()) repository.removeVersionFromDisk(name);
|
||||||
|
});
|
||||||
|
|
||||||
|
ModpackConfiguration<Modpack> config = null;
|
||||||
try {
|
try {
|
||||||
if (this.updateTarget != null && Files.exists(json)) {
|
if (Files.exists(json)) {
|
||||||
config = JsonUtils.fromJsonFile(json, ModpackConfiguration.typeOf(Modpack.class));
|
config = JsonUtils.fromJsonFile(json, ModpackConfiguration.typeOf(Modpack.class));
|
||||||
|
|
||||||
if (config == null || !HMCLModpackProvider.INSTANCE.getName().equals(config.getType()))
|
if (!HMCLModpackProvider.INSTANCE.getName().equals(config.getType()))
|
||||||
throw new IllegalArgumentException("Instance " + instanceId + " is not a HMCL modpack. Cannot update this instance.");
|
throw new IllegalArgumentException("Version " + name + " is not a HMCL modpack. Cannot update this version.");
|
||||||
}
|
}
|
||||||
} catch (JsonParseException | IOException ignore) {
|
} catch (JsonParseException | IOException ignore) {
|
||||||
}
|
}
|
||||||
|
dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), Collections.singletonList("/minecraft"), it -> !"pack.json".equals(it), config));
|
||||||
onDone().register(event -> {
|
dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), Collections.singletonList("/minecraft"), modpack, HMCLModpackProvider.INSTANCE, modpack.getName(), modpack.getVersion(), repository.getModpackConfiguration(name)).withStage("hmcl.modpack"));
|
||||||
if (this.updateTarget == null && event.isFailed()) {
|
|
||||||
repository.removeInstanceFromDisk(this.instanceId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
dependents.add(new ModpackInstallTask<>(zipFile, run, List.of("/minecraft"), it -> !"pack.json".equals(it), config));
|
|
||||||
dependents.add(new MinecraftInstanceTask<>(zipFile, List.of("/minecraft"), modpack, HMCLModpackProvider.INSTANCE, modpack.getName(), modpack.getVersion(), repository.getLayout().getModpackConfigurationFile(this.instanceId)).withStage("hmcl.modpack"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -128,43 +87,20 @@ public final class HMCLModpackInstallTask extends Task<Void> {
|
|||||||
return dependents;
|
return dependents;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reads the legacy pack manifest and schedules one builder for all recognized components.
|
|
||||||
///
|
|
||||||
/// The pack manifest is used only for component detection. The builder creates a new
|
|
||||||
/// patch-structured manifest instead of attempting to mutate the legacy flattened manifest.
|
|
||||||
@Override
|
@Override
|
||||||
public void execute() throws Exception {
|
public void execute() throws Exception {
|
||||||
String json = CompressingUtils.readTextZipEntry(zipFile, "minecraft/pack.json");
|
String json = CompressingUtils.readTextZipEntry(zipFile, "minecraft/pack.json");
|
||||||
GameInstanceManifest originalManifest = JsonUtils.fromNonNullJson(json, GameInstanceManifest.class)
|
Version originalVersion = JsonUtils.GSON.fromJson(json, Version.class).setId(name).setJar(null);
|
||||||
.withId(instanceId)
|
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(originalVersion, null);
|
||||||
.withJar(null);
|
Task<Version> libraryTask = Task.supplyAsync(() -> originalVersion);
|
||||||
GameComponentAnalyzer analyzer = GameComponentAnalyzer.analyze(originalManifest, null);
|
// reinstall libraries
|
||||||
|
// libraries of Forge and OptiFine should be obtained by installation.
|
||||||
try (GameBuilder builder = updateTarget == null
|
for (LibraryAnalyzer.LibraryMark mark : analyzer) {
|
||||||
? dependency.newGameBuilder(instanceId)
|
if (LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId().equals(mark.getLibraryId()))
|
||||||
: dependency.newGameBuilder(updateTarget)) {
|
continue;
|
||||||
builder.enableIsolation();
|
libraryTask = libraryTask.thenComposeAsync(version -> dependency.installLibraryAsync(modpack.getGameVersion(), version, mark.getLibraryId(), mark.getLibraryVersion()));
|
||||||
|
|
||||||
String gameVersion = modpack.getGameVersion();
|
|
||||||
builder.component(GameComponentType.GAME, gameVersion);
|
|
||||||
for (GameComponentAnalyzer.Mark mark : analyzer) {
|
|
||||||
if (mark.componentType() == GameComponentType.GAME) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable String componentVersion = mark.version();
|
|
||||||
if (componentVersion != null) {
|
|
||||||
if (mark.componentType() == GameComponentType.OPTIFINE) {
|
|
||||||
Matcher matcher = GameComponentAnalyzer.OPTIFINE_VERSION_PATTERN.matcher(componentVersion);
|
|
||||||
if (matcher.matches()) {
|
|
||||||
componentVersion = matcher.group("optifine");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
builder.component(mark.componentType(), componentVersion);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dependencies.add(builder.buildAsync());
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
dependencies.add(libraryTask.thenComposeAsync(repository::saveAsync));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,24 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
|
/*
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
* Hello Minecraft! Launcher
|
||||||
|
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
package org.jackhuang.hmcl.game;
|
package org.jackhuang.hmcl.game;
|
||||||
|
|
||||||
import org.jackhuang.hmcl.modpack.ModpackManifest;
|
import org.jackhuang.hmcl.mod.ModpackManifest;
|
||||||
import org.jackhuang.hmcl.modpack.ModpackProvider;
|
import org.jackhuang.hmcl.mod.ModpackProvider;
|
||||||
|
|
||||||
public final class HMCLModpackManifest implements ModpackManifest {
|
public final class HMCLModpackManifest implements ModpackManifest {
|
||||||
public static final HMCLModpackManifest INSTANCE = new HMCLModpackManifest();
|
public static final HMCLModpackManifest INSTANCE = new HMCLModpackManifest();
|
||||||
|
|||||||
@@ -1,23 +1,38 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright (C) 2022 The HMCL Authors
|
/*
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
* Hello Minecraft! Launcher
|
||||||
|
* Copyright (C) 2022 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
package org.jackhuang.hmcl.game;
|
package org.jackhuang.hmcl.game;
|
||||||
|
|
||||||
import com.google.gson.JsonParseException;
|
import com.google.gson.JsonParseException;
|
||||||
import kala.compress.archivers.zip.ZipArchiveReader;
|
import kala.compress.archivers.zip.ZipArchiveReader;
|
||||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||||
import org.jackhuang.hmcl.modpack.MismatchedModpackTypeException;
|
import org.jackhuang.hmcl.mod.MismatchedModpackTypeException;
|
||||||
import org.jackhuang.hmcl.modpack.Modpack;
|
import org.jackhuang.hmcl.mod.Modpack;
|
||||||
import org.jackhuang.hmcl.modpack.ModpackProvider;
|
import org.jackhuang.hmcl.mod.ModpackProvider;
|
||||||
import org.jackhuang.hmcl.modpack.ModpackUpdateTask;
|
import org.jackhuang.hmcl.mod.ModpackUpdateTask;
|
||||||
|
import org.jackhuang.hmcl.setting.Profile;
|
||||||
import org.jackhuang.hmcl.task.Task;
|
import org.jackhuang.hmcl.task.Task;
|
||||||
import org.jackhuang.hmcl.util.StringUtils;
|
import org.jackhuang.hmcl.util.StringUtils;
|
||||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
public final class HMCLModpackProvider implements ModpackProvider {
|
public final class HMCLModpackProvider implements ModpackProvider {
|
||||||
public static final HMCLModpackProvider INSTANCE = new HMCLModpackProvider();
|
public static final HMCLModpackProvider INSTANCE = new HMCLModpackProvider();
|
||||||
@@ -28,17 +43,12 @@ public final class HMCLModpackProvider implements ModpackProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @Nullable Task<?> createCompletionTask(DefaultDependencyManager dependencyManager, DefaultGameInstance instance) {
|
public Task<?> createCompletionTask(DefaultDependencyManager dependencyManager, String version) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Task<?> createUpdateTask(
|
public Task<?> createUpdateTask(DefaultDependencyManager dependencyManager, String name, Path zipFile, Modpack modpack) throws MismatchedModpackTypeException {
|
||||||
DefaultDependencyManager dependencyManager,
|
|
||||||
DefaultGameInstance instance,
|
|
||||||
Path zipFile,
|
|
||||||
Modpack modpack,
|
|
||||||
@Nullable Set<String> excludedFiles) throws MismatchedModpackTypeException {
|
|
||||||
if (!(modpack.getManifest() instanceof HMCLModpackManifest))
|
if (!(modpack.getManifest() instanceof HMCLModpackManifest))
|
||||||
throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName());
|
throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName());
|
||||||
|
|
||||||
@@ -46,34 +56,31 @@ public final class HMCLModpackProvider implements ModpackProvider {
|
|||||||
throw new IllegalArgumentException("HMCLModpackProvider requires HMCLGameRepository");
|
throw new IllegalArgumentException("HMCLModpackProvider requires HMCLGameRepository");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ModpackUpdateTask(instance, new HMCLModpackInstallTask(repository, zipFile, modpack, instance));
|
Profile profile = repository.getProfile();
|
||||||
|
|
||||||
|
return new ModpackUpdateTask(dependencyManager.getGameRepository(), name, new HMCLModpackInstallTask(profile, zipFile, modpack, name));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Modpack readManifest(ZipArchiveReader file, Path path) throws IOException, JsonParseException {
|
public Modpack readManifest(ZipArchiveReader file, Path path, Charset encoding) throws IOException, JsonParseException {
|
||||||
String manifestJson = CompressingUtils.readTextZipEntry(file, "modpack.json");
|
String manifestJson = CompressingUtils.readTextZipEntry(file, "modpack.json");
|
||||||
Modpack manifest = JsonUtils.fromNonNullJson(manifestJson, HMCLModpack.class);
|
Modpack manifest = JsonUtils.fromNonNullJson(manifestJson, HMCLModpack.class).setEncoding(encoding);
|
||||||
String gameJson = CompressingUtils.readTextZipEntry(file, "minecraft/pack.json");
|
String gameJson = CompressingUtils.readTextZipEntry(file, "minecraft/pack.json");
|
||||||
GameInstanceManifest game = JsonUtils.fromNonNullJson(gameJson, GameInstanceManifest.class);
|
Version game = JsonUtils.fromNonNullJson(gameJson, Version.class);
|
||||||
if (game.jar() == null)
|
if (game.getJar() == null)
|
||||||
if (StringUtils.isBlank(manifest.getVersion()))
|
if (StringUtils.isBlank(manifest.getVersion()))
|
||||||
throw new JsonParseException("Cannot recognize the game version of modpack " + file + ".");
|
throw new JsonParseException("Cannot recognize the game version of modpack " + file + ".");
|
||||||
else
|
else
|
||||||
manifest.setManifest(HMCLModpackManifest.INSTANCE);
|
manifest.setManifest(HMCLModpackManifest.INSTANCE);
|
||||||
else
|
else
|
||||||
manifest.setManifest(HMCLModpackManifest.INSTANCE).setGameVersion(game.jar().id());
|
manifest.setManifest(HMCLModpackManifest.INSTANCE).setGameVersion(game.getJar());
|
||||||
return manifest;
|
return manifest;
|
||||||
}
|
}
|
||||||
|
|
||||||
private final static class HMCLModpack extends Modpack {
|
private final static class HMCLModpack extends Modpack {
|
||||||
@Override
|
@Override
|
||||||
public Task<?> getInstallTask(
|
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, String name) {
|
||||||
DefaultDependencyManager dependencyManager,
|
return new HMCLModpackInstallTask(((HMCLGameRepository) dependencyManager.getGameRepository()).getProfile(), zipFile, this, name);
|
||||||
Path zipFile,
|
|
||||||
GameInstanceID instanceId,
|
|
||||||
String iconUrl,
|
|
||||||
@Nullable Set<String> excludedFiles) {
|
|
||||||
return new HMCLModpackInstallTask((HMCLGameRepository) dependencyManager.getGameRepository(), zipFile, this, instanceId);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,20 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors
|
/*
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
* Hello Minecraft! Launcher
|
||||||
|
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
package org.jackhuang.hmcl.game;
|
package org.jackhuang.hmcl.game;
|
||||||
|
|
||||||
import com.jfoenix.controls.JFXButton;
|
import com.jfoenix.controls.JFXButton;
|
||||||
@@ -7,24 +22,23 @@ import javafx.stage.Stage;
|
|||||||
import org.jackhuang.hmcl.Launcher;
|
import org.jackhuang.hmcl.Launcher;
|
||||||
import org.jackhuang.hmcl.auth.*;
|
import org.jackhuang.hmcl.auth.*;
|
||||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloadException;
|
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloadException;
|
||||||
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
|
|
||||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||||
|
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||||
|
import org.jackhuang.hmcl.download.MaintainTask;
|
||||||
import org.jackhuang.hmcl.download.game.*;
|
import org.jackhuang.hmcl.download.game.*;
|
||||||
import org.jackhuang.hmcl.java.JavaManager;
|
import org.jackhuang.hmcl.java.JavaManager;
|
||||||
import org.jackhuang.hmcl.java.JavaRuntime;
|
import org.jackhuang.hmcl.java.JavaRuntime;
|
||||||
import org.jackhuang.hmcl.launch.*;
|
import org.jackhuang.hmcl.launch.*;
|
||||||
import org.jackhuang.hmcl.modpack.ModpackCompletionException;
|
import org.jackhuang.hmcl.mod.ModpackCompletionException;
|
||||||
import org.jackhuang.hmcl.modpack.ModpackConfiguration;
|
import org.jackhuang.hmcl.mod.ModpackConfiguration;
|
||||||
import org.jackhuang.hmcl.modpack.ModpackProvider;
|
import org.jackhuang.hmcl.mod.ModpackProvider;
|
||||||
import org.jackhuang.hmcl.setting.GameSettings;
|
import org.jackhuang.hmcl.setting.*;
|
||||||
import org.jackhuang.hmcl.setting.JavaVersionType;
|
|
||||||
import org.jackhuang.hmcl.setting.LauncherVisibility;
|
|
||||||
import org.jackhuang.hmcl.task.*;
|
import org.jackhuang.hmcl.task.*;
|
||||||
import org.jackhuang.hmcl.ui.*;
|
import org.jackhuang.hmcl.ui.*;
|
||||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
|
|
||||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||||
|
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
|
||||||
import org.jackhuang.hmcl.ui.construct.PromptDialogPane;
|
import org.jackhuang.hmcl.ui.construct.PromptDialogPane;
|
||||||
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
|
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
|
||||||
import org.jackhuang.hmcl.util.*;
|
import org.jackhuang.hmcl.util.*;
|
||||||
@@ -32,63 +46,50 @@ import org.jackhuang.hmcl.util.i18n.I18n;
|
|||||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||||
import org.jackhuang.hmcl.util.io.ResponseCodeException;
|
import org.jackhuang.hmcl.util.io.ResponseCodeException;
|
||||||
import org.jackhuang.hmcl.util.platform.*;
|
import org.jackhuang.hmcl.util.platform.*;
|
||||||
import org.jackhuang.hmcl.util.platform.windows.WinReg;
|
|
||||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.ref.WeakReference;
|
|
||||||
import java.net.SocketTimeoutException;
|
import java.net.SocketTimeoutException;
|
||||||
|
import java.net.URI;
|
||||||
import java.nio.file.AccessDeniedException;
|
import java.nio.file.AccessDeniedException;
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.*;
|
import java.util.concurrent.*;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
|
|
||||||
import static javafx.application.Platform.runLater;
|
import static javafx.application.Platform.runLater;
|
||||||
import static javafx.application.Platform.setImplicitExit;
|
import static javafx.application.Platform.setImplicitExit;
|
||||||
import static org.jackhuang.hmcl.setting.SettingsManager.state;
|
|
||||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||||
import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
|
import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
|
||||||
import static org.jackhuang.hmcl.util.Lang.resolveException;
|
import static org.jackhuang.hmcl.util.Lang.resolveException;
|
||||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||||
import static org.jackhuang.hmcl.util.logging.Logger.TIME_FORMATTER;
|
|
||||||
import static org.jackhuang.hmcl.util.platform.Platform.SYSTEM_PLATFORM;
|
import static org.jackhuang.hmcl.util.platform.Platform.SYSTEM_PLATFORM;
|
||||||
|
import static org.jackhuang.hmcl.util.platform.Platform.isCompatibleWithX86Java;
|
||||||
|
|
||||||
public final class LauncherHelper {
|
public final class LauncherHelper {
|
||||||
|
|
||||||
private static final String LWJGL_3_4_1_TIP = "lwjgl3.4.1-ffm";
|
private final Profile profile;
|
||||||
|
|
||||||
private final HMCLGameInstance gameInstance;
|
|
||||||
private Account account;
|
private Account account;
|
||||||
|
private final String selectedVersion;
|
||||||
private Path scriptFile;
|
private Path scriptFile;
|
||||||
private final GameSettings.Effective setting;
|
private final VersionSetting setting;
|
||||||
private LauncherVisibility launcherVisibility;
|
private LauncherVisibility launcherVisibility;
|
||||||
private boolean showLogs;
|
private boolean showLogs;
|
||||||
private QuickPlayOption quickPlayOption;
|
|
||||||
private boolean disableOfflineSkin = false;
|
|
||||||
|
|
||||||
public LauncherHelper(HMCLGameInstance gameInstance, Account account) {
|
public LauncherHelper(Profile profile, Account account, String selectedVersion) {
|
||||||
this.gameInstance = Objects.requireNonNull(gameInstance);
|
this.profile = Objects.requireNonNull(profile);
|
||||||
this.account = Objects.requireNonNull(account);
|
this.account = Objects.requireNonNull(account);
|
||||||
this.setting = gameInstance.getEffectiveSettings();
|
this.selectedVersion = Objects.requireNonNull(selectedVersion);
|
||||||
this.launcherVisibility = setting.getInheritable(GameSettings::launcherVisibilityProperty);
|
this.setting = profile.getVersionSetting(selectedVersion);
|
||||||
this.showLogs = setting.getInheritable(GameSettings::showLogsProperty);
|
this.launcherVisibility = setting.getLauncherVisibility();
|
||||||
this.launchingStepsPane.setTitle(i18n("instance.launch"));
|
this.showLogs = setting.isShowLogs();
|
||||||
}
|
this.launchingStepsPane.setTitle(i18n("version.launch"));
|
||||||
|
|
||||||
public HMCLGameInstance getGameInstance() {
|
|
||||||
return gameInstance;
|
|
||||||
}
|
|
||||||
|
|
||||||
private HMCLGameRepository repository() {
|
|
||||||
return gameInstance.getRepository();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private final TaskExecutorDialogPane launchingStepsPane = new TaskExecutorDialogPane(TaskCancellationAction.NORMAL);
|
private final TaskExecutorDialogPane launchingStepsPane = new TaskExecutorDialogPane(TaskCancellationAction.NORMAL);
|
||||||
@@ -110,18 +111,10 @@ public final class LauncherHelper {
|
|||||||
launcherVisibility = LauncherVisibility.KEEP;
|
launcherVisibility = LauncherVisibility.KEEP;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setQuickPlayOption(QuickPlayOption quickPlayOption) {
|
|
||||||
this.quickPlayOption = quickPlayOption;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDisableOfflineSkin() {
|
|
||||||
disableOfflineSkin = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void launch() {
|
public void launch() {
|
||||||
FXUtils.checkFxUserThread();
|
FXUtils.checkFxUserThread();
|
||||||
|
|
||||||
LOG.info("Launching game instance: " + gameInstance.getId());
|
LOG.info("Launching game version: " + selectedVersion);
|
||||||
|
|
||||||
Controllers.dialog(launchingStepsPane);
|
Controllers.dialog(launchingStepsPane);
|
||||||
launch0();
|
launch0();
|
||||||
@@ -132,170 +125,82 @@ public final class LauncherHelper {
|
|||||||
launch();
|
launch();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds and executes the launch pipeline for the captured game instance.
|
|
||||||
@SuppressWarnings("NonAtomicOperationOnVolatileField")
|
|
||||||
private void launch0() {
|
private void launch0() {
|
||||||
// https://github.com/HMCL-dev/HMCL/pull/4121
|
// https://github.com/HMCL-dev/HMCL/pull/4121
|
||||||
PROCESSES.removeIf(it -> it.get() == null);
|
PROCESSES.removeIf(it -> it.get() == null);
|
||||||
|
|
||||||
final var context = new Object() {
|
HMCLGameRepository repository = profile.getRepository();
|
||||||
volatile GameInstanceManifest launchManifest;
|
DefaultDependencyManager dependencyManager = profile.getDependency();
|
||||||
volatile JavaRuntime javaRuntime;
|
AtomicReference<Version> version = new AtomicReference<>(MaintainTask.maintain(repository, repository.getResolvedVersion(selectedVersion)));
|
||||||
volatile Path javaExecutable;
|
Optional<String> gameVersion = repository.getGameVersion(version.get());
|
||||||
};
|
boolean integrityCheck = repository.unmarkVersionLaunchedAbnormally(selectedVersion);
|
||||||
|
|
||||||
HMCLGameRepository repository = repository();
|
|
||||||
DefaultDependencyManager dependencyManager = repository.getDependency();
|
|
||||||
boolean integrityCheck = gameInstance.unmarkLaunchedAbnormally();
|
|
||||||
CountDownLatch launchingLatch = new CountDownLatch(1);
|
CountDownLatch launchingLatch = new CountDownLatch(1);
|
||||||
List<String> javaAgents = new ArrayList<>(0);
|
List<String> javaAgents = new ArrayList<>(0);
|
||||||
List<String> javaArguments = new ArrayList<>(0);
|
List<String> javaArguments = new ArrayList<>(0);
|
||||||
|
|
||||||
// Resolve already deduplicated libraries; apply loader-specific argument repairs for this launch.
|
AtomicReference<JavaRuntime> javaVersionRef = new AtomicReference<>();
|
||||||
context.launchManifest = LaunchManifestNormalizer.repairForLaunch(gameInstance.getResolvedManifest());
|
|
||||||
|
|
||||||
TaskExecutor executor = checkGameState(gameInstance, setting, context.launchManifest)
|
TaskExecutor executor = checkGameState(profile, setting, version.get())
|
||||||
.thenComposeAsync(java -> {
|
.thenComposeAsync(java -> {
|
||||||
context.javaRuntime = Objects.requireNonNull(java);
|
javaVersionRef.set(Objects.requireNonNull(java));
|
||||||
context.javaExecutable = java.getBinary();
|
version.set(NativePatcher.patchNative(repository, version.get(), gameVersion.orElse(null), java, setting, javaArguments));
|
||||||
if (java.getPlatform().os() == OperatingSystem.WINDOWS
|
if (setting.isNotCheckGame())
|
||||||
&& scriptFile == null
|
|
||||||
&& !setting.getInheritable(GameSettings::notUseJavawProperty)
|
|
||||||
&& FileUtils.getName(java.getBinary()).equalsIgnoreCase("java.exe")) {
|
|
||||||
Path javawBinary = java.getBinary().resolveSibling("javaw.exe");
|
|
||||||
if (Files.isRegularFile(javawBinary)) {
|
|
||||||
context.javaExecutable = javawBinary;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
context.launchManifest = NativePatcher.patchNative(gameInstance, context.launchManifest, java, setting, javaArguments);
|
|
||||||
if (setting.getInheritable(GameSettings::notCheckGameProperty))
|
|
||||||
return null;
|
return null;
|
||||||
return Task.allOf(
|
return Task.allOf(
|
||||||
dependencyManager.checkGameCompletionAsync(gameInstance, context.launchManifest, integrityCheck),
|
dependencyManager.checkGameCompletionAsync(version.get(), integrityCheck),
|
||||||
Task.composeAsync(() -> {
|
Task.composeAsync(() -> {
|
||||||
try {
|
try {
|
||||||
@Nullable ModpackConfiguration<?> configuration =
|
ModpackConfiguration<?> configuration = ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(selectedVersion));
|
||||||
gameInstance.readModpackConfiguration();
|
ModpackProvider provider = ModpackHelper.getProviderByType(configuration.getType());
|
||||||
if (configuration == null) return null;
|
|
||||||
@Nullable ModpackProvider provider =
|
|
||||||
ModpackHelper.getProviderByType(configuration.getType());
|
|
||||||
if (provider == null) return null;
|
if (provider == null) return null;
|
||||||
else return provider.createCompletionTask(
|
else return provider.createCompletionTask(dependencyManager, selectedVersion);
|
||||||
dependencyManager,
|
|
||||||
gameInstance);
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
Task.composeAsync(() -> {
|
Task.composeAsync(() -> {
|
||||||
if (OperatingSystem.CURRENT_OS != OperatingSystem.WINDOWS
|
Renderer renderer = setting.getRenderer();
|
||||||
|| !(setting.getRenderer(gameInstance.getVersion()) instanceof Renderer.Driver renderer)
|
if (renderer != Renderer.DEFAULT && OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||||
|| renderer.mesaDriverName() == null)
|
Library lib = NativePatcher.getWindowsMesaLoader(java, renderer, OperatingSystem.SYSTEM_VERSION);
|
||||||
return null;
|
if (lib == null)
|
||||||
|
return null;
|
||||||
|
Path file = dependencyManager.getGameRepository().getLibraryFile(version.get(), lib);
|
||||||
|
if (file.toAbsolutePath().toString().indexOf('=') >= 0) {
|
||||||
|
LOG.warning("Invalid character '=' in the libraries directory path, unable to attach software renderer loader");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
Library lib = NativePatcher.getWindowsMesaLoader(java, renderer, OperatingSystem.SYSTEM_VERSION);
|
String agent = FileUtils.getAbsolutePath(file) + "=" + renderer.name().toLowerCase(Locale.ROOT);
|
||||||
if (lib == null)
|
|
||||||
return null;
|
|
||||||
Path file = gameInstance.getLayout().getLibraryFile(gameInstance.getId(), lib);
|
|
||||||
if (file.toAbsolutePath().toString().indexOf('=') >= 0) {
|
|
||||||
LOG.warning("Invalid character '=' in the libraries directory path, unable to attach software renderer loader");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
String agent = FileUtils.getAbsolutePath(file) + "=" + renderer.mesaDriverName();
|
if (GameLibrariesTask.shouldDownloadLibrary(repository, version.get(), lib, integrityCheck)) {
|
||||||
|
return new LibraryDownloadTask(dependencyManager, file, lib)
|
||||||
if (GameLibrariesTask.shouldDownloadLibrary(repository, context.launchManifest, lib, integrityCheck)) {
|
.thenRunAsync(() -> javaAgents.add(agent));
|
||||||
return new LibraryDownloadTask(dependencyManager, file, lib)
|
} else {
|
||||||
.thenRunAsync(() -> javaAgents.add(agent));
|
javaAgents.add(agent);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
javaAgents.add(agent);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}).withStage("launch.state.dependencies")
|
}).withStage("launch.state.dependencies")
|
||||||
.thenComposeAsync(() -> new GameVerificationFixTask(gameInstance, gameInstance.getVersion(), context.launchManifest))
|
.thenComposeAsync(() -> gameVersion.map(s -> new GameVerificationFixTask(dependencyManager, s, version.get())).orElse(null))
|
||||||
.thenComposeAsync(() -> {
|
|
||||||
if (setting.getInheritable(GameSettings::allowAutoAgentProperty)
|
|
||||||
|| setting.getInheritable(GameSettings::noJVMOptionsProperty)
|
|
||||||
|| setting.getInheritable(GameSettings::noOptimizingJVMOptionsProperty)
|
|
||||||
|| Boolean.TRUE.equals(state().getShownTips().get(LWJGL_3_4_1_TIP))
|
|
||||||
|| !NativePatcher.needPatchMemoryUtil(context.launchManifest, context.javaRuntime.getParsedVersion())) {
|
|
||||||
return Task.completed(null);
|
|
||||||
} else {
|
|
||||||
CompletableFuture<Void> future = new CompletableFuture<>();
|
|
||||||
runInFX(() -> {
|
|
||||||
Controllers.confirm(i18n("launch.advice.lwjgl_3_4_1"), i18n("launch.advice.lwjgl_3_4_1.title"), MessageType.QUESTION, () -> {
|
|
||||||
state().getShownTips().put(LWJGL_3_4_1_TIP, true);
|
|
||||||
enableAutoAgentForCurrentSetting();
|
|
||||||
future.complete(null);
|
|
||||||
}, () -> {
|
|
||||||
state().getShownTips().put(LWJGL_3_4_1_TIP, true);
|
|
||||||
future.complete(null);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return Task.fromCompletableFuture(future);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.thenComposeAsync(() -> logIn(account).withStage("launch.state.logging_in"))
|
.thenComposeAsync(() -> logIn(account).withStage("launch.state.logging_in"))
|
||||||
.thenComposeAsync(authInfo -> Task.supplyAsync(() -> {
|
.thenComposeAsync(authInfo -> Task.supplyAsync(() -> {
|
||||||
LaunchOptions.Builder launchOptionsBuilder = gameInstance.getLaunchOptions(
|
LaunchOptions launchOptions = repository.getLaunchOptions(
|
||||||
context.javaRuntime,
|
selectedVersion, javaVersionRef.get(), profile.getGameDir(), javaAgents, javaArguments, scriptFile != null);
|
||||||
context.javaExecutable,
|
|
||||||
repository.getBaseDirectory(),
|
|
||||||
javaAgents,
|
|
||||||
javaArguments,
|
|
||||||
scriptFile != null);
|
|
||||||
if (disableOfflineSkin) {
|
|
||||||
launchOptionsBuilder.setDaemon(false);
|
|
||||||
}
|
|
||||||
if (quickPlayOption != null) {
|
|
||||||
launchOptionsBuilder.setQuickPlayOption(quickPlayOption);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && setting.getInheritable(GameSettings::highPerformanceProperty)) {
|
LOG.info("Here's the structure of game mod directory:\n" + FileUtils.printFileStructure(repository.getModsDirectory(selectedVersion), 10));
|
||||||
@Nullable String javaPathGpuReg = FileUtils.getAbsolutePath(context.javaExecutable);
|
|
||||||
try {
|
|
||||||
WinReg reg = WinReg.INSTANCE;
|
|
||||||
if (reg != null) {
|
|
||||||
Object current = reg.queryValue(
|
|
||||||
WinReg.HKEY.HKEY_CURRENT_USER,
|
|
||||||
"Software\\Microsoft\\DirectX\\UserGpuPreferences",
|
|
||||||
javaPathGpuReg
|
|
||||||
);
|
|
||||||
if (current == null) {
|
|
||||||
boolean result = reg.setValue(
|
|
||||||
WinReg.HKEY.HKEY_CURRENT_USER,
|
|
||||||
"Software\\Microsoft\\DirectX\\UserGpuPreferences",
|
|
||||||
javaPathGpuReg,
|
|
||||||
"GpuPreference=2;"
|
|
||||||
);
|
|
||||||
if (result)
|
|
||||||
LOG.info("Successfully applied high performance GPU preference for java: " + javaPathGpuReg);
|
|
||||||
else
|
|
||||||
LOG.warning("Failed to apply high performance GPU preference for java: " + javaPathGpuReg);
|
|
||||||
} else {
|
|
||||||
LOG.info("GPU preference for %s already exists: %s".formatted(javaPathGpuReg, current));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
LOG.warning("Failed to apply high performance GPU preference", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchOptions launchOptions = launchOptionsBuilder.create();
|
|
||||||
|
|
||||||
LOG.info("Here's the structure of game mod directory:\n" + FileUtils.printFileStructure(gameInstance.getModsDirectory(), 10));
|
|
||||||
|
|
||||||
return new HMCLGameLauncher(
|
return new HMCLGameLauncher(
|
||||||
gameInstance,
|
repository,
|
||||||
context.launchManifest,
|
version.get(),
|
||||||
authInfo,
|
authInfo,
|
||||||
launchOptions,
|
launchOptions,
|
||||||
launcherVisibility == LauncherVisibility.CLOSE
|
launcherVisibility == LauncherVisibility.CLOSE
|
||||||
? null // Unnecessary to start listening to game process output when close launcher immediately after game launched.
|
? null // Unnecessary to start listening to game process output when close launcher immediately after game launched.
|
||||||
: new HMCLProcessListener(authInfo, launchOptions, launchingLatch, gameInstance.getVersion().compareTo(GameVersionNumber.unknown()) != 0)
|
: new HMCLProcessListener(repository, version.get(), authInfo, launchOptions, launchingLatch, gameVersion.isPresent())
|
||||||
);
|
);
|
||||||
}).thenComposeAsync(launcher -> { // launcher is prev task's result
|
}).thenComposeAsync(launcher -> { // launcher is prev task's result
|
||||||
if (scriptFile == null) {
|
if (scriptFile == null) {
|
||||||
@@ -312,25 +217,25 @@ public final class LauncherHelper {
|
|||||||
if (launcherVisibility == LauncherVisibility.CLOSE)
|
if (launcherVisibility == LauncherVisibility.CLOSE)
|
||||||
Launcher.stopApplication();
|
Launcher.stopApplication();
|
||||||
else
|
else
|
||||||
launchingStepsPane.setCancel(it -> {
|
launchingStepsPane.setCancel(new TaskCancellationAction(it -> {
|
||||||
process.stop();
|
process.stop();
|
||||||
it.fireEvent(new DialogCloseEvent());
|
it.fireEvent(new DialogCloseEvent());
|
||||||
});
|
}));
|
||||||
} else {
|
} else {
|
||||||
runLater(() -> {
|
runLater(() -> {
|
||||||
launchingStepsPane.fireEvent(new DialogCloseEvent());
|
launchingStepsPane.fireEvent(new DialogCloseEvent());
|
||||||
Controllers.dialog(i18n("instance.launch_script.success", FileUtils.getAbsolutePath(scriptFile)));
|
Controllers.dialog(i18n("version.launch_script.success", FileUtils.getAbsolutePath(scriptFile)));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}).withFakeProgress(
|
}).withFakeProgress(
|
||||||
i18n("message.doing"),
|
i18n("message.doing"),
|
||||||
() -> launchingLatch.getCount() == 0, 6.95
|
() -> launchingLatch.getCount() == 0, 6.95
|
||||||
).withStage("launch.state.waiting_launching"))
|
).withStage("launch.state.waiting_launching"))
|
||||||
.withStagesHints(
|
.withStagesHint(Lang.immutableListOf(
|
||||||
new Task.StagesHint("launch.state.java"),
|
"launch.state.java",
|
||||||
new Task.StagesHint("launch.state.dependencies", List.of("hmcl.install.assets", "hmcl.install.libraries", "hmcl.modpack.download")),
|
"launch.state.dependencies",
|
||||||
new Task.StagesHint("launch.state.logging_in"),
|
"launch.state.logging_in",
|
||||||
new Task.StagesHint("launch.state.waiting_launching"))
|
"launch.state.waiting_launching"))
|
||||||
.executor();
|
.executor();
|
||||||
launchingStepsPane.setExecutor(executor, false);
|
launchingStepsPane.setExecutor(executor, false);
|
||||||
executor.addTaskListener(new TaskListener() {
|
executor.addTaskListener(new TaskListener() {
|
||||||
@@ -358,29 +263,31 @@ public final class LauncherHelper {
|
|||||||
} else if (ex instanceof NotDecompressingNativesException) {
|
} else if (ex instanceof NotDecompressingNativesException) {
|
||||||
message = i18n("launch.failed.decompressing_natives") + "\n" + ex.getLocalizedMessage();
|
message = i18n("launch.failed.decompressing_natives") + "\n" + ex.getLocalizedMessage();
|
||||||
} else if (ex instanceof LibraryDownloadException) {
|
} else if (ex instanceof LibraryDownloadException) {
|
||||||
message = i18n("launch.failed.download_library", ((LibraryDownloadException) ex).getLibrary().name()) + "\n";
|
message = i18n("launch.failed.download_library", ((LibraryDownloadException) ex).getLibrary().getName()) + "\n";
|
||||||
if (ex.getCause() instanceof ResponseCodeException rce) {
|
if (ex.getCause() instanceof ResponseCodeException) {
|
||||||
|
ResponseCodeException rce = (ResponseCodeException) ex.getCause();
|
||||||
int responseCode = rce.getResponseCode();
|
int responseCode = rce.getResponseCode();
|
||||||
String url = rce.getUrl();
|
String uri = rce.getUri();
|
||||||
if (responseCode == 404)
|
if (responseCode == 404)
|
||||||
message += i18n("download.code.404", url);
|
message += i18n("download.code.404", uri);
|
||||||
else
|
else
|
||||||
message += i18n("download.failed", url, responseCode);
|
message += i18n("download.failed", uri, responseCode);
|
||||||
} else {
|
} else {
|
||||||
message += StringUtils.getStackTrace(ex.getCause());
|
message += StringUtils.getStackTrace(ex.getCause());
|
||||||
}
|
}
|
||||||
} else if (ex instanceof DownloadException de) {
|
} else if (ex instanceof DownloadException) {
|
||||||
String url = de.getUrl();
|
URI uri = ((DownloadException) ex).getUri();
|
||||||
if (ex.getCause() instanceof SocketTimeoutException) {
|
if (ex.getCause() instanceof SocketTimeoutException) {
|
||||||
message = i18n("install.failed.downloading.timeout", url);
|
message = i18n("install.failed.downloading.timeout", uri);
|
||||||
} else if (ex.getCause() instanceof ResponseCodeException responseCodeException) {
|
} else if (ex.getCause() instanceof ResponseCodeException) {
|
||||||
|
ResponseCodeException responseCodeException = (ResponseCodeException) ex.getCause();
|
||||||
if (I18n.hasKey("download.code." + responseCodeException.getResponseCode())) {
|
if (I18n.hasKey("download.code." + responseCodeException.getResponseCode())) {
|
||||||
message = i18n("download.code." + responseCodeException.getResponseCode(), url);
|
message = i18n("download.code." + responseCodeException.getResponseCode(), uri);
|
||||||
} else {
|
} else {
|
||||||
message = i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(ex.getCause());
|
message = i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(ex.getCause());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
message = i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(ex.getCause());
|
message = i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(ex.getCause());
|
||||||
}
|
}
|
||||||
} else if (ex instanceof GameAssetIndexDownloadTask.GameAssetIndexMalformedException) {
|
} else if (ex instanceof GameAssetIndexDownloadTask.GameAssetIndexMalformedException) {
|
||||||
message = i18n("assets.index.malformed");
|
message = i18n("assets.index.malformed");
|
||||||
@@ -388,24 +295,25 @@ public final class LauncherHelper {
|
|||||||
message = i18n("account.failed.injector_download_failure");
|
message = i18n("account.failed.injector_download_failure");
|
||||||
} else if (ex instanceof CharacterDeletedException) {
|
} else if (ex instanceof CharacterDeletedException) {
|
||||||
message = i18n("account.failed.character_deleted");
|
message = i18n("account.failed.character_deleted");
|
||||||
} else if (ex instanceof ResponseCodeException rce) {
|
} else if (ex instanceof ResponseCodeException) {
|
||||||
|
ResponseCodeException rce = (ResponseCodeException) ex;
|
||||||
int responseCode = rce.getResponseCode();
|
int responseCode = rce.getResponseCode();
|
||||||
String url = rce.getUrl();
|
String uri = rce.getUri();
|
||||||
if (responseCode == 404)
|
if (responseCode == 404)
|
||||||
message = i18n("download.code.404", url);
|
message = i18n("download.code.404", uri);
|
||||||
else
|
else
|
||||||
message = i18n("download.failed", url, responseCode);
|
message = i18n("download.failed", uri, responseCode);
|
||||||
} else if (ex instanceof CommandTooLongException) {
|
} else if (ex instanceof CommandTooLongException) {
|
||||||
message = i18n("launch.failed.command_too_long");
|
message = i18n("launch.failed.command_too_long");
|
||||||
} else if (ex instanceof ExecutionPolicyLimitException) {
|
} else if (ex instanceof ExecutionPolicyLimitException) {
|
||||||
Controllers.prompt(new PromptDialogPane.Builder(i18n("launch.failed.execution_policy"),
|
Controllers.prompt(new PromptDialogPane.Builder(i18n("launch.failed.execution_policy"),
|
||||||
(result, handler) -> {
|
(result, resolve, reject) -> {
|
||||||
if (CommandBuilder.setExecutionPolicy()) {
|
if (CommandBuilder.setExecutionPolicy()) {
|
||||||
LOG.info("Set the ExecutionPolicy for the scope 'CurrentUser' to 'RemoteSigned'");
|
LOG.info("Set the ExecutionPolicy for the scope 'CurrentUser' to 'RemoteSigned'");
|
||||||
handler.resolve();
|
resolve.run();
|
||||||
} else {
|
} else {
|
||||||
LOG.warning("Failed to set ExecutionPolicy");
|
LOG.warning("Failed to set ExecutionPolicy");
|
||||||
handler.reject(i18n("launch.failed.execution_policy.failed_to_set"));
|
reject.accept(i18n("launch.failed.execution_policy.failed_to_set"));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.addQuestion(new PromptDialogPane.Builder.HintQuestion(i18n("launch.failed.execution_policy.hint")))
|
.addQuestion(new PromptDialogPane.Builder.HintQuestion(i18n("launch.failed.execution_policy.hint")))
|
||||||
@@ -418,7 +326,7 @@ public final class LauncherHelper {
|
|||||||
message = StringUtils.getStackTrace(ex);
|
message = StringUtils.getStackTrace(ex);
|
||||||
}
|
}
|
||||||
Controllers.dialog(message,
|
Controllers.dialog(message,
|
||||||
scriptFile == null ? i18n("launch.failed") : i18n("instance.launch_script.failed"),
|
scriptFile == null ? i18n("launch.failed") : i18n("version.launch_script.failed"),
|
||||||
MessageType.ERROR);
|
MessageType.ERROR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -428,26 +336,24 @@ public final class LauncherHelper {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
launchingStepsPane.startExecutor();
|
executor.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks launch requirements, allowing required Java downloads to share this launch's execution slot.
|
private static Task<JavaRuntime> checkGameState(Profile profile, VersionSetting setting, Version version) {
|
||||||
private Task<JavaRuntime> checkGameState(HMCLGameInstance gameInstance, GameSettings.Effective setting, GameInstanceManifest manifest) {
|
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(version, profile.getRepository().getGameVersion(version).orElse(null));
|
||||||
GameComponentAnalyzer analyzer = GameComponentAnalyzer.analyze(manifest, gameInstance.getVersion());
|
GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(analyzer.getVersion(LibraryAnalyzer.LibraryType.MINECRAFT));
|
||||||
GameVersionNumber gameVersion = gameInstance.getVersion();
|
|
||||||
|
|
||||||
Task<JavaRuntime> getJavaTask = Task.supplyAsync(() -> {
|
Task<JavaRuntime> getJavaTask = Task.supplyAsync(() -> {
|
||||||
try {
|
try {
|
||||||
return setting.getJava(gameVersion, manifest);
|
return setting.getJava(gameVersion, version);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
throw new CancellationException();
|
throw new CancellationException();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Task<JavaRuntime> task;
|
Task<JavaRuntime> task;
|
||||||
JavaVersionType javaVersionType = setting.getInheritable(GameSettings::javaTypeProperty);
|
if (setting.isNotCheckJVM()) {
|
||||||
if (setting.getInheritable(GameSettings::notCheckJVMProperty)) {
|
|
||||||
task = getJavaTask.thenApplyAsync(java -> Lang.requireNonNullElse(java, JavaRuntime.getDefault()));
|
task = getJavaTask.thenApplyAsync(java -> Lang.requireNonNullElse(java, JavaRuntime.getDefault()));
|
||||||
} else if (javaVersionType == JavaVersionType.AUTO || javaVersionType == JavaVersionType.VERSION) {
|
} else if (setting.getJavaVersionType() == JavaVersionType.AUTO || setting.getJavaVersionType() == JavaVersionType.VERSION) {
|
||||||
task = getJavaTask.thenComposeAsync(Schedulers.javafx(), java -> {
|
task = getJavaTask.thenComposeAsync(Schedulers.javafx(), java -> {
|
||||||
if (java != null) {
|
if (java != null) {
|
||||||
return Task.completed(java);
|
return Task.completed(java);
|
||||||
@@ -460,21 +366,12 @@ public final class LauncherHelper {
|
|||||||
List<GameJavaVersion> supportedVersions = GameJavaVersion.getSupportedVersions(SYSTEM_PLATFORM);
|
List<GameJavaVersion> supportedVersions = GameJavaVersion.getSupportedVersions(SYSTEM_PLATFORM);
|
||||||
|
|
||||||
GameJavaVersion targetJavaVersion = null;
|
GameJavaVersion targetJavaVersion = null;
|
||||||
if (javaVersionType == JavaVersionType.VERSION) {
|
if (setting.getJavaVersionType() == JavaVersionType.VERSION) {
|
||||||
try {
|
try {
|
||||||
int targetJavaVersionMajor = Integer.parseInt(setting.getInheritable(GameSettings::customJavaVersionProperty));
|
int targetJavaVersionMajor = Integer.parseInt(setting.getJavaVersion());
|
||||||
GameJavaVersion minimumJavaVersion = null;
|
GameJavaVersion minimumJavaVersion = GameJavaVersion.getMinimumJavaVersion(gameVersion);
|
||||||
if (gameVersion.compareTo("1.12.2") == 0) {
|
|
||||||
@Nullable String cleanroomVersion = analyzer.getVersion(GameComponentType.CLEANROOM);
|
|
||||||
if (cleanroomVersion != null) {
|
|
||||||
minimumJavaVersion = GameJavaVersion.getCleanroomJavaVersion(cleanroomVersion);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (minimumJavaVersion == null)
|
if (minimumJavaVersion != null && targetJavaVersionMajor < minimumJavaVersion.getMajorVersion()) {
|
||||||
minimumJavaVersion = GameJavaVersion.getMinimumJavaVersion(gameVersion);
|
|
||||||
|
|
||||||
if (minimumJavaVersion != null && targetJavaVersionMajor < minimumJavaVersion.majorVersion()) {
|
|
||||||
Controllers.dialog(
|
Controllers.dialog(
|
||||||
i18n("launch.failed.java_version_too_low"),
|
i18n("launch.failed.java_version_too_low"),
|
||||||
i18n("message.error"),
|
i18n("message.error"),
|
||||||
@@ -487,20 +384,11 @@ public final class LauncherHelper {
|
|||||||
targetJavaVersion = GameJavaVersion.get(targetJavaVersionMajor);
|
targetJavaVersion = GameJavaVersion.get(targetJavaVersionMajor);
|
||||||
} catch (NumberFormatException ignored) {
|
} catch (NumberFormatException ignored) {
|
||||||
}
|
}
|
||||||
} else {
|
} else
|
||||||
if (gameVersion.compareTo("1.12.2") == 0) {
|
targetJavaVersion = version.getJavaVersion();
|
||||||
@Nullable String cleanroomVersion = analyzer.getVersion(GameComponentType.CLEANROOM);
|
|
||||||
if (cleanroomVersion != null) {
|
|
||||||
targetJavaVersion = GameJavaVersion.getCleanroomJavaVersion(cleanroomVersion);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetJavaVersion == null)
|
|
||||||
targetJavaVersion = manifest.javaVersion();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetJavaVersion != null && supportedVersions.contains(targetJavaVersion)) {
|
if (targetJavaVersion != null && supportedVersions.contains(targetJavaVersion)) {
|
||||||
downloadJava(targetJavaVersion, gameInstance.getRepository())
|
downloadJava(targetJavaVersion, profile)
|
||||||
.whenCompleteAsync((downloadedJava, exception) -> {
|
.whenCompleteAsync((downloadedJava, exception) -> {
|
||||||
if (exception == null) {
|
if (exception == null) {
|
||||||
future.complete(downloadedJava);
|
future.complete(downloadedJava);
|
||||||
@@ -526,8 +414,8 @@ public final class LauncherHelper {
|
|||||||
|
|
||||||
if (java != null) {
|
if (java != null) {
|
||||||
for (JavaVersionConstraint constraint : JavaVersionConstraint.ALL) {
|
for (JavaVersionConstraint constraint : JavaVersionConstraint.ALL) {
|
||||||
if (constraint.appliesToVersion(gameVersion, manifest, java, analyzer)) {
|
if (constraint.appliesToVersion(gameVersion, version, java, analyzer)) {
|
||||||
if (!constraint.checkJava(gameVersion, manifest, java, analyzer)) {
|
if (!constraint.checkJava(gameVersion, version, java)) {
|
||||||
if (constraint.isMandatory()) {
|
if (constraint.isMandatory()) {
|
||||||
violatedMandatoryConstraints.add(constraint);
|
violatedMandatoryConstraints.add(constraint);
|
||||||
} else {
|
} else {
|
||||||
@@ -543,7 +431,7 @@ public final class LauncherHelper {
|
|||||||
Runnable breakAction = () -> future.completeExceptionally(new CancellationException("Launch operation was cancelled by user"));
|
Runnable breakAction = () -> future.completeExceptionally(new CancellationException("Launch operation was cancelled by user"));
|
||||||
|
|
||||||
if (java == null || !violatedMandatoryConstraints.isEmpty()) {
|
if (java == null || !violatedMandatoryConstraints.isEmpty()) {
|
||||||
JavaRuntime suggestedJava = JavaManager.findSuitableJava(gameVersion, manifest);
|
JavaRuntime suggestedJava = JavaManager.findSuitableJava(gameVersion, version);
|
||||||
if (suggestedJava != null) {
|
if (suggestedJava != null) {
|
||||||
FXUtils.runInFX(() -> {
|
FXUtils.runInFX(() -> {
|
||||||
Controllers.confirm(i18n("launch.advice.java.auto"), i18n("message.warning"), () -> {
|
Controllers.confirm(i18n("launch.advice.java.auto"), i18n("message.warning"), () -> {
|
||||||
@@ -562,21 +450,17 @@ public final class LauncherHelper {
|
|||||||
return result;
|
return result;
|
||||||
} else {
|
} else {
|
||||||
GameJavaVersion gameJavaVersion;
|
GameJavaVersion gameJavaVersion;
|
||||||
if (violatedMandatoryConstraints.contains(JavaVersionConstraint.CLEANROOM)) {
|
if (violatedMandatoryConstraints.contains(JavaVersionConstraint.CLEANROOM_JAVA_21))
|
||||||
@Nullable String cleanroomVersion = analyzer.getVersion(GameComponentType.CLEANROOM);
|
gameJavaVersion = GameJavaVersion.JAVA_21;
|
||||||
|
else if (violatedMandatoryConstraints.contains(JavaVersionConstraint.GAME_JSON))
|
||||||
gameJavaVersion = cleanroomVersion != null
|
gameJavaVersion = version.getJavaVersion();
|
||||||
? GameJavaVersion.getCleanroomJavaVersion(cleanroomVersion)
|
|
||||||
: GameJavaVersion.JAVA_21;
|
|
||||||
} else if (violatedMandatoryConstraints.contains(JavaVersionConstraint.GAME_JSON))
|
|
||||||
gameJavaVersion = manifest.javaVersion();
|
|
||||||
else if (violatedMandatoryConstraints.contains(JavaVersionConstraint.VANILLA))
|
else if (violatedMandatoryConstraints.contains(JavaVersionConstraint.VANILLA))
|
||||||
gameJavaVersion = GameJavaVersion.getMinimumJavaVersion(gameVersion);
|
gameJavaVersion = GameJavaVersion.getMinimumJavaVersion(gameVersion);
|
||||||
else
|
else
|
||||||
gameJavaVersion = null;
|
gameJavaVersion = null;
|
||||||
|
|
||||||
if (gameJavaVersion != null) {
|
if (gameJavaVersion != null) {
|
||||||
FXUtils.runInFX(() -> downloadJava(gameJavaVersion, gameInstance.getRepository()).whenCompleteAsync((downloadedJava, throwable) -> {
|
FXUtils.runInFX(() -> downloadJava(gameJavaVersion, profile).whenCompleteAsync((downloadedJava, throwable) -> {
|
||||||
if (throwable == null) {
|
if (throwable == null) {
|
||||||
setting.setJavaAutoSelected();
|
setting.setJavaAutoSelected();
|
||||||
future.complete(downloadedJava);
|
future.complete(downloadedJava);
|
||||||
@@ -589,7 +473,7 @@ public final class LauncherHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (violatedMandatoryConstraints.contains(JavaVersionConstraint.VANILLA_LINUX_JAVA_8)) {
|
if (violatedMandatoryConstraints.contains(JavaVersionConstraint.VANILLA_LINUX_JAVA_8)) {
|
||||||
if (!setting.getInheritable(GameSettings::useCustomNativesProperty)) {
|
if (setting.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER) {
|
||||||
FXUtils.runInFX(() -> Controllers.dialog(i18n("launch.advice.vanilla_linux_java_8"), i18n("message.error"), MessageType.ERROR, breakAction));
|
FXUtils.runInFX(() -> Controllers.dialog(i18n("launch.advice.vanilla_linux_java_8"), i18n("message.error"), MessageType.ERROR, breakAction));
|
||||||
return result;
|
return result;
|
||||||
} else {
|
} else {
|
||||||
@@ -626,7 +510,7 @@ public final class LauncherHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 32-bit JVM cannot make use of too much memory.
|
// 32-bit JVM cannot make use of too much memory.
|
||||||
if (java.getBits() == Bits.BIT_32 && !setting.getInheritable(GameSettings::autoMemoryProperty) && setting.getMaxMemory() > 1.5 * 1024) {
|
if (java.getBits() == Bits.BIT_32 && setting.getMaxMemory() > 1.5 * 1024) {
|
||||||
// 1.5 * 1024 is an inaccurate number.
|
// 1.5 * 1024 is an inaccurate number.
|
||||||
// Actual memory limit depends on operating system and memory.
|
// Actual memory limit depends on operating system and memory.
|
||||||
suggestions.add(i18n("launch.advice.too_large_memory_for_32bit"));
|
suggestions.add(i18n("launch.advice.too_large_memory_for_32bit"));
|
||||||
@@ -646,7 +530,7 @@ public final class LauncherHelper {
|
|||||||
break;
|
break;
|
||||||
case MODDED_JAVA_16:
|
case MODDED_JAVA_16:
|
||||||
// Minecraft<=1.17.1+Forge[37.0.0,37.0.60) not compatible with Java 17
|
// Minecraft<=1.17.1+Forge[37.0.0,37.0.60) not compatible with Java 17
|
||||||
@Nullable String forgePatchVersion = analyzer.getVersion(GameComponentType.FORGE);
|
String forgePatchVersion = analyzer.getVersion(LibraryAnalyzer.LibraryType.FORGE).orElse(null);
|
||||||
if (forgePatchVersion != null && VersionNumber.compare(forgePatchVersion, "37.0.60") < 0)
|
if (forgePatchVersion != null && VersionNumber.compare(forgePatchVersion, "37.0.60") < 0)
|
||||||
suggestions.add(i18n("launch.advice.forge37_0_60"));
|
suggestions.add(i18n("launch.advice.forge37_0_60"));
|
||||||
else
|
else
|
||||||
@@ -658,14 +542,9 @@ public final class LauncherHelper {
|
|||||||
case MODDED_JAVA_21:
|
case MODDED_JAVA_21:
|
||||||
suggestions.add(i18n("launch.advice.modded_java", 21, gameVersion));
|
suggestions.add(i18n("launch.advice.modded_java", 21, gameVersion));
|
||||||
break;
|
break;
|
||||||
case CLEANROOM: {
|
case CLEANROOM_JAVA_21:
|
||||||
@Nullable String cleanroomVersion = analyzer.getVersion(GameComponentType.CLEANROOM);
|
suggestions.add(i18n("launch.advice.cleanroom"));
|
||||||
if (cleanroomVersion != null)
|
|
||||||
suggestions.add(i18n("launch.advice.cleanroom", GameJavaVersion.getCleanroomJavaVersion(cleanroomVersion).majorVersion(), cleanroomVersion));
|
|
||||||
else
|
|
||||||
suggestions.add(i18n("launch.advice.cleanroom", 21, ""));
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
case VANILLA_JAVA_8_51:
|
case VANILLA_JAVA_8_51:
|
||||||
suggestions.add(i18n("launch.advice.java8_51_1_13"));
|
suggestions.add(i18n("launch.advice.java8_51_1_13"));
|
||||||
break;
|
break;
|
||||||
@@ -673,8 +552,8 @@ public final class LauncherHelper {
|
|||||||
suggestions.add(i18n("launch.advice.modlauncher8"));
|
suggestions.add(i18n("launch.advice.modlauncher8"));
|
||||||
break;
|
break;
|
||||||
case VANILLA_X86:
|
case VANILLA_X86:
|
||||||
if (!setting.getInheritable(GameSettings::useCustomNativesProperty)
|
if (setting.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER
|
||||||
&& Platform.isSupportedTranslationX86_64()) {
|
&& isCompatibleWithX86Java()) {
|
||||||
suggestions.add(i18n("launch.advice.vanilla_x86.translation"));
|
suggestions.add(i18n("launch.advice.vanilla_x86.translation"));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -685,24 +564,24 @@ public final class LauncherHelper {
|
|||||||
|
|
||||||
// Cannot allocate too much memory exceeding free space.
|
// Cannot allocate too much memory exceeding free space.
|
||||||
long totalMemorySizeMB = (long) MEGABYTES.convertFromBytes(SystemInfo.getTotalMemorySize());
|
long totalMemorySizeMB = (long) MEGABYTES.convertFromBytes(SystemInfo.getTotalMemorySize());
|
||||||
if (totalMemorySizeMB > 0 && !setting.getInheritable(GameSettings::autoMemoryProperty) && totalMemorySizeMB < setting.getMaxMemory()) {
|
if (totalMemorySizeMB > 0 && totalMemorySizeMB < setting.getMaxMemory()) {
|
||||||
suggestions.add(i18n("launch.advice.not_enough_space", totalMemorySizeMB));
|
suggestions.add(i18n("launch.advice.not_enough_space", totalMemorySizeMB));
|
||||||
}
|
}
|
||||||
|
|
||||||
VersionNumber forgeVersion = Optional.ofNullable(analyzer.getVersion(GameComponentType.FORGE))
|
VersionNumber forgeVersion = analyzer.getVersion(LibraryAnalyzer.LibraryType.FORGE)
|
||||||
.map(VersionNumber::asVersion)
|
.map(VersionNumber::asVersion)
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
|
|
||||||
// Forge 2760~2773 will crash game with LiteLoader.
|
// Forge 2760~2773 will crash game with LiteLoader.
|
||||||
boolean hasForge2760 = forgeVersion != null && (forgeVersion.compareTo("1.12.2-14.23.5.2760") >= 0) && (forgeVersion.compareTo("1.12.2-14.23.5.2773") < 0);
|
boolean hasForge2760 = forgeVersion != null && (forgeVersion.compareTo("1.12.2-14.23.5.2760") >= 0) && (forgeVersion.compareTo("1.12.2-14.23.5.2773") < 0);
|
||||||
boolean hasLiteLoader = manifest.getLibraries().stream().anyMatch(it -> it.is("com.mumfrey", "liteloader"));
|
boolean hasLiteLoader = version.getLibraries().stream().anyMatch(it -> it.is("com.mumfrey", "liteloader"));
|
||||||
if (hasForge2760 && hasLiteLoader && gameVersion.compareTo("1.12.2") == 0) {
|
if (hasForge2760 && hasLiteLoader && gameVersion.compareTo("1.12.2") == 0) {
|
||||||
suggestions.add(i18n("launch.advice.forge2760_liteloader"));
|
suggestions.add(i18n("launch.advice.forge2760_liteloader"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// OptiFine 1.14.4 is not compatible with Forge 28.2.2 and later versions.
|
// OptiFine 1.14.4 is not compatible with Forge 28.2.2 and later versions.
|
||||||
boolean hasForge28_2_2 = forgeVersion != null && (forgeVersion.compareTo("1.14.4-28.2.2") >= 0);
|
boolean hasForge28_2_2 = forgeVersion != null && (forgeVersion.compareTo("1.14.4-28.2.2") >= 0);
|
||||||
boolean hasOptiFine = manifest.getLibraries().stream().anyMatch(it -> it.is("optifine", "OptiFine"));
|
boolean hasOptiFine = version.getLibraries().stream().anyMatch(it -> it.is("optifine", "OptiFine"));
|
||||||
if (hasForge28_2_2 && hasOptiFine && gameVersion.compareTo("1.14.4") == 0) {
|
if (hasForge28_2_2 && hasOptiFine && gameVersion.compareTo("1.14.4") == 0) {
|
||||||
suggestions.add(i18n("launch.advice.forge28_2_2_optifine"));
|
suggestions.add(i18n("launch.advice.forge28_2_2_optifine"));
|
||||||
}
|
}
|
||||||
@@ -734,19 +613,14 @@ public final class LauncherHelper {
|
|||||||
return task.withStage("launch.state.java");
|
return task.withStage("launch.state.java");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prompts for and downloads Java as a required child of this launch operation.
|
private static CompletableFuture<JavaRuntime> downloadJava(GameJavaVersion javaVersion, Profile profile) {
|
||||||
private CompletableFuture<JavaRuntime> downloadJava(GameJavaVersion javaVersion, HMCLGameRepository repository) {
|
|
||||||
CompletableFuture<JavaRuntime> future = new CompletableFuture<>();
|
CompletableFuture<JavaRuntime> future = new CompletableFuture<>();
|
||||||
Controllers.dialog(new MessageDialogPane.Builder(
|
Controllers.dialog(new MessageDialogPane.Builder(
|
||||||
i18n("launch.advice.require_newer_java_version", javaVersion.majorVersion()),
|
i18n("launch.advice.require_newer_java_version", javaVersion.getMajorVersion()),
|
||||||
i18n("message.warning"),
|
i18n("message.warning"),
|
||||||
MessageType.QUESTION)
|
MessageType.QUESTION)
|
||||||
.yesOrNo(() -> {
|
.yesOrNo(() -> {
|
||||||
if (!launchingStepsPane.canStartChild()) {
|
DownloadProvider downloadProvider = profile.getDependency().getDownloadProvider();
|
||||||
future.completeExceptionally(new CancellationException());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
DownloadProvider downloadProvider = repository.getDependency().getDownloadProvider();
|
|
||||||
Controllers.taskDialog(JavaManager.getDownloadJavaTask(downloadProvider, SYSTEM_PLATFORM, javaVersion)
|
Controllers.taskDialog(JavaManager.getDownloadJavaTask(downloadProvider, SYSTEM_PLATFORM, javaVersion)
|
||||||
.whenComplete(Schedulers.javafx(), (result, exception) -> {
|
.whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||||
if (exception == null) {
|
if (exception == null) {
|
||||||
@@ -755,23 +629,20 @@ public final class LauncherHelper {
|
|||||||
Throwable resolvedException = resolveException(exception);
|
Throwable resolvedException = resolveException(exception);
|
||||||
LOG.warning("Failed to download java", exception);
|
LOG.warning("Failed to download java", exception);
|
||||||
if (!(resolvedException instanceof CancellationException)) {
|
if (!(resolvedException instanceof CancellationException)) {
|
||||||
Controllers.dialog(I18n.localizeErrorMessage(resolvedException), i18n("install.failed"));
|
Controllers.dialog(DownloadProviders.localizeErrorMessage(resolvedException), i18n("install.failed"));
|
||||||
}
|
}
|
||||||
future.completeExceptionally(new CancellationException());
|
future.completeExceptionally(new CancellationException());
|
||||||
}
|
}
|
||||||
}), i18n("download.java"), it -> future.completeExceptionally(new CancellationException()), launchingStepsPane);
|
}), i18n("download.java"), new TaskCancellationAction(() -> future.completeExceptionally(new CancellationException())));
|
||||||
}, () -> future.completeExceptionally(new CancellationException())).build());
|
}, () -> future.completeExceptionally(new CancellationException())).build());
|
||||||
|
|
||||||
return future;
|
return future;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task<AuthInfo> logIn(Account account) {
|
private static Task<AuthInfo> logIn(Account account) {
|
||||||
return Task.composeAsync(() -> {
|
return Task.composeAsync(() -> {
|
||||||
try {
|
try {
|
||||||
if (disableOfflineSkin && account instanceof OfflineAccount offlineAccount)
|
return Task.completed(account.logIn());
|
||||||
return Task.completed(offlineAccount.logInWithoutSkin());
|
|
||||||
else
|
|
||||||
return Task.completed(account.logIn());
|
|
||||||
} catch (CredentialExpiredException e) {
|
} catch (CredentialExpiredException e) {
|
||||||
LOG.info("Credential has expired", e);
|
LOG.info("Credential has expired", e);
|
||||||
|
|
||||||
@@ -810,10 +681,7 @@ public final class LauncherHelper {
|
|||||||
case HIDE_AND_REOPEN:
|
case HIDE_AND_REOPEN:
|
||||||
runLater(() -> {
|
runLater(() -> {
|
||||||
Optional.ofNullable(Controllers.getStage())
|
Optional.ofNullable(Controllers.getStage())
|
||||||
.ifPresent(stage -> {
|
.ifPresent(Stage::show);
|
||||||
stage.setIconified(false);
|
|
||||||
stage.show();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case KEEP:
|
case KEEP:
|
||||||
@@ -832,22 +700,16 @@ public final class LauncherHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void enableAutoAgentForCurrentSetting() {
|
/**
|
||||||
GameSettings.Instance instance = setting.getInstance();
|
* The managed process listener.
|
||||||
if (instance != null
|
* Guarantee that one [JavaProcess], one [HMCLProcessListener].
|
||||||
&& instance.getOverrideProperties().contains(GameSettings.PROPERTY_ALLOW_AUTO_AGENT)) {
|
* Because every time we launched a game, we generates a new [HMCLProcessListener]
|
||||||
instance.allowAutoAgentProperty().setValue(true);
|
*/
|
||||||
} else {
|
|
||||||
setting.getPreset().allowAutoAgentProperty().setValue(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The managed process listener.
|
|
||||||
/// Guarantee that one Java [Process], one [HMCLProcessListener].
|
|
||||||
/// Because every time we launched a game, we generates a new [HMCLProcessListener]
|
|
||||||
private final class HMCLProcessListener implements ProcessListener {
|
private final class HMCLProcessListener implements ProcessListener {
|
||||||
|
|
||||||
private final ReentrantLock lock = new ReentrantLock();
|
private final ReentrantLock lock = new ReentrantLock();
|
||||||
|
private final HMCLGameRepository repository;
|
||||||
|
private final Version version;
|
||||||
private final LaunchOptions launchOptions;
|
private final LaunchOptions launchOptions;
|
||||||
private ManagedProcess process;
|
private ManagedProcess process;
|
||||||
private volatile boolean lwjgl;
|
private volatile boolean lwjgl;
|
||||||
@@ -859,7 +721,9 @@ public final class LauncherHelper {
|
|||||||
private Thread submitLogThread;
|
private Thread submitLogThread;
|
||||||
private LinkedBlockingQueue<Log> logBuffer;
|
private LinkedBlockingQueue<Log> logBuffer;
|
||||||
|
|
||||||
public HMCLProcessListener(AuthInfo authInfo, LaunchOptions launchOptions, CountDownLatch launchingLatch, boolean detectWindow) {
|
public HMCLProcessListener(HMCLGameRepository repository, Version version, AuthInfo authInfo, LaunchOptions launchOptions, CountDownLatch launchingLatch, boolean detectWindow) {
|
||||||
|
this.repository = repository;
|
||||||
|
this.version = version;
|
||||||
this.launchOptions = launchOptions;
|
this.launchOptions = launchOptions;
|
||||||
this.launchingLatch = launchingLatch;
|
this.launchingLatch = launchingLatch;
|
||||||
this.detectWindow = detectWindow;
|
this.detectWindow = detectWindow;
|
||||||
@@ -942,10 +806,8 @@ public final class LauncherHelper {
|
|||||||
runLater(() -> {
|
runLater(() -> {
|
||||||
// If application was stopped and execution services did not finish termination,
|
// If application was stopped and execution services did not finish termination,
|
||||||
// these codes will be executed.
|
// these codes will be executed.
|
||||||
@Nullable Stage stage = Controllers.getStage();
|
if (Controllers.getStage() != null) {
|
||||||
if (stage != null) {
|
Controllers.getStage().hide();
|
||||||
stage.setIconified(false);
|
|
||||||
stage.hide();
|
|
||||||
launchingLatch.countDown();
|
launchingLatch.countDown();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -986,7 +848,7 @@ public final class LauncherHelper {
|
|||||||
Log4jLevel level = isErrorStream && !log.startsWith("[authlib-injector]") ? Log4jLevel.ERROR : null;
|
Log4jLevel level = isErrorStream && !log.startsWith("[authlib-injector]") ? Log4jLevel.ERROR : null;
|
||||||
if (showLogs) {
|
if (showLogs) {
|
||||||
if (level == null)
|
if (level == null)
|
||||||
level = Objects.requireNonNullElse(Log4jLevel.guessLevel(log), Log4jLevel.INFO);
|
level = Lang.requireNonNullElse(Log4jLevel.guessLevel(log), Log4jLevel.INFO);
|
||||||
logBuffer.add(new Log(log, level));
|
logBuffer.add(new Log(log, level));
|
||||||
} else {
|
} else {
|
||||||
lock.lock();
|
lock.lock();
|
||||||
@@ -1018,7 +880,7 @@ public final class LauncherHelper {
|
|||||||
@Override
|
@Override
|
||||||
public void onExit(int exitCode, ExitType exitType) {
|
public void onExit(int exitCode, ExitType exitType) {
|
||||||
if (showLogs) {
|
if (showLogs) {
|
||||||
logBuffer.add(new Log(String.format("[%s] [HMCL ProcessListener] Minecraft exit with code %d(0x%x), type is %s.", TIME_FORMATTER.format(Instant.now()), exitCode, exitCode, exitType), Log4jLevel.INFO));
|
logBuffer.add(new Log(String.format("[HMCL ProcessListener] Minecraft exit with code %d(0x%x), type is %s.", exitCode, exitCode, exitType), Log4jLevel.INFO));
|
||||||
submitLogThread.interrupt();
|
submitLogThread.interrupt();
|
||||||
try {
|
try {
|
||||||
submitLogThread.join();
|
submitLogThread.join();
|
||||||
@@ -1044,8 +906,8 @@ public final class LauncherHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (exitType != ExitType.NORMAL) {
|
if (exitType != ExitType.NORMAL) {
|
||||||
gameInstance.markLaunchedAbnormally();
|
repository.markVersionLaunchedAbnormally(version.getId());
|
||||||
runLater(() -> new GameCrashWindow(process, exitType, gameInstance, launchOptions, logs).show());
|
runLater(() -> new GameCrashWindow(process, exitType, repository, version, launchOptions, logs).show());
|
||||||
}
|
}
|
||||||
|
|
||||||
checkExit();
|
checkExit();
|
||||||
|
|||||||
@@ -1,151 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright (C) 2022 The HMCL Authors
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
|
||||||
package org.jackhuang.hmcl.game;
|
|
||||||
|
|
||||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
|
||||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
|
||||||
import org.jackhuang.hmcl.addon.RemoteAddonRepository;
|
|
||||||
import org.jackhuang.hmcl.addon.AddonProjectInfo;
|
|
||||||
import org.jackhuang.hmcl.addon.AddonProjectInfoRegistry;
|
|
||||||
import org.jackhuang.hmcl.util.Pair;
|
|
||||||
import org.jackhuang.hmcl.util.StringUtils;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.*;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
|
||||||
|
|
||||||
public abstract class LocalizedRemoteAddonRepository implements RemoteAddonRepository {
|
|
||||||
private static final int CONTAIN_CHINESE_WEIGHT = 10;
|
|
||||||
|
|
||||||
private static final int INITIAL_CAPACITY = 16;
|
|
||||||
|
|
||||||
protected abstract RemoteAddonRepository getBackedRemoteModRepository();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getApiBaseUrl() {
|
|
||||||
return getBackedRemoteModRepository().getApiBaseUrl();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getBaseUrl() {
|
|
||||||
return getBackedRemoteModRepository().getBaseUrl();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SearchResult search(DownloadProvider downloadProvider, RemoteAddon.Type type, String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
|
|
||||||
@Nullable AddonProjectInfoRegistry<?> registry = AddonProjectInfoRegistry.getByAddonType(type);
|
|
||||||
if (registry == null || !StringUtils.containsChinese(searchFilter)) {
|
|
||||||
return getBackedRemoteModRepository().search(downloadProvider, type, gameVersion, category, pageOffset, pageSize, searchFilter, sort, sortOrder);
|
|
||||||
}
|
|
||||||
|
|
||||||
Set<String> englishSearchFiltersSet = new LinkedHashSet<>(INITIAL_CAPACITY);
|
|
||||||
|
|
||||||
int count = 0;
|
|
||||||
for (AddonProjectInfo mod : registry.search(searchFilter)) {
|
|
||||||
String englishSearchFilter = String.join(" ", StringUtils.tokenize(StringUtils.isNotBlank(mod.subName()) ? mod.subName() : mod.mainName()));
|
|
||||||
if (StringUtils.isNotBlank(englishSearchFilter)) {
|
|
||||||
englishSearchFiltersSet.add(englishSearchFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
count++;
|
|
||||||
if (count >= 3) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (englishSearchFiltersSet.isEmpty()) {
|
|
||||||
return getBackedRemoteModRepository().search(downloadProvider, type, gameVersion, category, pageOffset, pageSize, searchFilter, sort, sortOrder);
|
|
||||||
}
|
|
||||||
|
|
||||||
RemoteAddon[] searchResultArray = new RemoteAddon[pageSize];
|
|
||||||
int totalPages, chineseIndex = 0, englishIndex = pageSize - 1;
|
|
||||||
{
|
|
||||||
SearchResult searchResult = null;
|
|
||||||
List<RemoteAddon> remoteAddons = List.of();
|
|
||||||
for (String englishSearchFilter : englishSearchFiltersSet) {
|
|
||||||
searchResult = getBackedRemoteModRepository().search(downloadProvider, type, gameVersion, category, pageOffset, pageSize, englishSearchFilter, SortType.RELEVANCY, sortOrder);
|
|
||||||
remoteAddons = searchResult.results().toList();
|
|
||||||
if (!remoteAddons.isEmpty()) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (RemoteAddon remoteAddon : remoteAddons) {
|
|
||||||
if (chineseIndex > englishIndex) {
|
|
||||||
LOG.warning("Too many search results! Are the backed remote mod repository broken? Or are the API broken?");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable AddonProjectInfo chineseTranslation = registry.getInfoByCurseForgeId(remoteAddon.slug());
|
|
||||||
if (chineseTranslation != null && !StringUtils.isBlank(chineseTranslation.mainName()) && StringUtils.containsChinese(chineseTranslation.mainName())) {
|
|
||||||
searchResultArray[chineseIndex++] = remoteAddon;
|
|
||||||
} else {
|
|
||||||
searchResultArray[englishIndex--] = remoteAddon;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
totalPages = searchResult.totalPages();
|
|
||||||
}
|
|
||||||
|
|
||||||
StringUtils.LevCalculator levCalculator = new StringUtils.LevCalculator();
|
|
||||||
return new SearchResult(Stream.concat(Arrays.stream(searchResultArray, 0, chineseIndex).map(remoteMod -> {
|
|
||||||
@Nullable AddonProjectInfo chineseRemoteMod = registry.getInfoByCurseForgeId(remoteMod.slug());
|
|
||||||
if (chineseRemoteMod == null || StringUtils.isBlank(chineseRemoteMod.mainName()) || !StringUtils.containsChinese(chineseRemoteMod.mainName())) {
|
|
||||||
return Pair.pair(remoteMod, Integer.MAX_VALUE);
|
|
||||||
}
|
|
||||||
String chineseRemoteModName = chineseRemoteMod.mainName();
|
|
||||||
if (searchFilter.isEmpty() || chineseRemoteModName.isEmpty()) {
|
|
||||||
return Pair.pair(remoteMod, Math.max(searchFilter.length(), chineseRemoteModName.length()));
|
|
||||||
}
|
|
||||||
int l = levCalculator.calc(searchFilter, chineseRemoteModName);
|
|
||||||
for (int i = 0; i < searchFilter.length(); i++) {
|
|
||||||
if (chineseRemoteModName.indexOf(searchFilter.charAt(i)) >= 0) {
|
|
||||||
l -= CONTAIN_CHINESE_WEIGHT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Pair.pair(remoteMod, l);
|
|
||||||
}).sorted(Comparator.comparingInt(Pair::getValue)).map(Pair::getKey), Arrays.stream(searchResultArray, englishIndex + 1, searchResultArray.length)), totalPages);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Stream<Category> getCategories(RemoteAddon.Type type) throws IOException {
|
|
||||||
return getBackedRemoteModRepository().getCategories(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Optional<RemoteAddon.Version> getRemoteVersionByLocalFile(Path file) throws IOException {
|
|
||||||
return getBackedRemoteModRepository().getRemoteVersionByLocalFile(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public RemoteAddon getAddonById(DownloadProvider downloadProvider, String id) throws IOException {
|
|
||||||
return getBackedRemoteModRepository().getAddonById(downloadProvider, id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public RemoteAddon resolveDependency(DownloadProvider downloadProvider, String id) throws IOException {
|
|
||||||
return getBackedRemoteModRepository().resolveDependency(downloadProvider, id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public RemoteAddon.File getAddonFile(String projectId, String fileId) throws IOException {
|
|
||||||
return getBackedRemoteModRepository().getAddonFile(projectId, fileId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Stream<RemoteAddon.Version> getRemoteVersionsById(DownloadProvider downloadProvider, String id) throws IOException {
|
|
||||||
return getBackedRemoteModRepository().getRemoteVersionsById(downloadProvider, id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getAddonChangelog(DownloadProvider downloadProvider, String addonId, String versionId) throws IOException {
|
|
||||||
return getBackedRemoteModRepository().getAddonChangelog(downloadProvider, addonId, versionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NotNull String getVersionPageUrl(RemoteAddon.Version version) throws IOException {
|
|
||||||
return getBackedRemoteModRepository().getVersionPageUrl(version);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
/*
|
||||||
|
* Hello Minecraft! Launcher
|
||||||
|
* Copyright (C) 2022 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.jackhuang.hmcl.game;
|
||||||
|
|
||||||
|
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||||
|
import org.jackhuang.hmcl.mod.LocalModFile;
|
||||||
|
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||||
|
import org.jackhuang.hmcl.mod.RemoteModRepository;
|
||||||
|
import org.jackhuang.hmcl.ui.versions.ModTranslations;
|
||||||
|
import org.jackhuang.hmcl.util.Pair;
|
||||||
|
import org.jackhuang.hmcl.util.StringUtils;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||||
|
|
||||||
|
public abstract class LocalizedRemoteModRepository implements RemoteModRepository {
|
||||||
|
private static final int CONTAIN_CHINESE_WEIGHT = 10;
|
||||||
|
|
||||||
|
private static final int INITIAL_CAPACITY = 16;
|
||||||
|
|
||||||
|
protected abstract RemoteModRepository getBackedRemoteModRepository();
|
||||||
|
|
||||||
|
protected abstract SortType getBackedRemoteModRepositorySortOrder();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SearchResult search(DownloadProvider downloadProvider, String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
|
||||||
|
if (!StringUtils.containsChinese(searchFilter)) {
|
||||||
|
return getBackedRemoteModRepository().search(downloadProvider, gameVersion, category, pageOffset, pageSize, searchFilter, sort, sortOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> englishSearchFiltersSet = new HashSet<>(INITIAL_CAPACITY);
|
||||||
|
|
||||||
|
int count = 0;
|
||||||
|
for (ModTranslations.Mod mod : ModTranslations.getTranslationsByRepositoryType(getType()).searchMod(searchFilter)) {
|
||||||
|
for (String englishWord : StringUtils.tokenize(StringUtils.isNotBlank(mod.getSubname()) ? mod.getSubname() : mod.getName())) {
|
||||||
|
if (englishSearchFiltersSet.contains(englishWord)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
englishSearchFiltersSet.add(englishWord);
|
||||||
|
}
|
||||||
|
|
||||||
|
count++;
|
||||||
|
if (count >= 3) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
RemoteMod[] searchResultArray = new RemoteMod[pageSize];
|
||||||
|
int totalPages, chineseIndex = 0, englishIndex = pageSize - 1;
|
||||||
|
{
|
||||||
|
SearchResult searchResult = getBackedRemoteModRepository().search(downloadProvider, gameVersion, category, pageOffset, pageSize, String.join(" ", englishSearchFiltersSet), getBackedRemoteModRepositorySortOrder(), sortOrder);
|
||||||
|
for (Iterator<RemoteMod> iterator = searchResult.getUnsortedResults().iterator(); iterator.hasNext(); ) {
|
||||||
|
if (chineseIndex > englishIndex) {
|
||||||
|
LOG.warning("Too many search results! Are the backed remote mod repository broken? Or are the API broken?");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
RemoteMod remoteMod = iterator.next();
|
||||||
|
ModTranslations.Mod chineseTranslation = ModTranslations.getTranslationsByRepositoryType(getType()).getModByCurseForgeId(remoteMod.getSlug());
|
||||||
|
if (chineseTranslation != null && !StringUtils.isBlank(chineseTranslation.getName()) && StringUtils.containsChinese(chineseTranslation.getName())) {
|
||||||
|
searchResultArray[chineseIndex++] = remoteMod;
|
||||||
|
} else {
|
||||||
|
searchResultArray[englishIndex--] = remoteMod;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
totalPages = searchResult.getTotalPages();
|
||||||
|
}
|
||||||
|
|
||||||
|
StringUtils.LevCalculator levCalculator = new StringUtils.LevCalculator();
|
||||||
|
return new SearchResult(Stream.concat(Arrays.stream(searchResultArray, 0, chineseIndex).map(remoteMod -> {
|
||||||
|
ModTranslations.Mod chineseRemoteMod = ModTranslations.getTranslationsByRepositoryType(getType()).getModByCurseForgeId(remoteMod.getSlug());
|
||||||
|
if (chineseRemoteMod == null || StringUtils.isBlank(chineseRemoteMod.getName()) || !StringUtils.containsChinese(chineseRemoteMod.getName())) {
|
||||||
|
return Pair.pair(remoteMod, Integer.MAX_VALUE);
|
||||||
|
}
|
||||||
|
String chineseRemoteModName = chineseRemoteMod.getName();
|
||||||
|
if (searchFilter.isEmpty() || chineseRemoteModName.isEmpty()) {
|
||||||
|
return Pair.pair(remoteMod, Math.max(searchFilter.length(), chineseRemoteModName.length()));
|
||||||
|
}
|
||||||
|
int l = levCalculator.calc(searchFilter, chineseRemoteModName);
|
||||||
|
for (int i = 0; i < searchFilter.length(); i++) {
|
||||||
|
if (chineseRemoteModName.indexOf(searchFilter.charAt(i)) >= 0) {
|
||||||
|
l -= CONTAIN_CHINESE_WEIGHT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Pair.pair(remoteMod, l);
|
||||||
|
}).sorted(Comparator.comparingInt(Pair::getValue)).map(Pair::getKey), Arrays.stream(searchResultArray, englishIndex + 1, searchResultArray.length)), totalPages);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Stream<Category> getCategories() throws IOException {
|
||||||
|
return getBackedRemoteModRepository().getCategories();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<RemoteMod.Version> getRemoteVersionByLocalFile(LocalModFile localModFile, Path file) throws IOException {
|
||||||
|
return getBackedRemoteModRepository().getRemoteVersionByLocalFile(localModFile, file);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RemoteMod getModById(String id) throws IOException {
|
||||||
|
return getBackedRemoteModRepository().getModById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RemoteMod.File getModFile(String modId, String fileId) throws IOException {
|
||||||
|
return getBackedRemoteModRepository().getModFile(modId, fileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Stream<RemoteMod.Version> getRemoteVersionsById(String id) throws IOException {
|
||||||
|
return getBackedRemoteModRepository().getRemoteVersionsById(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +1,37 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright (C) 2024 The HMCL Authors
|
/*
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
* Hello Minecraft! Launcher
|
||||||
|
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
package org.jackhuang.hmcl.game;
|
package org.jackhuang.hmcl.game;
|
||||||
|
|
||||||
import org.jackhuang.hmcl.util.Log4jLevel;
|
import org.jackhuang.hmcl.util.Log4jLevel;
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.setting.SettingsManager.settings;
|
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||||
|
|
||||||
public final class Log {
|
public final class Log {
|
||||||
public static final int DEFAULT_LOG_LINES = 2000;
|
public static final int DEFAULT_LOG_LINES = 2000;
|
||||||
|
|
||||||
public static int getLogLines() {
|
public static int getLogLines() {
|
||||||
Integer lines = settings().logLinesProperty().get();
|
Integer lines = config().getLogLines();
|
||||||
return lines != null && lines > 0 ? lines : DEFAULT_LOG_LINES;
|
return lines != null && lines > 0 ? lines : DEFAULT_LOG_LINES;
|
||||||
}
|
}
|
||||||
|
|
||||||
private final String log;
|
private final String log;
|
||||||
private Log4jLevel level;
|
private Log4jLevel level;
|
||||||
|
private boolean selected = false;
|
||||||
|
|
||||||
public Log(String log) {
|
public Log(String log) {
|
||||||
this.log = log;
|
this.log = log;
|
||||||
@@ -41,6 +57,14 @@ public final class Log {
|
|||||||
return level;
|
return level;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isSelected() {
|
||||||
|
return selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSelected(boolean selected) {
|
||||||
|
this.selected = selected;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return log;
|
return log;
|
||||||
|
|||||||
@@ -1,23 +1,37 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors
|
/*
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
* Hello Minecraft! Launcher
|
||||||
|
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
package org.jackhuang.hmcl.game;
|
package org.jackhuang.hmcl.game;
|
||||||
|
|
||||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
|
||||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||||
import org.jackhuang.hmcl.util.io.Zipper;
|
|
||||||
import org.jackhuang.hmcl.util.logging.Logger;
|
import org.jackhuang.hmcl.util.logging.Logger;
|
||||||
|
import org.jackhuang.hmcl.util.StringUtils;
|
||||||
|
import org.jackhuang.hmcl.util.io.Zipper;
|
||||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStreamWriter;
|
|
||||||
import java.io.UncheckedIOException;
|
import java.io.UncheckedIOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.lang.management.ManagementFactory;
|
||||||
import java.nio.file.DirectoryStream;
|
import java.nio.file.DirectoryStream;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.PathMatcher;
|
import java.nio.file.attribute.BasicFileAttributes;
|
||||||
|
import java.nio.file.attribute.FileTime;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -29,47 +43,41 @@ public final class LogExporter {
|
|||||||
private LogExporter() {
|
private LogExporter() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static CompletableFuture<Void> exportLogs(
|
public static CompletableFuture<Void> exportLogs(Path zipFile, DefaultGameRepository gameRepository, String versionId, String logs, String launchScript) {
|
||||||
Path zipFile, DefaultGameInstance instance, LaunchOptions options, String logs, String launchScript,
|
Path runDirectory = gameRepository.getRunDirectory(versionId);
|
||||||
PathMatcher logMatcher) {
|
Path baseDirectory = gameRepository.getBaseDirectory();
|
||||||
DefaultGameRepositorySnapshot repositorySnapshot = instance.getSnapshot();
|
List<String> versions = new ArrayList<>();
|
||||||
Path runDirectory = options.getGameDir();
|
|
||||||
List<GameInstanceID> instances = new ArrayList<>();
|
|
||||||
|
|
||||||
GameInstanceID currentInstanceId = instance.id;
|
String currentVersionId = versionId;
|
||||||
HashSet<GameInstanceID> resolvedSoFar = new HashSet<>();
|
HashSet<String> resolvedSoFar = new HashSet<>();
|
||||||
while (true) {
|
while (true) {
|
||||||
if (resolvedSoFar.contains(currentInstanceId)) break;
|
if (resolvedSoFar.contains(currentVersionId)) break;
|
||||||
resolvedSoFar.add(currentInstanceId);
|
resolvedSoFar.add(currentVersionId);
|
||||||
@Nullable DefaultGameInstance currentInstance = repositorySnapshot.get(currentInstanceId);
|
Version currentVersion = gameRepository.getVersion(currentVersionId);
|
||||||
if (currentInstance == null)
|
versions.add(currentVersionId);
|
||||||
break;
|
|
||||||
instances.add(currentInstanceId);
|
|
||||||
|
|
||||||
if (currentInstance.getManifest().inheritsFrom() != null) {
|
if (StringUtils.isNotBlank(currentVersion.getInheritsFrom())) {
|
||||||
currentInstanceId = currentInstance.getManifest().inheritsFrom();
|
currentVersionId = currentVersion.getInheritsFrom();
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return CompletableFuture.runAsync(() -> {
|
return CompletableFuture.runAsync(() -> {
|
||||||
try (Zipper zipper = new Zipper(zipFile, true)) {
|
try (Zipper zipper = new Zipper(zipFile)) {
|
||||||
processLogs(runDirectory.resolve("liteconfig"), "*.log", "liteconfig", zipper, logMatcher);
|
processLogs(runDirectory.resolve("liteconfig"), "*.log", "liteconfig", zipper);
|
||||||
processLogs(runDirectory.resolve("logs"), "*.log", "logs", zipper, logMatcher);
|
processLogs(runDirectory.resolve("logs"), "*.log", "logs", zipper);
|
||||||
processLogs(runDirectory, "*.log", "runDirectory", zipper, logMatcher);
|
processLogs(runDirectory, "*.log", "runDirectory", zipper);
|
||||||
processLogs(runDirectory.resolve("crash-reports"), "*.txt", "crash-reports", zipper, logMatcher);
|
processLogs(runDirectory.resolve("crash-reports"), "*.txt", "crash-reports", zipper);
|
||||||
|
|
||||||
zipper.putTextFile(LOG.getLogs(), "hmcl.log");
|
zipper.putTextFile(LOG.getLogs(), "hmcl.log");
|
||||||
zipper.putTextFile(logs, "minecraft.log");
|
zipper.putTextFile(logs, "minecraft.log");
|
||||||
zipper.putTextFile(Logger.filterForbiddenToken(launchScript), OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "launch.bat" : "launch.sh");
|
zipper.putTextFile(Logger.filterForbiddenToken(launchScript), OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "launch.bat" : "launch.sh");
|
||||||
|
|
||||||
for (GameInstanceID id : instances) {
|
for (String id : versions) {
|
||||||
@Nullable DefaultGameInstance currentInstance = repositorySnapshot.get(id);
|
Path versionJson = baseDirectory.resolve("versions").resolve(id).resolve(id + ".json");
|
||||||
if (currentInstance != null) {
|
if (Files.exists(versionJson)) {
|
||||||
try (var writer = new OutputStreamWriter(zipper.putStream(id + ".json"), StandardCharsets.UTF_8)) {
|
zipper.putFile(versionJson, id + ".json");
|
||||||
JsonUtils.GSON.toJson(currentInstance.getManifest(), writer);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@@ -78,11 +86,14 @@ public final class LogExporter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void processLogs(Path directory, String fileExtension, String logDirectory, Zipper zipper, PathMatcher logMatcher) {
|
private static void processLogs(Path directory, String fileExtension, String logDirectory, Zipper zipper) {
|
||||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory, fileExtension)) {
|
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory, fileExtension)) {
|
||||||
|
long processStartTime = ManagementFactory.getRuntimeMXBean().getStartTime();
|
||||||
|
|
||||||
for (Path file : stream) {
|
for (Path file : stream) {
|
||||||
if (Files.isRegularFile(file)) {
|
if (Files.isRegularFile(file)) {
|
||||||
if (logMatcher == null || logMatcher.matches(file)) {
|
FileTime time = Files.readAttributes(file, BasicFileAttributes.class).lastModifiedTime();
|
||||||
|
if (time.toMillis() >= processStartTime) {
|
||||||
try (BufferedReader reader = IOUtils.newBufferedReaderMaybeNativeEncoding(file)) {
|
try (BufferedReader reader = IOUtils.newBufferedReaderMaybeNativeEncoding(file)) {
|
||||||
zipper.putLines(reader.lines().map(Logger::filterForbiddenToken), file.getFileName().toString());
|
zipper.putLines(reader.lines().map(Logger::filterForbiddenToken), file.getFileName().toString());
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
|||||||
@@ -1,5 +1,20 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors
|
/*
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
* Hello Minecraft! Launcher
|
||||||
|
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
package org.jackhuang.hmcl.game;
|
package org.jackhuang.hmcl.game;
|
||||||
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
|||||||
@@ -1,38 +1,61 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors
|
/*
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
* Hello Minecraft! Launcher
|
||||||
|
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
package org.jackhuang.hmcl.game;
|
package org.jackhuang.hmcl.game;
|
||||||
|
|
||||||
|
import org.jackhuang.hmcl.setting.Profile;
|
||||||
import org.jackhuang.hmcl.task.Task;
|
import org.jackhuang.hmcl.task.Task;
|
||||||
|
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||||
import org.jackhuang.hmcl.util.io.Unzipper;
|
import org.jackhuang.hmcl.util.io.Unzipper;
|
||||||
|
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.nio.file.FileSystem;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
|
||||||
|
|
||||||
public class ManuallyCreatedModpackInstallTask extends Task<Path> {
|
public class ManuallyCreatedModpackInstallTask extends Task<Path> {
|
||||||
|
|
||||||
|
private final Profile profile;
|
||||||
private final Path zipFile;
|
private final Path zipFile;
|
||||||
|
private final Charset charset;
|
||||||
private final String name;
|
private final String name;
|
||||||
|
|
||||||
public ManuallyCreatedModpackInstallTask(Path zipFile, String name) {
|
public ManuallyCreatedModpackInstallTask(Profile profile, Path zipFile, Charset charset, String name) {
|
||||||
|
this.profile = profile;
|
||||||
this.zipFile = zipFile;
|
this.zipFile = zipFile;
|
||||||
|
this.charset = charset;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
|
|
||||||
setName(i18n("modpack.installing"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void execute() throws Exception {
|
public void execute() throws Exception {
|
||||||
String subdirectory = ModpackHelper.findMinecraftDirectoryInManuallyCreatedModpack(zipFile.toString(), zipFile);
|
Path subdirectory;
|
||||||
|
try (FileSystem fs = CompressingUtils.readonly(zipFile).setEncoding(charset).build()) {
|
||||||
|
subdirectory = ModpackHelper.findMinecraftDirectoryInManuallyCreatedModpack(zipFile.toString(), fs);
|
||||||
|
}
|
||||||
|
|
||||||
Path dest = Paths.get("externalgames").resolve(name);
|
Path dest = Paths.get("externalgames").resolve(name);
|
||||||
|
|
||||||
setResult(dest);
|
setResult(dest);
|
||||||
|
|
||||||
new Unzipper(zipFile, dest)
|
new Unzipper(zipFile, dest)
|
||||||
.setSubDirectory(subdirectory)
|
.setSubDirectory(subdirectory.toString())
|
||||||
.setTerminateIfSubDirectoryNotExists()
|
.setTerminateIfSubDirectoryNotExists()
|
||||||
|
.setEncoding(charset)
|
||||||
.unzip();
|
.unzip();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,52 +1,63 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
|
/*
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
* Hello Minecraft! Launcher
|
||||||
|
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
package org.jackhuang.hmcl.game;
|
package org.jackhuang.hmcl.game;
|
||||||
|
|
||||||
import com.google.gson.JsonParseException;
|
import com.google.gson.JsonParseException;
|
||||||
import kala.compress.archivers.zip.ZipArchiveReader;
|
import kala.compress.archivers.zip.ZipArchiveReader;
|
||||||
import org.jackhuang.hmcl.Metadata;
|
import org.jackhuang.hmcl.Metadata;
|
||||||
import org.jackhuang.hmcl.modpack.*;
|
import org.jackhuang.hmcl.mod.*;
|
||||||
import org.jackhuang.hmcl.modpack.curse.CurseModpackProvider;
|
import org.jackhuang.hmcl.mod.curse.CurseModpackProvider;
|
||||||
import org.jackhuang.hmcl.modpack.mcbbs.McbbsModpackManifest;
|
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackManifest;
|
||||||
import org.jackhuang.hmcl.modpack.mcbbs.McbbsModpackProvider;
|
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackProvider;
|
||||||
import org.jackhuang.hmcl.modpack.modrinth.ModrinthModpackProvider;
|
import org.jackhuang.hmcl.mod.modrinth.ModrinthModpackProvider;
|
||||||
import org.jackhuang.hmcl.modpack.multimc.MultiMCComponents;
|
import org.jackhuang.hmcl.mod.multimc.MultiMCComponents;
|
||||||
import org.jackhuang.hmcl.modpack.multimc.MultiMCInstanceConfiguration;
|
import org.jackhuang.hmcl.mod.multimc.MultiMCInstanceConfiguration;
|
||||||
import org.jackhuang.hmcl.modpack.multimc.MultiMCModpackProvider;
|
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackProvider;
|
||||||
import org.jackhuang.hmcl.modpack.server.ServerModpackManifest;
|
import org.jackhuang.hmcl.mod.server.ServerModpackManifest;
|
||||||
import org.jackhuang.hmcl.modpack.server.ServerModpackProvider;
|
import org.jackhuang.hmcl.mod.server.ServerModpackProvider;
|
||||||
import org.jackhuang.hmcl.modpack.server.ServerModpackRemoteInstallTask;
|
import org.jackhuang.hmcl.mod.server.ServerModpackRemoteInstallTask;
|
||||||
import org.jackhuang.hmcl.setting.*;
|
import org.jackhuang.hmcl.setting.Profile;
|
||||||
|
import org.jackhuang.hmcl.setting.Profiles;
|
||||||
|
import org.jackhuang.hmcl.setting.VersionSetting;
|
||||||
import org.jackhuang.hmcl.task.Schedulers;
|
import org.jackhuang.hmcl.task.Schedulers;
|
||||||
import org.jackhuang.hmcl.task.Task;
|
import org.jackhuang.hmcl.task.Task;
|
||||||
import org.jackhuang.hmcl.util.Lang;
|
import org.jackhuang.hmcl.util.Lang;
|
||||||
import org.jackhuang.hmcl.util.PortablePath;
|
|
||||||
import org.jackhuang.hmcl.util.function.ExceptionalConsumer;
|
import org.jackhuang.hmcl.util.function.ExceptionalConsumer;
|
||||||
import org.jackhuang.hmcl.util.function.ExceptionalRunnable;
|
import org.jackhuang.hmcl.util.function.ExceptionalRunnable;
|
||||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||||
import org.jackhuang.hmcl.util.i18n.LocalizedText;
|
|
||||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||||
import org.jackhuang.hmcl.util.tree.ArchiveFileTree;
|
|
||||||
import org.jetbrains.annotations.NotNullByDefault;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.nio.file.FileSystem;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.List;
|
import java.util.*;
|
||||||
import java.util.Map;
|
import java.util.stream.Stream;
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||||
|
import static org.jackhuang.hmcl.util.Lang.toIterable;
|
||||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||||
|
|
||||||
/// Utilities for reading, installing, and applying modpack-specific game settings.
|
|
||||||
@NotNullByDefault
|
|
||||||
public final class ModpackHelper {
|
public final class ModpackHelper {
|
||||||
private ModpackHelper() {
|
private ModpackHelper() {
|
||||||
}
|
}
|
||||||
@@ -61,7 +72,7 @@ public final class ModpackHelper {
|
|||||||
);
|
);
|
||||||
|
|
||||||
static {
|
static {
|
||||||
MultiMCComponents.setImplementation(Metadata.FULL_NAME + " v" + Metadata.VERSION);
|
MultiMCComponents.setImplementation(Metadata.FULL_TITLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@@ -74,8 +85,8 @@ public final class ModpackHelper {
|
|||||||
return "zip".equals(ext) || "mrpack".equals(ext);
|
return "zip".equals(ext) || "mrpack".equals(ext);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Modpack readModpackManifest(Path file) throws UnsupportedModpackException, ManuallyCreatedModpackException {
|
public static Modpack readModpackManifest(Path file, Charset charset) throws UnsupportedModpackException, ManuallyCreatedModpackException {
|
||||||
try (ZipArchiveReader zipFile = CompressingUtils.openZipFile(file)) {
|
try (ZipArchiveReader zipFile = CompressingUtils.openZipFile(file, charset)) {
|
||||||
// Order for trying detecting manifest is necessary here.
|
// Order for trying detecting manifest is necessary here.
|
||||||
// Do not change to iterating providers.
|
// Do not change to iterating providers.
|
||||||
for (ModpackProvider provider : new ModpackProvider[]{
|
for (ModpackProvider provider : new ModpackProvider[]{
|
||||||
@@ -86,15 +97,15 @@ public final class ModpackHelper {
|
|||||||
MultiMCModpackProvider.INSTANCE,
|
MultiMCModpackProvider.INSTANCE,
|
||||||
ServerModpackProvider.INSTANCE}) {
|
ServerModpackProvider.INSTANCE}) {
|
||||||
try {
|
try {
|
||||||
return provider.readManifest(zipFile, file);
|
return provider.readManifest(zipFile, file, charset);
|
||||||
} catch (Exception ignored) {
|
} catch (Exception ignored) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (IOException ignored) {
|
} catch (IOException ignored) {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(file, charset)) {
|
||||||
findMinecraftDirectoryInManuallyCreatedModpack(file.toString(), file);
|
findMinecraftDirectoryInManuallyCreatedModpack(file.toString(), fs);
|
||||||
throw new ManuallyCreatedModpackException(file);
|
throw new ManuallyCreatedModpackException(file);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
// ignore it
|
// ignore it
|
||||||
@@ -103,35 +114,28 @@ public final class ModpackHelper {
|
|||||||
throw new UnsupportedModpackException(file.toString());
|
throw new UnsupportedModpackException(file.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String findMinecraftDirectoryInManuallyCreatedModpack(String modpackName, Path zipPath)
|
public static Path findMinecraftDirectoryInManuallyCreatedModpack(String modpackName, FileSystem fs) throws IOException, UnsupportedModpackException {
|
||||||
throws IOException, UnsupportedModpackException {
|
Path root = fs.getPath("/");
|
||||||
|
if (isMinecraftDirectory(root)) return root;
|
||||||
|
try (Stream<Path> firstLayer = Files.list(root)) {
|
||||||
|
for (Path dir : toIterable(firstLayer)) {
|
||||||
|
if (isMinecraftDirectory(dir)) return dir;
|
||||||
|
|
||||||
try (ArchiveFileTree<?, ?> tree = ArchiveFileTree.open(zipPath)) {
|
try (Stream<Path> secondLayer = Files.list(dir)) {
|
||||||
ArchiveFileTree.Dir<?> rootDir = tree.getRoot();
|
for (Path subdir : toIterable(secondLayer)) {
|
||||||
|
if (isMinecraftDirectory(subdir)) return subdir;
|
||||||
if (isMinecraftDirectory(rootDir)) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
for (ArchiveFileTree.Dir<?> firstLayer : rootDir.getSubDirs().values()) {
|
|
||||||
if (isMinecraftDirectory(firstLayer)) {
|
|
||||||
return firstLayer.getName();
|
|
||||||
}
|
|
||||||
|
|
||||||
for (ArchiveFileTree.Dir<?> secondLayer : firstLayer.getSubDirs().values()) {
|
|
||||||
if (isMinecraftDirectory(secondLayer)) {
|
|
||||||
return firstLayer.getName() + "/" + secondLayer.getName();
|
|
||||||
}
|
}
|
||||||
|
} catch (IOException ignored) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (IOException ignored) {
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new UnsupportedModpackException(modpackName);
|
throw new UnsupportedModpackException(modpackName);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isMinecraftDirectory(ArchiveFileTree.Dir<?> dir) {
|
private static boolean isMinecraftDirectory(Path path) {
|
||||||
return dir.getSubDirs().containsKey("versions") && (dir.isRoot() || ".minecraft".equals(dir.getName()));
|
return Files.isDirectory(path.resolve("versions")) &&
|
||||||
|
(path.getFileName() == null || ".minecraft".equals(FileUtils.getName(path)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ModpackConfiguration<?> readModpackConfiguration(Path file) throws IOException {
|
public static ModpackConfiguration<?> readModpackConfiguration(Path file) throws IOException {
|
||||||
@@ -142,10 +146,16 @@ public final class ModpackHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Task<?> getInstallTask(HMCLGameRepository repository, ServerModpackManifest manifest, GameInstanceID instanceId, Modpack modpack) {
|
public static Task<?> getInstallTask(Profile profile, ServerModpackManifest manifest, String name, Modpack modpack) {
|
||||||
|
profile.getRepository().markVersionAsModpack(name);
|
||||||
|
|
||||||
ExceptionalRunnable<?> success = () -> {
|
ExceptionalRunnable<?> success = () -> {
|
||||||
repository.refresh();
|
HMCLGameRepository repository = profile.getRepository();
|
||||||
repository.getInstance(instanceId).enableIsolation();
|
repository.refreshVersions();
|
||||||
|
VersionSetting vs = repository.specializeVersionSetting(name);
|
||||||
|
repository.undoMark(name);
|
||||||
|
if (vs != null)
|
||||||
|
vs.setGameDirType(GameDirectoryType.VERSION_FOLDER);
|
||||||
};
|
};
|
||||||
|
|
||||||
ExceptionalConsumer<Exception, ?> failure = ex -> {
|
ExceptionalConsumer<Exception, ?> failure = ex -> {
|
||||||
@@ -155,59 +165,39 @@ public final class ModpackHelper {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return new ServerModpackRemoteInstallTask(repository.getDependency(), manifest, instanceId)
|
return new ServerModpackRemoteInstallTask(profile.getDependency(), manifest, name)
|
||||||
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
||||||
.withStage("hmcl.modpack")
|
.withStagesHint(Arrays.asList("hmcl.modpack", "hmcl.modpack.download"));
|
||||||
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isExternalGameNameConflicts(String name) {
|
public static boolean isExternalGameNameConflicts(String name) {
|
||||||
return Files.exists(Paths.get("externalgames").resolve(name));
|
return Files.exists(Paths.get("externalgames").resolve(name));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extracts a manually created modpack and selects its new external game directory.
|
public static Task<?> getInstallManuallyCreatedModpackTask(Profile profile, Path zipFile, String name, Charset charset) {
|
||||||
public static Task<?> getInstallManuallyCreatedModpackTask(Path zipFile, String name) {
|
|
||||||
if (isExternalGameNameConflicts(name)) {
|
if (isExternalGameNameConflicts(name)) {
|
||||||
throw new IllegalArgumentException("name existing");
|
throw new IllegalArgumentException("name existing");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ManuallyCreatedModpackInstallTask(zipFile, name)
|
return new ManuallyCreatedModpackInstallTask(profile, zipFile, charset, name)
|
||||||
.thenAcceptAsync(Schedulers.javafx(), location -> {
|
.thenAcceptAsync(Schedulers.javafx(), location -> {
|
||||||
GameDirectory newGameDirectory = new GameDirectory(
|
Profile newProfile = new Profile(name, location);
|
||||||
GameDirectoryManager.newGameDirectoryId(),
|
newProfile.setUseRelativePath(true);
|
||||||
LocalizedText.plain(name),
|
Profiles.getProfiles().add(newProfile);
|
||||||
PortablePath.fromPath(location));
|
Profiles.setSelectedProfile(newProfile);
|
||||||
GameDirectoryManager.addLocalGameDirectory(newGameDirectory);
|
});
|
||||||
GameDirectoryManager.setSelectedGameDirectory(newGameDirectory);
|
|
||||||
})
|
|
||||||
.withStage("hmcl.modpack")
|
|
||||||
.withStagesHints("hmcl.modpack");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Task<?> getInstallTask(HMCLGameRepository repository, Path zipFile, GameInstanceID instanceId, Modpack modpack, @Nullable String iconUrl) {
|
public static Task<?> getInstallTask(Profile profile, Path zipFile, String name, Modpack modpack) {
|
||||||
return getInstallTask(repository, zipFile, instanceId, modpack, iconUrl, null);
|
profile.getRepository().markVersionAsModpack(name);
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates an install task that respects optional-file selection.
|
|
||||||
///
|
|
||||||
/// @param repository the target repository
|
|
||||||
/// @param zipFile the modpack archive
|
|
||||||
/// @param instanceId the new instance id
|
|
||||||
/// @param modpack the parsed modpack
|
|
||||||
/// @param iconUrl the optional icon URL, or `null`
|
|
||||||
/// @param excludedFiles keys of optional files the user chose not to install; `null` means install all.
|
|
||||||
/// When non-null, must not contain `null` elements.
|
|
||||||
/// @return the install task
|
|
||||||
public static Task<?> getInstallTask(
|
|
||||||
HMCLGameRepository repository,
|
|
||||||
Path zipFile,
|
|
||||||
GameInstanceID instanceId,
|
|
||||||
Modpack modpack,
|
|
||||||
@Nullable String iconUrl,
|
|
||||||
@Nullable Set<String> excludedFiles) {
|
|
||||||
ExceptionalRunnable<?> success = () -> {
|
ExceptionalRunnable<?> success = () -> {
|
||||||
repository.refresh();
|
HMCLGameRepository repository = profile.getRepository();
|
||||||
repository.getInstance(instanceId).enableIsolation();
|
repository.refreshVersions();
|
||||||
|
VersionSetting vs = repository.specializeVersionSetting(name);
|
||||||
|
repository.undoMark(name);
|
||||||
|
if (vs != null)
|
||||||
|
vs.setGameDirType(GameDirectoryType.VERSION_FOLDER);
|
||||||
};
|
};
|
||||||
|
|
||||||
ExceptionalConsumer<Exception, ?> failure = ex -> {
|
ExceptionalConsumer<Exception, ?> failure = ex -> {
|
||||||
@@ -218,181 +208,111 @@ public final class ModpackHelper {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
|
if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
|
||||||
return modpack.getInstallTask(repository.getDependency(), zipFile, instanceId, iconUrl, excludedFiles)
|
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
|
||||||
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
||||||
.thenComposeAsync(createMultiMCPostInstallTask(repository, (MultiMCInstanceConfiguration) modpack.getManifest(), instanceId))
|
.thenComposeAsync(createMultiMCPostInstallTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name))
|
||||||
.withStage("hmcl.modpack")
|
.withStagesHint(List.of("hmcl.modpack", "hmcl.modpack.download"));
|
||||||
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
|
|
||||||
else if (modpack.getManifest() instanceof McbbsModpackManifest)
|
else if (modpack.getManifest() instanceof McbbsModpackManifest)
|
||||||
return modpack.getInstallTask(repository.getDependency(), zipFile, instanceId, iconUrl, excludedFiles)
|
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
|
||||||
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
||||||
.thenComposeAsync(createMcbbsPostInstallTask(repository, (McbbsModpackManifest) modpack.getManifest(), instanceId))
|
.thenComposeAsync(createMcbbsPostInstallTask(profile, (McbbsModpackManifest) modpack.getManifest(), name))
|
||||||
.withStage("hmcl.modpack")
|
.withStagesHint(List.of("hmcl.modpack", "hmcl.modpack.download"));
|
||||||
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
|
|
||||||
else
|
else
|
||||||
return modpack.getInstallTask(repository.getDependency(), zipFile, instanceId, iconUrl, excludedFiles)
|
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
|
||||||
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
.whenComplete(Schedulers.javafx(), success, failure)
|
||||||
.withStage("hmcl.modpack")
|
.withStagesHint(List.of("hmcl.modpack", "hmcl.modpack.download"));
|
||||||
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates a server modpack with backup, repository refresh, and modpack stage hints.
|
public static Task<Void> getUpdateTask(Profile profile, ServerModpackManifest manifest, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException {
|
||||||
public static Task<Void> getUpdateTask(HMCLGameRepository repository, ServerModpackManifest manifest, GameInstanceID instanceId, ModpackConfiguration<?> configuration) throws UnsupportedModpackException {
|
|
||||||
switch (configuration.getType()) {
|
switch (configuration.getType()) {
|
||||||
case ServerModpackRemoteInstallTask.MODPACK_TYPE: {
|
case ServerModpackRemoteInstallTask.MODPACK_TYPE:
|
||||||
HMCLGameInstance instance = repository.getInstance(instanceId);
|
return new ModpackUpdateTask(profile.getRepository(), name, new ServerModpackRemoteInstallTask(profile.getDependency(), manifest, name))
|
||||||
return new ModpackUpdateTask(
|
.withStagesHint(Arrays.asList("hmcl.modpack", "hmcl.modpack.download"));
|
||||||
instance,
|
|
||||||
new ServerModpackRemoteInstallTask(repository.getDependency(), manifest, instance))
|
|
||||||
.thenComposeAsync(repository.refreshAsync())
|
|
||||||
.withStage("hmcl.modpack")
|
|
||||||
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
throw new UnsupportedModpackException();
|
throw new UnsupportedModpackException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Task<?> getUpdateTask(HMCLGameRepository repository, Path zipFile, GameInstanceID instanceId, ModpackConfiguration<?> configuration) throws UnsupportedModpackException, ManuallyCreatedModpackException, MismatchedModpackTypeException {
|
public static Task<?> getUpdateTask(Profile profile, Path zipFile, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException, ManuallyCreatedModpackException, MismatchedModpackTypeException {
|
||||||
return getUpdateTask(repository, zipFile, instanceId, configuration, null);
|
Modpack modpack = ModpackHelper.readModpackManifest(zipFile, charset);
|
||||||
}
|
ModpackProvider provider = getProviderByType(configuration.getType());
|
||||||
|
|
||||||
/// Creates an update task that respects optional-file selection.
|
|
||||||
///
|
|
||||||
/// @param repository the target repository
|
|
||||||
/// @param zipFile the modpack archive
|
|
||||||
/// @param instanceId the instance to update
|
|
||||||
/// @param configuration the existing modpack configuration
|
|
||||||
/// @param excludedFiles keys of optional files the user chose not to install; `null` means install all.
|
|
||||||
/// When non-null, must not contain `null` elements.
|
|
||||||
/// @return the update task
|
|
||||||
public static Task<?> getUpdateTask(
|
|
||||||
HMCLGameRepository repository,
|
|
||||||
Path zipFile,
|
|
||||||
GameInstanceID instanceId,
|
|
||||||
ModpackConfiguration<?> configuration,
|
|
||||||
@Nullable Set<String> excludedFiles)
|
|
||||||
throws UnsupportedModpackException, ManuallyCreatedModpackException, MismatchedModpackTypeException {
|
|
||||||
Modpack modpack = ModpackHelper.readModpackManifest(zipFile);
|
|
||||||
@Nullable ModpackProvider provider = getProviderByType(configuration.getType());
|
|
||||||
if (provider == null) {
|
if (provider == null) {
|
||||||
throw new UnsupportedModpackException();
|
throw new UnsupportedModpackException();
|
||||||
}
|
}
|
||||||
if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
|
if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
|
||||||
return provider.createUpdateTask(repository.getDependency(), repository.getInstance(instanceId), zipFile, modpack, excludedFiles)
|
return provider.createUpdateTask(profile.getDependency(), name, zipFile, modpack)
|
||||||
.thenComposeAsync(() -> createMultiMCPostUpdateTask(repository, (MultiMCInstanceConfiguration) modpack.getManifest(), instanceId))
|
.thenComposeAsync(() -> createMultiMCPostUpdateTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name));
|
||||||
.thenComposeAsync(repository.refreshAsync())
|
|
||||||
.withStage("hmcl.modpack")
|
|
||||||
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
|
|
||||||
else
|
else
|
||||||
return provider.createUpdateTask(repository.getDependency(), repository.getInstance(instanceId), zipFile, modpack, excludedFiles)
|
return provider.createUpdateTask(profile.getDependency(), name, zipFile, modpack);
|
||||||
.thenComposeAsync(repository.refreshAsync())
|
|
||||||
.withStage("hmcl.modpack")
|
|
||||||
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void toGameSettings(MultiMCInstanceConfiguration c, GameSettings.Instance setting) {
|
public static void toVersionSetting(MultiMCInstanceConfiguration c, VersionSetting vs) {
|
||||||
setting.getOverrideProperties().add(GameSettings.PROPERTY_RUNNING_DIRECTORY);
|
vs.setUsesGlobal(false);
|
||||||
|
vs.setGameDirType(GameDirectoryType.VERSION_FOLDER);
|
||||||
|
|
||||||
if (c.isOverrideJavaLocation()) {
|
if (c.isOverrideJavaLocation()) {
|
||||||
setting.getOverrideProperties().add(GameSettings.PROPERTY_JAVA_TYPE);
|
vs.setJavaDir(Lang.nonNull(c.getJavaPath(), ""));
|
||||||
setting.getOverrideProperties().add(GameSettings.PROPERTY_CUSTOM_JAVA_PATH);
|
|
||||||
setting.javaTypeProperty().setValue(JavaVersionType.CUSTOM);
|
|
||||||
setting.customJavaPathProperty().setValue(Objects.requireNonNullElse(c.getJavaPath(), ""));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (c.isOverrideMemory()) {
|
if (c.isOverrideMemory()) {
|
||||||
setting.getOverrideProperties().addAll(List.of(
|
vs.setPermSize(Optional.ofNullable(c.getPermGen()).map(Object::toString).orElse(""));
|
||||||
GameSettings.PROPERTY_AUTO_MEMORY,
|
|
||||||
GameSettings.PROPERTY_PERM_SIZE,
|
|
||||||
GameSettings.PROPERTY_MAX_MEMORY,
|
|
||||||
GameSettings.PROPERTY_MIN_MEMORY
|
|
||||||
));
|
|
||||||
setting.permSizeProperty().setValue(Optional.ofNullable(c.getPermGen()).map(Object::toString).orElse(""));
|
|
||||||
if (c.getMaxMemory() != null)
|
if (c.getMaxMemory() != null)
|
||||||
setting.maxMemoryProperty().setValue(c.getMaxMemory());
|
vs.setMaxMemory(c.getMaxMemory());
|
||||||
setting.minMemoryProperty().setValue(c.getMinMemory());
|
vs.setMinMemory(c.getMinMemory());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (c.isOverrideCommands()) {
|
if (c.isOverrideCommands()) {
|
||||||
setting.getOverrideProperties().addAll(List.of(
|
vs.setWrapper(Lang.nonNull(c.getWrapperCommand(), ""));
|
||||||
GameSettings.PROPERTY_COMMAND_WRAPPER,
|
vs.setPreLaunchCommand(Lang.nonNull(c.getPreLaunchCommand(), ""));
|
||||||
GameSettings.PROPERTY_PRE_LAUNCH_COMMAND
|
|
||||||
));
|
|
||||||
setting.commandWrapperProperty().setValue(Objects.requireNonNullElse(c.getWrapperCommand(), ""));
|
|
||||||
setting.preLaunchCommandProperty().setValue(Objects.requireNonNullElse(c.getPreLaunchCommand(), ""));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (c.isOverrideJavaArgs()) {
|
if (c.isOverrideJavaArgs()) {
|
||||||
setting.getOverrideProperties().add(GameSettings.PROPERTY_JVM_OPTIONS);
|
vs.setJavaArgs(Lang.nonNull(c.getJvmArgs(), ""));
|
||||||
setting.jvmOptionsProperty().setValue(Objects.requireNonNullElse(c.getJvmArgs(), ""));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (c.isOverrideConsole()) {
|
if (c.isOverrideConsole()) {
|
||||||
setting.getOverrideProperties().add(GameSettings.PROPERTY_SHOW_LOGS);
|
vs.setShowLogs(c.isShowConsole());
|
||||||
setting.showLogsProperty().setValue(c.isShowConsole());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (c.isOverrideWindow()) {
|
if (c.isOverrideWindow()) {
|
||||||
setting.getOverrideProperties().addAll(List.of(
|
vs.setFullscreen(c.isFullscreen());
|
||||||
GameSettings.PROPERTY_WINDOW_TYPE,
|
|
||||||
GameSettings.PROPERTY_WIDTH,
|
|
||||||
GameSettings.PROPERTY_HEIGHT
|
|
||||||
));
|
|
||||||
setting.windowTypeProperty().setValue(c.isFullscreen() ? GameWindowType.FULLSCREEN : GameWindowType.WINDOWED);
|
|
||||||
if (c.getWidth() != null)
|
if (c.getWidth() != null)
|
||||||
setting.widthProperty().setValue(c.getWidth().doubleValue());
|
vs.setWidth(c.getWidth());
|
||||||
if (c.getHeight() != null)
|
if (c.getHeight() != null)
|
||||||
setting.heightProperty().setValue(c.getHeight().doubleValue());
|
vs.setHeight(c.getHeight());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void applyCommandAndJvmSettings(MultiMCInstanceConfiguration c, GameSettings.Instance setting) {
|
private static void applyCommandAndJvmSettings(MultiMCInstanceConfiguration c, VersionSetting vs) {
|
||||||
if (c.isOverrideCommands()) {
|
if (c.isOverrideCommands()) {
|
||||||
setting.getOverrideProperties().addAll(List.of(
|
vs.setWrapper(Lang.nonNull(c.getWrapperCommand(), ""));
|
||||||
GameSettings.PROPERTY_COMMAND_WRAPPER,
|
vs.setPreLaunchCommand(Lang.nonNull(c.getPreLaunchCommand(), ""));
|
||||||
GameSettings.PROPERTY_PRE_LAUNCH_COMMAND
|
|
||||||
));
|
|
||||||
setting.commandWrapperProperty().setValue(Lang.nonNull(c.getWrapperCommand(), ""));
|
|
||||||
setting.preLaunchCommandProperty().setValue(Lang.nonNull(c.getPreLaunchCommand(), ""));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (c.isOverrideJavaArgs()) {
|
if (c.isOverrideJavaArgs()) {
|
||||||
setting.getOverrideProperties().add(GameSettings.PROPERTY_JVM_OPTIONS);
|
vs.setJavaArgs(Lang.nonNull(c.getJvmArgs(), ""));
|
||||||
setting.jvmOptionsProperty().setValue(Lang.nonNull(c.getJvmArgs(), ""));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Task<Void> createMultiMCPostUpdateTask(HMCLGameRepository repository, MultiMCInstanceConfiguration manifest, GameInstanceID instanceId) {
|
private static Task<Void> createMultiMCPostUpdateTask(Profile profile, MultiMCInstanceConfiguration manifest, String version) {
|
||||||
return Task.runAsync(Schedulers.javafx(), () -> {
|
return Task.runAsync(Schedulers.javafx(), () -> {
|
||||||
GameSettings.Instance setting = Objects.requireNonNull(repository.getInstanceGameSettingsOrCreate(instanceId));
|
VersionSetting vs = Objects.requireNonNull(profile.getRepository().specializeVersionSetting(version));
|
||||||
ModpackHelper.applyCommandAndJvmSettings(manifest, setting);
|
ModpackHelper.applyCommandAndJvmSettings(manifest, vs);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Task<Void> createMultiMCPostInstallTask(HMCLGameRepository repository, MultiMCInstanceConfiguration manifest, GameInstanceID instanceId) {
|
private static Task<Void> createMultiMCPostInstallTask(Profile profile, MultiMCInstanceConfiguration manifest, String version) {
|
||||||
return Task.runAsync(Schedulers.javafx(), () -> {
|
return Task.runAsync(Schedulers.javafx(), () -> {
|
||||||
GameSettings.Instance setting = Objects.requireNonNull(repository.getInstanceGameSettingsOrCreate(instanceId));
|
VersionSetting vs = Objects.requireNonNull(profile.getRepository().specializeVersionSetting(version));
|
||||||
ModpackHelper.toGameSettings(manifest, setting);
|
ModpackHelper.toVersionSetting(manifest, vs);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Task<Void> createMcbbsPostInstallTask(HMCLGameRepository repository, McbbsModpackManifest manifest, GameInstanceID instanceId) {
|
private static Task<Void> createMcbbsPostInstallTask(Profile profile, McbbsModpackManifest manifest, String version) {
|
||||||
return Task.runAsync(Schedulers.javafx(), () -> {
|
return Task.runAsync(Schedulers.javafx(), () -> {
|
||||||
GameSettings.Effective effective = repository.getEffectiveGameSettings(instanceId);
|
VersionSetting vs = Objects.requireNonNull(profile.getRepository().specializeVersionSetting(version));
|
||||||
if (manifest.getLaunchInfo().getMinMemory() > effective.getMaxMemory()) {
|
if (manifest.getLaunchInfo().getMinMemory() > vs.getMaxMemory())
|
||||||
GameSettings.Instance setting = Objects.requireNonNull(repository.getInstanceGameSettingsOrCreate(instanceId));
|
vs.setMaxMemory(manifest.getLaunchInfo().getMinMemory());
|
||||||
setting.getOverrideProperties().addAll(List.of(
|
|
||||||
GameSettings.PROPERTY_AUTO_MEMORY,
|
|
||||||
GameSettings.PROPERTY_MIN_MEMORY,
|
|
||||||
GameSettings.PROPERTY_MAX_MEMORY,
|
|
||||||
GameSettings.PROPERTY_PERM_SIZE
|
|
||||||
));
|
|
||||||
setting.autoMemoryProperty().setValue(effective.getInheritable(GameSettings::autoMemoryProperty));
|
|
||||||
setting.minMemoryProperty().setValue(effective.getInheritable(GameSettings::minMemoryProperty));
|
|
||||||
setting.maxMemoryProperty().setValue(manifest.getLaunchInfo().getMinMemory());
|
|
||||||
setting.permSizeProperty().setValue(effective.getInheritable(GameSettings::permSizeProperty));
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,20 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors
|
/*
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
* Hello Minecraft! Launcher
|
||||||
|
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
package org.jackhuang.hmcl.game;
|
package org.jackhuang.hmcl.game;
|
||||||
|
|
||||||
import fi.iki.elonen.NanoHTTPD;
|
import fi.iki.elonen.NanoHTTPD;
|
||||||
@@ -7,15 +22,13 @@ import org.jackhuang.hmcl.auth.AuthenticationException;
|
|||||||
import org.jackhuang.hmcl.auth.OAuth;
|
import org.jackhuang.hmcl.auth.OAuth;
|
||||||
import org.jackhuang.hmcl.event.Event;
|
import org.jackhuang.hmcl.event.Event;
|
||||||
import org.jackhuang.hmcl.event.EventManager;
|
import org.jackhuang.hmcl.event.EventManager;
|
||||||
import org.jackhuang.hmcl.theme.Themes;
|
import org.jackhuang.hmcl.ui.FXUtils;
|
||||||
import org.jackhuang.hmcl.util.StringUtils;
|
import org.jackhuang.hmcl.util.StringUtils;
|
||||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.util.Base64;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -24,51 +37,21 @@ import java.util.concurrent.ExecutionException;
|
|||||||
|
|
||||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||||
import static org.jackhuang.hmcl.util.Lang.thread;
|
import static org.jackhuang.hmcl.util.Lang.thread;
|
||||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
|
||||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||||
|
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||||
|
|
||||||
public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
||||||
private final int port;
|
private final int port;
|
||||||
private final CompletableFuture<String> future = new CompletableFuture<>();
|
private final CompletableFuture<String> future = new CompletableFuture<>();
|
||||||
private final String codeVerifier;
|
|
||||||
private final String state;
|
|
||||||
|
|
||||||
public static String lastlyOpenedURL;
|
public static String lastlyOpenedURL;
|
||||||
|
|
||||||
private String idToken;
|
private String idToken;
|
||||||
|
|
||||||
private OAuthServer(int port) {
|
private OAuthServer(int port) {
|
||||||
super("localhost", port);
|
super(port);
|
||||||
|
|
||||||
this.port = port;
|
this.port = port;
|
||||||
|
|
||||||
var encoder = Base64.getUrlEncoder().withoutPadding();
|
|
||||||
var random = new SecureRandom();
|
|
||||||
|
|
||||||
{
|
|
||||||
// https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1
|
|
||||||
// https://datatracker.ietf.org/doc/html/rfc6749#section-10.12
|
|
||||||
byte[] bytes = new byte[32];
|
|
||||||
random.nextBytes(bytes);
|
|
||||||
this.state = encoder.encodeToString(bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
// https://datatracker.ietf.org/doc/html/rfc7636#section-4.1
|
|
||||||
byte[] bytes = new byte[64];
|
|
||||||
random.nextBytes(bytes);
|
|
||||||
this.codeVerifier = encoder.encodeToString(bytes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getCodeVerifier() {
|
|
||||||
return codeVerifier;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getState() {
|
|
||||||
return state;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -110,32 +93,19 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
|||||||
String parameters = session.getQueryParameterString();
|
String parameters = session.getQueryParameterString();
|
||||||
|
|
||||||
Map<String, String> query = mapOf(NetworkUtils.parseQuery(parameters));
|
Map<String, String> query = mapOf(NetworkUtils.parseQuery(parameters));
|
||||||
|
if (query.containsKey("code")) {
|
||||||
String code = query.get("code");
|
idToken = query.get("id_token");
|
||||||
if (code != null) {
|
future.complete(query.get("code"));
|
||||||
if (this.state.equals(query.get("state"))) {
|
|
||||||
idToken = query.get("id_token");
|
|
||||||
future.complete(code);
|
|
||||||
} else if (query.containsKey("state")) {
|
|
||||||
LOG.warning("Failed to authenticate: invalid state in parameters");
|
|
||||||
future.completeExceptionally(new AuthenticationException("Failed to authenticate: invalid state"));
|
|
||||||
} else {
|
|
||||||
LOG.warning("Failed to authenticate: missing state in parameters");
|
|
||||||
future.completeExceptionally(new AuthenticationException("Failed to authenticate: missing state"));
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
LOG.warning("Failed to authenticate: missing authorization code in parameters");
|
LOG.warning("Error: " + parameters);
|
||||||
future.completeExceptionally(new AuthenticationException("Failed to authenticate: missing authorization code"));
|
future.completeExceptionally(new AuthenticationException("failed to authenticate"));
|
||||||
}
|
}
|
||||||
|
|
||||||
String html;
|
String html;
|
||||||
try {
|
try {
|
||||||
html = IOUtils.readFullyAsString(OAuthServer.class.getResourceAsStream("/assets/microsoft_auth.html"))
|
html = IOUtils.readFullyAsString(OAuthServer.class.getResourceAsStream("/assets/microsoft_auth.html"))
|
||||||
.replace("%style%", Themes.getColorScheme().toStyleSheet().replace("-monet", "--monet"))
|
|
||||||
.replace("%lang%", Locale.getDefault().toLanguageTag())
|
.replace("%lang%", Locale.getDefault().toLanguageTag())
|
||||||
.replace("%success%", i18n("message.success"))
|
.replace("%close-page%", i18n("account.methods.microsoft.close_page"));
|
||||||
.replace("%ok%", i18n("button.ok"))
|
|
||||||
.replace("%close_page%", i18n("account.methods.microsoft.close_page"));
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
LOG.error("Failed to load html", e);
|
LOG.error("Failed to load html", e);
|
||||||
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_HTML, "");
|
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_HTML, "");
|
||||||
@@ -151,18 +121,9 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
|||||||
return newFixedLengthResponse(Response.Status.OK, "text/html; charset=UTF-8", html);
|
return newFixedLengthResponse(Response.Status.OK, "text/html; charset=UTF-8", html);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() {
|
|
||||||
if (!future.isDone())
|
|
||||||
future.completeExceptionally(new AuthenticationException("OAuth server is closing"));
|
|
||||||
stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Factory implements OAuth.Callback {
|
public static class Factory implements OAuth.Callback {
|
||||||
public final EventManager<GrantDeviceCodeEvent> onGrantDeviceCode = new EventManager<>();
|
public final EventManager<GrantDeviceCodeEvent> onGrantDeviceCode = new EventManager<>();
|
||||||
public final EventManager<LoginCompletedDeviceCodeEvent> onLoginCompletedDeviceCode = new EventManager<>();
|
public final EventManager<OpenBrowserEvent> onOpenBrowser = new EventManager<>();
|
||||||
public final EventManager<OpenBrowserEvent> onOpenBrowserAuthorizationCode = new EventManager<>();
|
|
||||||
public final EventManager<OpenBrowserEvent> onOpenBrowserDevice = new EventManager<>();
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public OAuth.Session startServer() throws IOException, AuthenticationException {
|
public OAuth.Session startServer() throws IOException, AuthenticationException {
|
||||||
@@ -189,18 +150,11 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void loginCompletedDeviceCode() {
|
public void openBrowser(String url) throws IOException {
|
||||||
onLoginCompletedDeviceCode.fireEvent(new LoginCompletedDeviceCodeEvent(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void openBrowser(OAuth.GrantFlow grantFlow, String url) throws IOException {
|
|
||||||
lastlyOpenedURL = url;
|
lastlyOpenedURL = url;
|
||||||
|
FXUtils.openLink(url);
|
||||||
|
|
||||||
switch (grantFlow) {
|
onOpenBrowser.fireEvent(new OpenBrowserEvent(this, url));
|
||||||
case AUTHORIZATION_CODE -> onOpenBrowserAuthorizationCode.fireEvent(new OpenBrowserEvent(this, url));
|
|
||||||
case DEVICE -> onOpenBrowserDevice.fireEvent(new OpenBrowserEvent(this, url));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -208,6 +162,17 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
|||||||
return System.getProperty("hmcl.microsoft.auth.id",
|
return System.getProperty("hmcl.microsoft.auth.id",
|
||||||
JarUtils.getAttribute("hmcl.microsoft.auth.id", ""));
|
JarUtils.getAttribute("hmcl.microsoft.auth.id", ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getClientSecret() {
|
||||||
|
return System.getProperty("hmcl.microsoft.auth.secret",
|
||||||
|
JarUtils.getAttribute("hmcl.microsoft.auth.secret", ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isPublicClient() {
|
||||||
|
return true; // We have turned on the device auth flow.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class GrantDeviceCodeEvent extends Event {
|
public static class GrantDeviceCodeEvent extends Event {
|
||||||
@@ -229,12 +194,6 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class LoginCompletedDeviceCodeEvent extends Event {
|
|
||||||
public LoginCompletedDeviceCodeEvent(Object source) {
|
|
||||||
super(source);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class OpenBrowserEvent extends Event {
|
public static class OpenBrowserEvent extends Event {
|
||||||
private final String url;
|
private final String url;
|
||||||
|
|
||||||
|
|||||||
@@ -1,122 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
|
||||||
package org.jackhuang.hmcl.game;
|
|
||||||
|
|
||||||
import org.jackhuang.hmcl.Metadata;
|
|
||||||
import org.jackhuang.hmcl.modpack.ModpackConfiguration;
|
|
||||||
import org.jackhuang.hmcl.modpack.server.ServerModpackManifest;
|
|
||||||
import org.jackhuang.hmcl.util.StringUtils;
|
|
||||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
|
||||||
import org.jetbrains.annotations.NotNullByDefault;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
import org.jetbrains.annotations.Unmodifiable;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
|
||||||
|
|
||||||
/// Resolves the routes that the one-click join button can connect through.
|
|
||||||
@NotNullByDefault
|
|
||||||
public final class ServerEndpoint {
|
|
||||||
private ServerEndpoint() {
|
|
||||||
}
|
|
||||||
|
|
||||||
/// One selectable connection route.
|
|
||||||
///
|
|
||||||
/// @param name the display name
|
|
||||||
/// @param address the `host[:port]` or `ws(s)://` address handed to the game
|
|
||||||
public record Route(String name, String address) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Lists the routes configured for an instance.
|
|
||||||
///
|
|
||||||
/// Routes in the installed modpack manifest win, so they change with modpack updates. Without
|
|
||||||
/// them the manifest's single `serverAddress` and then [Metadata#getDefaultServerAddress()] are
|
|
||||||
/// used, each as a single route.
|
|
||||||
///
|
|
||||||
/// @param configurationFile the instance's `modpack.json` path
|
|
||||||
/// @return the routes in display order; empty when nothing is configured
|
|
||||||
public static @Unmodifiable List<Route> routes(Path configurationFile) {
|
|
||||||
List<Route> result = new ArrayList<>();
|
|
||||||
@Nullable ServerModpackManifest manifest = readManifest(configurationFile);
|
|
||||||
if (manifest != null) {
|
|
||||||
for (ServerModpackManifest.Route route : manifest.getRoutes()) {
|
|
||||||
if (StringUtils.isNotBlank(route.getAddress())) {
|
|
||||||
result.add(new Route(StringUtils.isBlank(route.getName()) ? route.getAddress().trim() : route.getName(), route.getAddress().trim()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (result.isEmpty() && StringUtils.isNotBlank(manifest.getServerAddress())) {
|
|
||||||
result.add(new Route("default", manifest.getServerAddress().trim()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (result.isEmpty()) {
|
|
||||||
String fallback = Metadata.getDefaultServerAddress();
|
|
||||||
if (!fallback.isEmpty()) {
|
|
||||||
result.add(new Route("default", fallback));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return List.copyOf(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Picks the route to join: the one the player chose last if it still exists, otherwise the first.
|
|
||||||
///
|
|
||||||
/// @param configurationFile the instance's `modpack.json` path
|
|
||||||
/// @return the route to connect through, or `null` when nothing is configured
|
|
||||||
public static @Nullable Route selected(Path configurationFile) {
|
|
||||||
List<Route> routes = routes(configurationFile);
|
|
||||||
if (routes.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
@Nullable String saved = readSelectedName(configurationFile);
|
|
||||||
for (Route route : routes) {
|
|
||||||
if (route.name().equals(saved)) {
|
|
||||||
return route;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return routes.get(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remembers the player's route choice next to the instance's modpack configuration.
|
|
||||||
///
|
|
||||||
/// @param configurationFile the instance's `modpack.json` path
|
|
||||||
/// @param route the chosen route
|
|
||||||
public static void select(Path configurationFile, Route route) {
|
|
||||||
try {
|
|
||||||
Files.writeString(selectionFile(configurationFile), route.name(), StandardCharsets.UTF_8);
|
|
||||||
} catch (IOException e) {
|
|
||||||
LOG.warning("Unable to save the selected route", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Path selectionFile(Path configurationFile) {
|
|
||||||
return configurationFile.resolveSibling("server-route.txt");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static @Nullable String readSelectedName(Path configurationFile) {
|
|
||||||
try {
|
|
||||||
Path file = selectionFile(configurationFile);
|
|
||||||
return Files.isRegularFile(file) ? Files.readString(file, StandardCharsets.UTF_8).trim() : null;
|
|
||||||
} catch (IOException e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static @Nullable ServerModpackManifest readManifest(Path configurationFile) {
|
|
||||||
if (!Files.isRegularFile(configurationFile)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
ModpackConfiguration<ServerModpackManifest> configuration =
|
|
||||||
JsonUtils.fromJsonFile(configurationFile, ModpackConfiguration.typeOf(ServerModpackManifest.class));
|
|
||||||
return configuration != null ? configuration.getManifest() : null;
|
|
||||||
} catch (Exception e) {
|
|
||||||
LOG.warning("Unable to read server routes from " + configurationFile, e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loaded 100 of 1290 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user