diff --git a/.github/ISSUE_TEMPLATE/-english--bug-report.md b/.github/ISSUE_TEMPLATE/-english--bug-report.md deleted file mode 100644 index 894d796b..00000000 --- a/.github/ISSUE_TEMPLATE/-english--bug-report.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -name: "[English] Bug Report" -about: To report a bug or a crash -title: '' -labels: '' -assignees: '' - ---- - - -**Version**: vX.X.X -**Platform**: Windows XX - -#### Description -_Please describe the problem._ - -#### Log -_Please provide the log or the stacktrace. The log file directory can be found in your installation directory. `desktop.log` is the log generated by Launcher and `core.log` is the log generated by ArkPets Core. If no log file is available, or it is determined that the log file is useless for solving this problem, you can remove this section._ -``` -# *** ArkPets Log - xxxx *** -# Created: xxxx-xx-xx xx:xx:xx,xxx -# OS: xxxx -# Java version: xxxx -# Working directory: xxxx -``` - -#### To Reproduce -_Please tell us how to reproduce this problem. If it is not reproducible, please also indicate it here._ diff --git a/.github/ISSUE_TEMPLATE/-english--proposal-or-feature-request.md b/.github/ISSUE_TEMPLATE/-english--proposal-or-feature-request.md deleted file mode 100644 index 7d703ee7..00000000 --- a/.github/ISSUE_TEMPLATE/-english--proposal-or-feature-request.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: "[English] Proposal or Feature Request" -about: To submit a proposal or a new feature request -title: '' -labels: '' -assignees: '' - ---- - - -**Version**: vX.X.X - -#### Description -_Please write your suggestions here... Thank you for your participation._ diff --git "a/.github/ISSUE_TEMPLATE/-\347\256\200\344\275\223\344\270\255\346\226\207--\344\274\230\345\214\226-\345\273\272\350\256\256-\346\226\260\345\212\237\350\203\275\350\277\275\345\212\240.md" "b/.github/ISSUE_TEMPLATE/-\347\256\200\344\275\223\344\270\255\346\226\207--\344\274\230\345\214\226-\345\273\272\350\256\256-\346\226\260\345\212\237\350\203\275\350\277\275\345\212\240.md" deleted file mode 100644 index 027ce0e7..00000000 --- "a/.github/ISSUE_TEMPLATE/-\347\256\200\344\275\223\344\270\255\346\226\207--\344\274\230\345\214\226-\345\273\272\350\256\256-\346\226\260\345\212\237\350\203\275\350\277\275\345\212\240.md" +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: "[简体中文] 优化/建议/新功能追加" -about: 用于提供优化建议,或者请求追加新功能与特性 -title: '' -labels: '' -assignees: '' - ---- - - -**软件版本**:vX.X.X - -#### 描述 -_感谢您对本项目的关注与支持!请描述您想要提供的优化建议。_ diff --git "a/.github/ISSUE_TEMPLATE/-\347\256\200\344\275\223\344\270\255\346\226\207--\351\227\256\351\242\230-\351\224\231\350\257\257-\345\264\251\346\272\203\346\212\245\345\221\212.md" "b/.github/ISSUE_TEMPLATE/-\347\256\200\344\275\223\344\270\255\346\226\207--\351\227\256\351\242\230-\351\224\231\350\257\257-\345\264\251\346\272\203\346\212\245\345\221\212.md" deleted file mode 100644 index ecf27896..00000000 --- "a/.github/ISSUE_TEMPLATE/-\347\256\200\344\275\223\344\270\255\346\226\207--\351\227\256\351\242\230-\351\224\231\350\257\257-\345\264\251\346\272\203\346\212\245\345\221\212.md" +++ /dev/null @@ -1,28 +0,0 @@ ---- -name: "[简体中文] 问题/错误/崩溃报告" -about: 用于报告程序所出现的问题、错误或者崩溃情形 -title: '' -labels: '' -assignees: '' - ---- - - -**软件版本**:vX.X.X -**运行环境**:Windows XX - -#### 问题描述 -_请简单描述您所遇到的问题,可提供图片以辅助说明。_ - -#### 程序日志 -_请在下方粘贴程序日志。日志文件夹 (logs) 可在安装目录下找到,其中 `desktop.log` 是启动器日志,`core.log` 是桌宠本体日志。如果没有日志生成,或者已明确日志对解决问题无益,则此栏可以删除。_ -``` -# *** ArkPets Log - xxxx *** -# Created: xxxx-xx-xx xx:xx:xx,xxx -# OS: xxxx -# Java version: xxxx -# Working directory: xxxx -``` - -#### 复现方法 -_请详细描述该问题在其他设备上的复现方法。如果问题无法稳定复现,或者只是特定设备上存在的特殊问题,也请在此处说明。_ diff --git a/.github/ISSUE_TEMPLATE/bug_report_en.yml b/.github/ISSUE_TEMPLATE/bug_report_en.yml new file mode 100644 index 00000000..177a0bcd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report_en.yml @@ -0,0 +1,50 @@ +name: "[English] Bug Report" +description: "Report a problem, error, or crash you encountered." +labels: ["Type: Bug"] + +body: + - type: markdown + attributes: + value: | + Before submitting a bug report, please make sure that: + 1. You are using the latest version of the program. + 2. You have read the [FAQ](https://github.com/isHarryh/Ark-Pets/blob/v3.x/docs/FAQ.md) and your issue is not resolved. + 3. You have browsed [existing GitHub Issues](https://github.com/isHarryh/Ark-Pets/issues) and found no similar issues. + - type: input + id: version + attributes: + label: Software Version + description: What version of the software are you using? + placeholder: vX.X.X + validations: + required: true + - type: input + id: os + attributes: + label: Operating System + description: What operating system are you using? + placeholder: Windows XX + validations: + required: true + - type: textarea + id: description + attributes: + label: Problem Description + description: Please describe the problem you encountered here. + placeholder: | + Please specify the conditions and steps to reproduce the problem, as well as the specific behavior observed. + You can paste screenshots here to help describe your issue more clearly. + validations: + required: true + - type: textarea + id: logs + attributes: + label: Program Logs + description: Please upload the program log file here (optional). + render: text + placeholder: | + How to upload log files? + 1. Open the launcher, go to the "Options" page, scroll down and click the "Export Logs" button. + 2. Click "Select Recent Logs", then click "Export Selected Logs". + 3. Choose where to save the log files and confirm. + 4. Drag and drop the saved log files here. diff --git a/.github/ISSUE_TEMPLATE/bug_report_zh_cn.yml b/.github/ISSUE_TEMPLATE/bug_report_zh_cn.yml new file mode 100644 index 00000000..44799ff6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report_zh_cn.yml @@ -0,0 +1,50 @@ +name: "[简体中文] 提交问题报告" +description: "向我们反馈程序所出现的问题、错误或者崩溃情形。" +labels: ["Type: Bug"] + +body: + - type: markdown + attributes: + value: | + 提交问题报告前,请确保您: + 1. 使用的是最新版本的程序。 + 2. 阅读了[常见问题解答](https://github.com/isHarryh/Ark-Pets/blob/v3.x/docs/FAQ.md)但未能解决您的问题。 + 3. 浏览了[现有的 GitHub Issues](https://github.com/isHarryh/Ark-Pets/issues) 但没有找到类似的问题。 + - type: input + id: version + attributes: + label: 软件版本 + description: 您所使用的软件版本是: + placeholder: vX.X.X + validations: + required: true + - type: input + id: os + attributes: + label: 操作系统 + description: 您所使用的操作系统是: + placeholder: Windows XX + validations: + required: true + - type: textarea + id: description + attributes: + label: 问题描述 + description: 请描述您所遇到的问题: + placeholder: | + 您需要说明您所遇到的问题的触发条件与复现步骤,以及问题的具体表现。 + 您可以将截图粘贴到此处,以便更清晰地描述您的问题。 + validations: + required: true + - type: textarea + id: logs + attributes: + label: 程序日志 + description: 请上传程序的日志文件(非必填): + render: text + placeholder: | + 如何上传日志文件? + 1. 打开启动器,进入“选项”页面,下滑找到并点击“导出日志”按钮。 + 2. 点击“选择近期日志”,然后点击“导出所选的日志”。 + 3. 选择日志文件的保存位置并确认。 + 4. 将保存的日志文件拖拽到此处。 diff --git a/.github/ISSUE_TEMPLATE/bug_report_zh_tw.yml b/.github/ISSUE_TEMPLATE/bug_report_zh_tw.yml new file mode 100644 index 00000000..2ecfbd56 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report_zh_tw.yml @@ -0,0 +1,50 @@ +name: "[繁體中文] 提交問題回報" +description: "向我們回報程式出現的問題、錯誤或崩潰情形。" +labels: ["Type: Bug"] + +body: + - type: markdown + attributes: + value: | + 提交問題回報前,請確保您: + 1. 使用的是最新版本的程式。 + 2. 閱讀了[常見問題解答](https://github.com/isHarryh/Ark-Pets/blob/v3.x/docs/FAQ.md)但未能解決您的問題。 + 3. 瀏覽了[現存的 GitHub Issues](https://github.com/isHarryh/Ark-Pets/issues) 但沒有找到類似的問題。 + - type: input + id: version + attributes: + label: 軟體版本 + description: 您所使用的軟體版本是: + placeholder: vX.X.X + validations: + required: true + - type: input + id: os + attributes: + label: 作業系統 + description: 您所使用的作業系統是: + placeholder: Windows XX + - type: textarea + id: description + attributes: + label: 問題描述 + description: 請描述您所遇到的問題: + placeholder: | + 請說明您遇到問題的觸發條件與重現步驟,以及問題的具體表現。 + 您可以將截圖貼在此處,以便更清楚地描述您的問題。 + validations: + required: true + - type: textarea + id: logs + attributes: + label: 程式日誌 + description: 請上傳程式的日誌檔案(非必填): + render: text + placeholder: | + 如何上傳日誌檔案? + 1. 開啟啟動器,進入「選項」頁面,下滑找到並點擊「匯出日誌」按鈕。 + 2. 點擊「選擇近期日誌」,然後點擊「匯出所選的日誌」。 + 3. 選擇日誌檔案的儲存位置並確認。 + 4. 將儲存的日誌檔案拖曳到此處。 + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/feature_request_en.yml b/.github/ISSUE_TEMPLATE/feature_request_en.yml new file mode 100644 index 00000000..441b18a9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request_en.yml @@ -0,0 +1,26 @@ +name: "[English] Feature Request" +description: "Request a new feature or suggest an improvement." +labels: ["Type: Enhancement"] + +body: + - type: markdown + attributes: + value: | + Before submitting, please make sure you have checked the [existing GitHub Issues](https://github.com/isHarryh/Ark-Pets/issues) and found no similar requests. + - type: input + id: version + attributes: + label: Software Version + description: What version of the software are you using? + placeholder: vX.X.X + validations: + required: true + - type: textarea + id: description + attributes: + label: Feature Description + description: Please describe the feature or improvement you would like to see. + placeholder: | + Please describe in detail the expected behavior and your reasons for the request. + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/feature_request_zh_cn.yml b/.github/ISSUE_TEMPLATE/feature_request_zh_cn.yml new file mode 100644 index 00000000..0e6e0bed --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request_zh_cn.yml @@ -0,0 +1,26 @@ +name: "[简体中文] 请求新的功能" +description: "向我们请求添加新的功能,或提供针对现有功能的建议。" +labels: ["Type: Enhancement"] + +body: + - type: markdown + attributes: + value: | + 提交诉求前,请确保您浏览了[现有的 GitHub Issues](https://github.com/isHarryh/Ark-Pets/issues) 并确认没有找到类似的诉求。 + - type: input + id: version + attributes: + label: 软件版本 + description: 您所使用的软件版本是: + placeholder: vX.X.X + validations: + required: true + - type: textarea + id: description + attributes: + label: 功能描述 + description: 请描述您希望追加的功能或改进: + placeholder: | + 您需要详细描述您所预期的功能行为,并提供您的理由。 + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/feature_request_zh_tw.yml b/.github/ISSUE_TEMPLATE/feature_request_zh_tw.yml new file mode 100644 index 00000000..32ab1ffe --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request_zh_tw.yml @@ -0,0 +1,26 @@ +name: "[繁體中文] 請求新功能" +description: "向我們請求新增功能,或針對現有功能提出建議。" +labels: ["Type: Enhancement"] + +body: + - type: markdown + attributes: + value: | + 提交需求前,請先瀏覽[現有的 GitHub Issues](https://github.com/isHarryh/Ark-Pets/issues) 並確認沒有找到類似的需求。 + - type: input + id: version + attributes: + label: 軟體版本 + description: 您所使用的軟體版本是: + placeholder: vX.X.X + validations: + required: true + - type: textarea + id: description + attributes: + label: 功能描述 + description: 請描述您希望新增或改進的功能: + placeholder: | + 請詳細描述您預期的功能行為,並說明您的理由。 + validations: + required: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 13b14830..6eeb2d71 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,82 +1,82 @@ name: Build -on: - push: - tags: - - 'v*' - branches: - - 'v*' - paths: - - 'assets/**' - - 'core/**' - - 'desktop/**' - pull_request: - tags: - - 'v*' - branches: - - 'v*' - paths: - - 'assets/**' - - 'core/**' - - 'desktop/**' - workflow_dispatch: +on: workflow_dispatch jobs: build: - runs-on: windows-latest + runs-on: ${{ matrix.target.os }} + strategy: + matrix: + target: [ + { os: windows-latest, dist-ext: exe }, + { os: macos-26, dist-ext: dmg }, + { os: macos-latest, dist-ext: dmg }, + { os: ubuntu-22.04, dist-ext: AppImage }, + { os: ubuntu-latest, dist-ext: AppImage } + ] steps: - name: Check Ref run: | echo "Current ref: ${{ github.ref }}" + echo "Current sha: ${{ github.sha }}" + echo "Current OS: ${{ matrix.target.os }}" - name: Clone Repo - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup JavaJDK - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: '17' distribution: 'liberica' cache: 'gradle' - name: Setup Gradle - uses: gradle/gradle-build-action@v3 + uses: gradle/actions/setup-gradle@v5 with: - gradle-version: 8.1.1 + gradle-version: 8.14.3 + + - name: Setup Inno Setup + if: ${{ startsWith(matrix.target.os,'windows') }} + run: choco install innosetup --no-progress + + - name: Setup Homebrew + if: ${{ startsWith(matrix.target.os,'macos') }} + uses: Homebrew/actions/setup-homebrew@main + + - name: Setup create-dmg + if: ${{ startsWith(matrix.target.os,'macos') }} + run: brew install create-dmg + + - name: Setup AppImage tools + if: ${{ startsWith(matrix.target.os,'ubuntu') }} + run: | + wget "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage" -O appimagetool + wget "https://github.com/linuxdeploy/linuxdeploy/releases/download/1-alpha-20250213-2/linuxdeploy-x86_64.AppImage" -O linuxdeploy + chmod +x appimagetool linuxdeploy + sudo chown root:root appimagetool linuxdeploy + sudo mv appimagetool linuxdeploy /usr/bin/ - name: Execute Gradle Tasks run: gradle clean distAll - - name: Upload Exe - uses: actions/upload-artifact@v4 + - name: Upload ${{ matrix.target.dist-ext }} + uses: actions/upload-artifact@v6 if: ${{ ! startsWith(github.ref, 'refs/tags/') }} with: - name: ArkPets.exe - path: desktop/build/dist/*.exe + name: ArkPets-${{ matrix.target.os }}.${{ matrix.target.dist-ext }} + path: desktop/build/dist/*.${{ matrix.target.dist-ext }} - name: Upload Zip - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 if: ${{ ! startsWith(github.ref, 'refs/tags/') }} with: - name: ArkPets.zip + name: ArkPets-${{ matrix.target.os }}.zip path: desktop/build/dist/*.zip - name: Upload Jar - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 if: ${{ ! startsWith(github.ref, 'refs/tags/') }} with: - name: ArkPets.jar + name: ArkPets-${{ matrix.target.os }}.jar path: desktop/build/dist/*.jar - -# DUE TO JLINK ISSUES, AUTO RELEASE WAS TEMPORARILY BANNED. -# -# - name: Publish Release -# uses: marvinpinto/action-automatic-releases@latest -# if: ${{ startsWith(github.ref, 'refs/tags/') }} -# with: -# repo_token: "${{ secrets.GITHUB_TOKEN }}" -# automatic_release_tag: "${{ github.ref_name }}" -# draft: false -# prerelease: false -# title: "${{ github.ref_name }}" -# files: desktop/build/dist/* diff --git a/.gitignore b/.gitignore index 692a28a6..ca443d3a 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ !/.github/ # Exclude generated files +.DS_Store bin/ build/ cache/ diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 4dc75cbd..b63e7358 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -6,7 +6,6 @@ - diff --git a/CHANGELOG.md b/CHANGELOG.md index f94668f8..4cb04b4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,55 @@ # 更新日志 / CHANGELOG +## v3.11 +| **新增** | | +|:---------------------------------------|:-------------------------------------------------| +| [`#152`]
[`ce00662`] | 新增了可以通过命令行选项 `--config` 来载入特定的配置文件的功能。 | +| [`#142`]
[`92ce842`]
[`669f3b1`] | 新增了基于马尔可夫矩阵的动作行为机制,现在动作的切换将更加自然。 | +| [`0408a2b`] | 新增了可以调节桌宠的交互式朝向翻转的触发条件的功能,现在可以让桌宠在拖拽时或光标掠过时翻转朝向。 | + +| **修复** | | +|:------------------------|:---------------------------------| +| [`55b30b8`] | 修复了使用 Windows 安装包更新软件后存在残留文件的问题。 | +| [`2edb6f9`] | 修复了在特定的窗口边界计算方式下,部分模型无法正常启动的问题。 | + +| **优化** | | +|:---------------------------------------|:-----------------------------------------------| +| [`25f3ff7`] | 优化了调试性截图的绘制与保存逻辑。 | +| [`#158`]
[`14f007f`] | 优化了启动器界面中列表视图(ListView)的交互体验。 | +| [`604ff70`]
[`3a7ddb2`] | 升级了 fastjson2 和 opencc4j 依赖库的版本,并移除了某些残留的依赖库。 | +| [`#159`]
[`a8829c4`]
[`823b904`] | 将 Gradle 版本从 8.1 升级到 8.14,并移除了过时的 Gradle 脚本语法。 | +| [`1c7200d`] | 优化了 GitHub Actions 的 `build` 工作流脚本,并升级了其依赖项版本。 | + +| **补丁** | | +|:-------------------------|:--------------------------------------------| +| `v3.11.1`
[`deedced`] | 修复了此版本引入的由阶段过滤问题导致的个别多阶段的敌方模型无法正常启动的问题。 | +| `v3.11.1`
[`69ef274`] | 修复了之前版本中的阶段名称前导零适配问题导致的个别多阶段的敌方模型无法正常启动的问题。 | + +## v3.10 +| **新增** | | +|:------------------------|:--------------------------------------------------------------| +| [`#131`]
[`9488a85`] | 新增了可以在导出日志对话框中选取 JVM 崩溃日志的功能。 | +| [`#136`]
[`93d4ff8`] | 新增了可以通过 JVM 选项 `-Darkpets.usesystemfont` 来控制是否在启动器中使用系统字体的功能。 | +| [`#117`]
[`19d5fd7`] | 新增了允许播放具有多阶段的敌方模型的攻击动作的功能(开放性测试)。 | +| [`796167b`] | 新增了默认启用 Angle 原生渲染模式的功能(开放性测试)。 | +| [`4d4b10d`] | 新增了可以在模型详情区域点击 **Wiki 按钮**快速跳转到对应角色的 Wiki 网页的功能。 | + +| **修复** | | +|:------------------------|:------------------------------------------------| +| [`1b73f80`] | 修复了在 Mirror 酱 CDK 失效时,下载模型库的逻辑不正确的问题。 | +| [`6bd28d3`] | 修复了当模型骨骼的 MeshAttachment 路径名称包含尾随空格时,无法加载模型的问题。 | + +| **优化** | | +|:---------------------------------------|:--------------------------------------------| +| [`#133`]
[`22cf0ca`] | 优化了桌宠鼠标穿透的实现方式,现在采用 GLFW 提供的原生穿透。 | +| [`#133`]
[`3fdad5d`] | 优化了设置桌宠窗口位置的调用逻辑,减少了桌宠静止时的性能开销。 | +| [`ae17135`] | 将 JavaFX 库升级到了 17.0.15。 | +| [`#139`]
[`86c0189`]
[`36ffc79`] | 优化了启动器界面中列表组件(例如模型列表和公告列表)的性能开销。 | +| [`#141`]
[`df89573`] | 优化了启动器界面中滚动面板(ScrollPane)的交互体验。 | +| [`#140`]
[`272e821`] | 优化了启动器界面的渲染缓存,缓解了界面卡顿问题。 | +| [`#146`]
[`17d406a`] | 优化了隐藏系统托盘图标的实现方式,现在采用 ITaskbarList 提供的底层实现。 | +| [`60be366`]
[`59469b8`] | 优化了加载骨骼文件的实现方式。 | + ## v3.9 | **新增** | | |:------------------------------------|:------------------------------------------------------------------------------------| @@ -14,11 +64,12 @@ |:------------------------|:-----------------------------| | [`#126`]
[`51b7806`] | 修复了在特定情况下,桌宠在被鼠标拖拽后会原地消失的问题。 | -| **优化** | | -|:---------------------------------------|:---------------------------------| -| [`#125`]
[`e0f2dce`] | 优化了日志模块的安全性,现在采用的是 reload4j 日志库。 | -| [`#113`]
[`4d35feb`]
[`f6a9c64`] | 优化了桌宠输入控制模块的代码逻辑。 | -| [`f5b0d9e`]
[`601f522`] | 优化了网络模块的代码逻辑。 | +| **优化** | | +|:---------------------------------------|:-------------------------------------------------| +| [`#113`]
[`8ccef37`] | 移除了 `--enable-snapshot` 命令行参数,现在使用 `--debug` 替代。 | +| [`#113`]
[`4d35feb`]
[`f6a9c64`] | 优化了桌宠输入控制模块的代码逻辑。 | +| [`f5b0d9e`]
[`601f522`] | 优化了网络模块的代码逻辑。 | +| [`#125`]
[`e0f2dce`] | 优化了日志模块的安全性,现在采用的是 reload4j 日志库。 | | **补丁** | | |:---------------------------------------|:---------------------------| @@ -84,14 +135,14 @@ | [`c277dae`] | 优化了桌宠的渲染偏移(OffsetY)参数,使得某些额外内容得以正常显示(例如对角色脚底的高亮描边)。 | ## v3.5 -| **新增** | | -|:--------------------------------------|:----------------------------------------| -| [`#86`]
[`e998e4a`]
[`7042699`] | 新增了**收藏模型**的功能,现在可以对模型进行收藏并在列表中单独显示它们。 | -| [`88ffa1e`] | 新增了启动器界面的**窗口圆角和窗口阴影**,使得启动器的外观更加现代。 | -| [`#90`]
[`8183242`]
[`a38e737`] | 新增了命令行选项 `--load-lib` 用于载入外部库。 | -| [`#93`]
[`16b34aa`] | 新增了命令行选项 `--enable-snapshot` 用于启用调试性截图。 | -| [`c36ec5e`]
[`f5c09bd`] | 新增了可以调节桌宠的动画交叉过渡时长等过渡设置的功能。 | -| [`97095c9`] | 新增了启动器模型页面的列表中“没有符合条件的模型”时的一个提示。 | +| **新增** | | +|:--------------------------------------|:--------------------------------------------| +| [`#86`]
[`e998e4a`]
[`7042699`] | 新增了**收藏模型**的功能,现在可以对模型进行收藏并在列表中单独显示它们。 | +| [`88ffa1e`] | 新增了启动器界面的**窗口圆角和窗口阴影**,使得启动器的外观更加现代。 | +| [`#90`]
[`8183242`]
[`a38e737`] | 新增了命令行选项 `--load-lib` 用于载入外部库。 | +| [`#93`]
[`16b34aa`] | ~~新增了命令行选项 `--enable-snapshot` 用于启用调试性截图。~~ | +| [`c36ec5e`]
[`f5c09bd`] | 新增了可以调节桌宠的动画交叉过渡时长等过渡设置的功能。 | +| [`97095c9`] | 新增了启动器模型页面的列表中“没有符合条件的模型”时的一个提示。 | | **修复** | | |:----------------------------------|:------------------------------------------| @@ -537,10 +588,22 @@ [`#112`]: https://github.com/isHarryh/Ark-Pets/pull/112 [`#113`]: https://github.com/isHarryh/Ark-Pets/pull/113 [`#116`]: https://github.com/isHarryh/Ark-Pets/pull/116 +[`#117`]: https://github.com/isHarryh/Ark-Pets/issues/117 [`#123`]: https://github.com/isHarryh/Ark-Pets/pull/123 [`#124`]: https://github.com/isHarryh/Ark-Pets/pull/124 [`#125`]: https://github.com/isHarryh/Ark-Pets/pull/125 [`#126`]: https://github.com/isHarryh/Ark-Pets/pull/126 +[`#131`]: https://github.com/isHarryh/Ark-Pets/pull/131 +[`#133`]: https://github.com/isHarryh/Ark-Pets/pull/133 +[`#136`]: https://github.com/isHarryh/Ark-Pets/pull/136 +[`#139`]: https://github.com/isHarryh/Ark-Pets/pull/139 +[`#140`]: https://github.com/isHarryh/Ark-Pets/pull/140 +[`#141`]: https://github.com/isHarryh/Ark-Pets/pull/141 +[`#142`]: https://github.com/isHarryh/Ark-Pets/pull/142 +[`#146`]: https://github.com/isHarryh/Ark-Pets/issues/146 +[`#152`]: https://github.com/isHarryh/Ark-Pets/pull/152 +[`#158`]: https://github.com/isHarryh/Ark-Pets/pull/158 +[`#159`]: https://github.com/isHarryh/Ark-Pets/pull/159 [`3253706`]: https://github.com/isHarryh/Ark-Pets/commit/3253706fde859a316b3e08362dd57adb98c1df8c [`7b2e856`]: https://github.com/isHarryh/Ark-Pets/commit/7b2e8562579ebabbb102b40122cf3130463f03bc [`ff82a1e`]: https://github.com/isHarryh/Ark-Pets/commit/ff82a1e21ce396c345038b4cb340f10eeca89cf2 @@ -633,6 +696,7 @@ [`a41e489`]: https://github.com/isHarryh/Ark-Pets/commit/a41e489c84661c30303c6cfcc3d10c4ad86293c5 [`49d13b6`]: https://github.com/isHarryh/Ark-Pets/commit/49d13b6627a73cd4cfe628db15062c7240cca334 [`0b2d11e`]: https://github.com/isHarryh/Ark-Pets/commit/0b2d11e519ccc4395e801379b9b1085cfb1779e2 +[`8ccef37`]: https://github.com/isHarryh/Ark-Pets/commit/8ccef3704ccc2b5d2cd628009c79817a77706445 [`4d35feb`]: https://github.com/isHarryh/Ark-Pets/commit/4d35febd0954e961b47150ff805e9b15a517cfee [`f6a9c64`]: https://github.com/isHarryh/Ark-Pets/commit/f6a9c640ade62dc6e1dea72db62dc72a173760ce [`fd45fe6`]: https://github.com/isHarryh/Ark-Pets/commit/fd45fe6d45759ed7261b4d8cb2eff497390171b7 @@ -653,3 +717,35 @@ [`c4e0f40`]: https://github.com/isHarryh/Ark-Pets/commit/c4e0f40638bed4e2a30d6c59209899559b988a53 [`cd34faa`]: https://github.com/isHarryh/Ark-Pets/commit/cd34faaee325ebb31d29ccfe4dfa3c766cdd634d [`5294d91`]: https://github.com/isHarryh/Ark-Pets/commit/5294d918451c4d3421cb9fc10110fa01f77d21bc +[`9488a85`]: https://github.com/isHarryh/Ark-Pets/commit/9488a85d9be4353158fefcb8e48b12f045f6ad23 +[`22cf0ca`]: https://github.com/isHarryh/Ark-Pets/commit/22cf0ca995ff13015856a3801d1aa962fe0786de +[`3fdad5d`]: https://github.com/isHarryh/Ark-Pets/commit/3fdad5db3f677b8289452ffd2a2079731b0a8185 +[`ae17135`]: https://github.com/isHarryh/Ark-Pets/commit/ae17135a752c0295d4d32032fe3371b4a4b866d3 +[`86c0189`]: https://github.com/isHarryh/Ark-Pets/commit/86c0189ef96956a9aac5047777f3b34ad9319f29 +[`36ffc79`]: https://github.com/isHarryh/Ark-Pets/commit/36ffc794c0d01db39b288c3b6ffe524c436f3d12 +[`93d4ff8`]: https://github.com/isHarryh/Ark-Pets/commit/93d4ff859fe61f12ee96e10bbda4599314cb863b +[`df89573`]: https://github.com/isHarryh/Ark-Pets/commit/df89573179a53f428716008b23484f06e4f539a6 +[`272e821`]: https://github.com/isHarryh/Ark-Pets/commit/272e8212b4a8c19e15e0d64a6a55ce7071fa79c4 +[`19d5fd7`]: https://github.com/isHarryh/Ark-Pets/commit/19d5fd71469f4cd6f241e5f51a124a7bb9bd0787 +[`796167b`]: https://github.com/isHarryh/Ark-Pets/commit/796167ba49acf2cdd1a33c526a2d4ed03ee55214 +[`4d4b10d`]: https://github.com/isHarryh/Ark-Pets/commit/4d4b10dddd99c6cdedf75ce13c57f161b4ee390d +[`17d406a`]: https://github.com/isHarryh/Ark-Pets/commit/17d406a8374d3c7d84ad13c9ba66f06fcbaa1c0d +[`1b73f80`]: https://github.com/isHarryh/Ark-Pets/commit/1b73f80fc2f2195ebac71de31dd610843ba5f52a +[`60be366`]: https://github.com/isHarryh/Ark-Pets/commit/60be366813df03a30c7dc81a03a809fddb831d9c +[`6bd28d3`]: https://github.com/isHarryh/Ark-Pets/commit/6bd28d360f96a5bf09f8ec409cfe944177da775d +[`59469b8`]: https://github.com/isHarryh/Ark-Pets/commit/59469b865651a4d63ccd95d8dd3adeae333186fc +[`ce00662`]: https://github.com/isHarryh/Ark-Pets/commit/ce006627fe4abe5564c88cb1f41097056f1be92a +[`92ce842`]: https://github.com/isHarryh/Ark-Pets/commit/92ce8428b28979482d74ab5083b41179ad3bdaf4 +[`669f3b1`]: https://github.com/isHarryh/Ark-Pets/commit/669f3b124467477b992f52bfef7dba075ce5c0e9 +[`55b30b8`]: https://github.com/isHarryh/Ark-Pets/commit/55b30b8169c7eb02ac3b4a2e48fb3170f819da43 +[`25f3ff7`]: https://github.com/isHarryh/Ark-Pets/commit/25f3ff7e5c31fd7e9f490db5ba3c0c1b91669a63 +[`2edb6f9`]: https://github.com/isHarryh/Ark-Pets/commit/2edb6f9052d26c5bdb08eebadf9cfe08eba5c235 +[`14f007f`]: https://github.com/isHarryh/Ark-Pets/commit/14f007fc9376df2277e12914af1493d1ea4b07b6 +[`0408a2b`]: https://github.com/isHarryh/Ark-Pets/commit/0408a2b99f2d82fd5b2f25b1066e06c66c658cb3 +[`604ff70`]: https://github.com/isHarryh/Ark-Pets/commit/604ff70de4fdb807cb37c43d72da300c735d0b61 +[`3a7ddb2`]: https://github.com/isHarryh/Ark-Pets/commit/3a7ddb2ae0338cc27a248c094b4c272056a48669 +[`a8829c4`]: https://github.com/isHarryh/Ark-Pets/commit/a8829c4546110a003b045f32c8ef4b6ff645bfbd +[`823b904`]: https://github.com/isHarryh/Ark-Pets/commit/823b904e5a0a88fd1cd9981d83fc167d9a5c07b8 +[`1c7200d`]: https://github.com/isHarryh/Ark-Pets/commit/1c7200d1ef78aa82efee9fcb3b38f65ab5d1eec3 +[`deedced`]: https://github.com/isHarryh/Ark-Pets/commit/deedced4b7c14defde4c60d2ec52e6e153de15fd +[`69ef274`]: https://github.com/isHarryh/Ark-Pets/commit/69ef2741ad703aab4a8490c7a3307b556982374d diff --git a/README.md b/README.md index caa97dda..d6885d43 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ ArkPets icon

Arknights Desktop Pets | 明日方舟桌宠 (ArkPets)
- v3.9 + v3.11

GitHub Top Language diff --git a/assets/ArkPetsConfigDefault.json b/assets/ArkPetsConfigDefault.json index 6ce68e3b..a9f9037b 100644 --- a/assets/ArkPetsConfigDefault.json +++ b/assets/ArkPetsConfigDefault.json @@ -5,6 +5,7 @@ "behavior_allow_sleep":false, "behavior_allow_special":true, "behavior_allow_walk":true, + "behavior_direction_switching":1, "behavior_do_peer_repulsion":true, "behavior_walk_speed": 30.0, "canvas_color":"#00000000", @@ -32,7 +33,7 @@ "physic_speed_limit_y":1000.0, "physic_static_friction_acc":500.0, "render_animation_mixture":0.3, - "render_enable_angle":false, + "render_enable_angle":true, "render_enable_mipmap":true, "render_outline":1, "render_outline_color":"#FFFF00FF", diff --git a/assets/UI/AnnounceDialog.fxml b/assets/UI/AnnounceDialog.fxml index 0e901ebf..2a220895 100644 --- a/assets/UI/AnnounceDialog.fxml +++ b/assets/UI/AnnounceDialog.fxml @@ -1,11 +1,12 @@ - + + @@ -14,7 +15,7 @@ + cache="true" cacheHint="SPEED" fx:controller="cn.harryh.arkpets.controllers.AnnounceDialog"> @@ -100,7 +101,7 @@ - diff --git a/assets/UI/BehaviorModule.fxml b/assets/UI/BehaviorModule.fxml index f35c6d8f..8455d422 100644 --- a/assets/UI/BehaviorModule.fxml +++ b/assets/UI/BehaviorModule.fxml @@ -1,7 +1,7 @@ @@ -46,6 +46,10 @@ + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -222,7 +255,7 @@ + prefWidth="${_modelViewW}" styleClass="arklist"> @@ -30,7 +30,8 @@ - + cache="true" cacheHint="SPEED" prefHeight="24.0" prefWidth="600.0" StackPane.alignment="TOP_CENTER"/> diff --git a/assets/utils/xdgstartup.desktop b/assets/utils/xdgstartup.desktop index 0815bf88..2d42b6d0 100644 --- a/assets/utils/xdgstartup.desktop +++ b/assets/utils/xdgstartup.desktop @@ -1,5 +1,5 @@ [Desktop Entry] Name=ArkPetsStartup -Exec={{ARKPETS_EXECUTABLE}} +Exec=env LIBDECOR_FORCE_CSD=1 {{ARKPETS_EXECUTABLE}} Type=Application Path={{ARKPETS_WORKING_DIR}} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 56ea9bb2..8fafb357 100644 --- a/build.gradle +++ b/build.gradle @@ -3,14 +3,14 @@ import org.gradle.internal.os.OperatingSystem buildscript { repositories { - maven { url "https://maven.aliyun.com/repository/public/" } // Aliyun Mirrors - maven { url "https://maven.aliyun.com/repository/gradle-plugin/" } + maven { url = "https://maven.aliyun.com/repository/public/" } // Aliyun Mirrors + maven { url = "https://maven.aliyun.com/repository/gradle-plugin/" } mavenLocal() mavenCentral() } dependencies { - classpath "org.openjfx:javafx-plugin:0.0.13" + classpath "org.openjfx:javafx-plugin:0.1.0" } } @@ -20,7 +20,7 @@ allprojects { apply plugin: "java-library" apply plugin: "org.openjfx.javafxplugin" - version = "3.9.2" + version = "3.11.1" ext { // App Metadata appName = "ArkPets" @@ -31,7 +31,7 @@ allprojects { // Prefabs gdxVersion = "1.11.0" jnaVersion = "5.12.1" - javaFXVersion = "17.0.8" + javaFXVersion = "17.0.15" dbusVersion = "5.1.0" lwjglVersion = "3.3.4" javaFXPlatform = "" @@ -71,7 +71,7 @@ allprojects { } repositories { - maven { url "https://maven.aliyun.com/repository/public/" } // Aliyun Mirrors + maven { url = "https://maven.aliyun.com/repository/public/" } // Aliyun Mirrors mavenLocal() mavenCentral() } @@ -84,7 +84,6 @@ project(":desktop") { implementation project(":core") // libGDX Desktop api "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-desktop" - api "com.badlogicgames.gdx:gdx-freetype-platform:$gdxVersion:natives-desktop" } } @@ -112,7 +111,7 @@ project(":core") { // JFoenix api "com.jfoenix:jfoenix:9.0.1" // FastJson - api "com.alibaba:fastjson:2.0.39" + api "com.alibaba.fastjson2:fastjson2:2.0.60" // CommonMark api "org.commonmark:commonmark:0.24.0" api "org.commonmark:commonmark-ext-autolink:0.24.0" @@ -133,6 +132,6 @@ project(":core") { // TiniPinyin api 'com.github.promeg:tinypinyin:2.0.3' // OpenCC4j - api 'com.github.houbb:opencc4j:1.8.1' + api 'com.github.houbb:opencc4j:1.14.0' } } diff --git a/core/src/cn/harryh/arkpets/ArkChar.java b/core/src/cn/harryh/arkpets/ArkChar.java index 34520750..efb5fd13 100644 --- a/core/src/cn/harryh/arkpets/ArkChar.java +++ b/core/src/cn/harryh/arkpets/ArkChar.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets; @@ -9,17 +9,21 @@ import cn.harryh.arkpets.animations.AnimComposer; import cn.harryh.arkpets.animations.AnimData; import cn.harryh.arkpets.assets.ModelItem.ModelAssetAccessor; +import cn.harryh.arkpets.assets.SkeletonLoader; import cn.harryh.arkpets.transitions.EasingFunction; import cn.harryh.arkpets.transitions.TransitionFloat; import cn.harryh.arkpets.transitions.TransitionVector3; import cn.harryh.arkpets.utils.DynamicOrthographicCamara; import cn.harryh.arkpets.utils.DynamicOrthographicCamara.Insert; import cn.harryh.arkpets.utils.Logger; +import cn.harryh.arkpets.utils.PixmapWrapper; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.files.FileHandle; -import com.badlogic.gdx.graphics.*; +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.graphics.GL20; +import com.badlogic.gdx.graphics.Pixmap; import com.badlogic.gdx.graphics.Pixmap.Format; -import com.badlogic.gdx.graphics.g2d.TextureAtlas; +import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.glutils.ShaderProgram; import com.badlogic.gdx.utils.GdxRuntimeException; import com.badlogic.gdx.utils.ScreenUtils; @@ -31,6 +35,7 @@ import java.io.InputStream; import java.util.HashMap; +import static cn.harryh.arkpets.Const.PathConfig.tempDirPath; import static cn.harryh.arkpets.Const.*; import static java.io.File.separator; @@ -89,45 +94,31 @@ public ArkChar(ArkConfig config, float scale) { ModelAssetAccessor modelAssetAccessor = new ModelAssetAccessor(config.character_files); String path2atlas = assetLocation + separator + modelAssetAccessor.getFirstFileOf(".atlas"); String path2skel = assetLocation + separator + modelAssetAccessor.getFirstFileOf(".skel"); - // Load atlas - FileHandle packFile = Gdx.files.internal(path2atlas); - TextureAtlas.TextureAtlasData atlasData = new TextureAtlas.TextureAtlasData(packFile, packFile.parent(), false); - if (config.render_enable_mipmap) { - for (TextureAtlas.TextureAtlasData.Page page : atlasData.getPages()) { - page.minFilter = Texture.TextureFilter.MipMapLinearLinear; - page.useMipMaps = true; + FileHandle atlasFile = Gdx.files.internal(path2atlas); + FileHandle skelFile = Gdx.files.internal(path2skel); + // Load skel + try { + SkeletonLoader skeletonLoader = new SkeletonLoader(skelFile); + Logger.info("Character", "Skeleton loading as " + (skeletonLoader.isJson() ? "JSON" : "binary")); + if (skeletonLoader.needFix()) { + skeletonLoader = skeletonLoader.fixed(); + Logger.warn("Character", "Skeleton fixed"); } + skeletonData = skeletonLoader.loadSkeletonDataWith(atlasFile, scale * skelBaseScale, config.render_enable_mipmap); + Logger.debug("Character", "Skeleton loaded with Spine version " + skeletonLoader.version); + } catch (Exception e) { + Logger.error("Character", "Failed to load skeleton, details see below.", e); + throw new RuntimeException("Launch ArkPets failed, the model asset may be inaccessible."); } - TextureAtlas atlas = new TextureAtlas(atlasData); - // Load skel (use SkeletonJson instead of SkeletonBinary if the file type is JSON) - FileHandle data = Gdx.files.internal(path2skel); - InputStream is = data.read(); - char first = (char) is.read(); - if (first == '{') { // Skeleton is json - Logger.debug("Character", "Skeleton format is JSON"); - SkeletonJson json = new SkeletonJson(atlas); - json.setScale(scale * skelBaseScale); - skeletonData = json.readSkeletonData(data); - } else { // Skeleton is binary - Logger.debug("Character", "Skeleton format is binary"); - SkeletonBinary binary = new SkeletonBinary(atlas); - binary.setScale(scale * skelBaseScale); - skeletonData = binary.readSkeletonData(data); - } - is.close(); - } catch (SerializationException | GdxRuntimeException | IOException e) { + } catch (SerializationException | GdxRuntimeException e) { Logger.error("Character", "The model asset may be inaccessible, details see below.", e); throw new RuntimeException("Launch ArkPets failed, the model asset may be inaccessible."); } skeleton = new Skeleton(skeletonData); skeleton.updateWorldTransform(); - animList = new AnimClipGroup(skeletonData.getAnimations().toArray(Animation.class)); - // 4.Animation mixing + // 4.Animation setup AnimationStateData asd = new AnimationStateData(skeletonData); - for (AnimClip i : animList) - for (AnimClip j : animList) - if (!i.fullName.equals(j.fullName)) - asd.setMix(i.fullName, j.fullName, config.render_animation_mixture); + animList = new AnimClipGroup(skeletonData.getAnimations().toArray(Animation.class)); // 5.Animation state setup animationState = new AnimationState(asd); animationState.apply(skeleton); @@ -145,6 +136,7 @@ protected void onApply(AnimData playing) { outlineWidth = config.render_outline_width; outlineColor = new Color(Color.CLEAR); shadowColor = ArkConfig.getGdxColorFrom(config.render_shadow_color); + // 7.Canvas fitting stageInsertMap = new HashMap<>(); for (AnimStage stage : animList.clusterByStage().keySet()) { // Figure out the suitable canvas size @@ -159,6 +151,8 @@ protected void onApply(AnimData playing) { } } camera.setInsertMaxed(); + // 8.Animation mixing + animList.applyCompleteAnimMix(asd, config.render_animation_mixture); } /** Sets the canvas with the specified background color. @@ -337,7 +331,9 @@ private void adjustCanvas(AnimStage stage, int framePerSample, float coverage) { camera.getFBO().begin(); ScreenUtils.clear(0, 0, 0, 0, true); // Render all animations to the FBO - float alphaPerSample = (float) Math.max(1.0 - 254.0 / 255.0, 1.0 - Math.pow(10.0, -4.0 / totalSamples)); + float alphaPerSample = (float) Math.max(1.0 - 254.0 / 255.0, Math.min(1.0, + 1.0 - Math.pow(10.0, -4.0 / totalSamples) + Math.pow(10, 1.0 / totalSamples - 2.0) + )); for (AnimClip animClip : animList.findAnimations(stage)) { composer.reset(); composer.offer(new AnimData(animClip)); @@ -357,13 +353,13 @@ private void adjustCanvas(AnimStage stage, int framePerSample, float coverage) { } } // Take down the snapshot from the rendered FBO - Pixmap snapshot = Pixmap.createFromFrameBuffer(0, 0, camera.getWidth(), camera.getHeight()); + PixmapWrapper pw = PixmapWrapper.fromCamera(camera); camera.getFBO().end(); // Crop the canvas in order to fit the snapshot float alphaThreshold = Math.max(0f, Math.min(coverage, 1f)); Insert insert; do { - insert = camera.getFittedInsert(snapshot, alphaThreshold, false, true); + insert = camera.getFittedInsert(pw.getPixmap(), alphaThreshold, false, true); if (!insert.equals(camera.getInsert()) || alphaThreshold < 0.75f) break; alphaThreshold *= 0.9375f; @@ -372,18 +368,21 @@ private void adjustCanvas(AnimStage stage, int framePerSample, float coverage) { Logger.warn("Character", stage + " has inappropriate canvas coverage setting, auto adjusted to " + alphaThreshold); // For debugging if (isDebugEnabled) { - snapshot.setColor(Color.RED); - snapshot.drawLine(0, -insert.bottom, camera.getWidth(), -insert.bottom); - snapshot.drawLine(0, camera.getHeight() + insert.top, camera.getWidth(), camera.getHeight() + insert.top); - snapshot.drawLine(-insert.left, 0, -insert.left, camera.getHeight()); - snapshot.drawLine(camera.getWidth() + insert.right, 0, camera.getWidth() + insert.right, camera.getHeight()); - FileHandle dir = new FileHandle("temp/"); + pw.drawCmap("tab16t", "a"); + pw.drawUnfilledRectangle(Color.RED, + -insert.left, + -insert.bottom, + camera.getWidth() + insert.left + insert.right, + camera.getHeight() + insert.top + insert.bottom, + 2); + FileHandle dir = new FileHandle(tempDirPath); dir.mkdirs(); FileHandle file = dir.child("acSnapshot-" + skeleton.toString() + "-" + stage.id() + ".png"); - PixmapIO.writePNG(file, snapshot); + pw.savePixmap(file, true); + Logger.debug("Character", "Saved acSnapshot to: " + file.path()); } // Complete camera.setInsert(insert); - snapshot.dispose(); + pw.dispose(); } } diff --git a/core/src/cn/harryh/arkpets/ArkConfig.java b/core/src/cn/harryh/arkpets/ArkConfig.java index 05746e12..fc2f16b5 100644 --- a/core/src/cn/harryh/arkpets/ArkConfig.java +++ b/core/src/cn/harryh/arkpets/ArkConfig.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets; @@ -8,9 +8,9 @@ import cn.harryh.arkpets.utils.IOUtils.FileUtil; import cn.harryh.arkpets.utils.Logger; import cn.harryh.arkpets.utils.SecretUtils; -import com.alibaba.fastjson.JSON; -import com.alibaba.fastjson.JSONObject; -import com.alibaba.fastjson.annotation.JSONField; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.alibaba.fastjson2.annotation.JSONField; import com.badlogic.gdx.graphics.Color; import java.io.File; @@ -23,6 +23,7 @@ import static cn.harryh.arkpets.Const.charsetDefault; import static cn.harryh.arkpets.Const.hexColorRegex; +import static com.alibaba.fastjson2.JSONWriter.Feature.PrettyFormat; public class ArkConfig implements Serializable { @@ -43,6 +44,8 @@ public class ArkConfig implements Serializable { public boolean behavior_allow_special; /** @since ArkPets 1.0 */ @JSONField(defaultValue = "true") public boolean behavior_allow_walk; + /** @since ArkPets 3.11 */ @JSONField(defaultValue = "1") + public int behavior_direction_switching; /** @since ArkPets 1.6 */ @JSONField(defaultValue = "true") public boolean behavior_do_peer_repulsion; /** @since ArkPets 3.9 */ @JSONField(defaultValue = "30.0") @@ -97,7 +100,7 @@ public class ArkConfig implements Serializable { public float physic_speed_limit_y; /** @since ArkPets 3.5 */ @JSONField(defaultValue = "0.3") public float render_animation_mixture; - /** @since ArkPets 3.8 */ @JSONField(defaultValue = "false") + /** @since ArkPets 3.8 */ @JSONField(defaultValue = "true") public boolean render_enable_angle; /** @since ArkPets 3.8 */ @JSONField(defaultValue = "true") public boolean render_enable_mipmap; @@ -106,9 +109,9 @@ public class ArkConfig implements Serializable { /** @since ArkPets 3.3 */ @JSONField(defaultValue = "#FFFF00FF") public String render_outline_color; /** @since ArkPets 3.9 */ @JSONField(defaultValue = "3") - public int render_outline_emphasis; + public int render_outline_emphasis; /** @since ArkPets 3.9 */ @JSONField(defaultValue = "#FFBB00FF") - public String render_outline_emphasis_color; + public String render_outline_emphasis_color; /** @since ArkPets 3.3 */ @JSONField(defaultValue = "2.0") public float render_outline_width; /** @since ArkPets 3.6 */ @JSONField(defaultValue = "#000000BB") @@ -134,7 +137,7 @@ private ArkConfig() { @JSONField(serialize = false) public void save() { try { - FileUtil.writeString(configCustom, charsetDefault, JSON.toJSONString(this, true), false); + FileUtil.writeString(configCustom, charsetDefault, JSON.toJSONString(this, PrettyFormat), false); Logger.debug("Config", "Config saved"); } catch (IOException e) { Logger.error("Config", "Config saving failed, details see below.", e); @@ -183,8 +186,8 @@ public void setMcCdk(String string) throws GeneralSecurityException { download_mc_cdk = ""; } - /** Gets the custom ArkConfig object by reading the external config file. - * If the external config file does not exist, a default config file will be generated. + /** Gets the custom ArkConfig object by reading the default custom config file. + * If the default custom config file does not exist, a default config file will be generated. * @return An ArkConfig object. {@code null} if failed. */ public static ArkConfig getConfig() { @@ -196,17 +199,26 @@ public static ArkConfig getConfig() { config.save(); return getDefaultConfig(); } else { - // Read and parse the external config file. + return getConfig(configCustom); + } + } + + /** Gets the ArkConfig object by reading the specific config file. + * @return An ArkConfig object. {@code null} if failed or config file not found. + */ + public static ArkConfig getConfig(File configFile) { + if (configFile.exists()) { + // Read and parse the config file. try { return Objects.requireNonNull( - JSONObject.parseObject(FileUtil.readString(configCustom, charsetDefault), ArkConfig.class), + JSONObject.parseObject(FileUtil.readString(configFile, charsetDefault), ArkConfig.class), "JSON parsing returns null." ); } catch (IOException | NullPointerException e) { Logger.error("Config", "Failed to get the custom config, details see below.", e); } - return null; } + return null; } /** Gets the default ArkConfig object by reading the internal config file. diff --git a/core/src/cn/harryh/arkpets/ArkPets.java b/core/src/cn/harryh/arkpets/ArkPets.java index 37c77c54..fbcb551f 100644 --- a/core/src/cn/harryh/arkpets/ArkPets.java +++ b/core/src/cn/harryh/arkpets/ArkPets.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets; @@ -14,16 +14,16 @@ import cn.harryh.arkpets.utils.*; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Input; +import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Graphics; import com.badlogic.gdx.files.FileHandle; import com.badlogic.gdx.graphics.GL20; -import com.badlogic.gdx.graphics.Pixmap; -import com.badlogic.gdx.graphics.PixmapIO; import java.util.HashMap; import java.util.List; -import java.util.Objects; import java.util.regex.Pattern; +import static cn.harryh.arkpets.Const.PathConfig.tempDirPath; +import static cn.harryh.arkpets.Const.changeDirectionXThreshold; import static cn.harryh.arkpets.Const.coreTitleManager; @@ -39,23 +39,28 @@ public class ArkPets extends InputApplicationAdaptor { private HWndCtrl hWndMine; private List hWndList; private final Cached hWndTopmostGetter; + private final Cached hWndTransparentSetter; + private final Cached hWndPosSetter; private final String APP_TITLE; private int offsetY = 0; - private boolean isToolwindowStyle = false; private boolean isAlwaysTransparent = false; private final Cached isFocused; - public ArkPets(String title) { + public ArkPets(String title, ArkConfig appConfig) { APP_TITLE = title; hWndTopmostGetter = new Cached<>(); hWndTopmostGetter.setValueProducer(this::refreshWindowIndex); - hWndTopmostGetter.setCacheAgeProducer(() -> 4.0 / getReducedFPS()); + hWndTopmostGetter.setCacheAgeProducer(() -> 8.0 / getReducedFPS()); isFocused = new Cached<>(); isFocused.setValueProducer(() -> hWndMine.isForeground()); isFocused.setCacheAgeProducer(() -> 4.0 / getReducedFPS()); + + hWndTransparentSetter = new Cached<>(); + hWndPosSetter = new Cached<>(); + config = appConfig; } @Override @@ -63,7 +68,6 @@ public void create() { // When the APP was created // 1.App setup Logger.info("App", "Create with title \"" + APP_TITLE + "\""); - config = Objects.requireNonNull(ArkConfig.getConfig(), "ArkConfig returns a null instance, please check the config file."); Gdx.input.setInputProcessor(this); Gdx.graphics.setForegroundFPS(config.display_fps); registerDebugger(); @@ -100,9 +104,11 @@ public void create() { // 5.Window style setup hWndMine = WindowSystem.findWindow(null, APP_TITLE); - hWndMine.setLayered(true); + hWndMine.attachGLFWWindow((Lwjgl3Graphics) Gdx.graphics); if (config.window_style_topmost) hWndMine.setTopmost(true); + if (config.window_style_toolwindow) + hWndMine.setTaskbar(false); updateWindow(); // 6.Tray icon setup @@ -212,12 +218,24 @@ private void changeAnimation(AnimData animData) { offsetY = (int) (animData.animClip().type.offsetY * config.display_scale); } + private void changeMobilitySign(int sign) { + cha.position.reset(cha.position.end().x, cha.position.end().y, sign); + if (cha.getPlaying() != null && cha.getPlaying().mobility() != 0) { + AnimData anim = cha.getPlaying(); + cha.setAnimation(anim.derive(Math.abs(anim.mobility()) * sign)); + } + if (tray.keepAnim != null && tray.keepAnim.mobility() != 0) { + AnimData anim = tray.keepAnim; + tray.keepAnim = anim.derive(Math.abs(anim.mobility()) * sign); + } + } + /* INPUT PROCESS */ @Override protected void onMouseDown() { if (!isMouseAtSolidPixel()) { // Transfer mouse event - RelativeWindowPosition rwp = getRelativeWindowPositionAt(getMouseX(), getMouseY()); + RelativeWindowPosition rwp = getUnderlyingRWP(); if (rwp != null) rwp.sendMouseEvent(switch (getMouseButton()) { case Input.Buttons.LEFT -> HWndCtrl.MouseEvent.LBUTTONDOWN; @@ -246,25 +264,19 @@ protected void onMouseDrag() { plane.changePosition(Gdx.graphics.getDeltaTime(), x, -(cha.camera.getHeight() + y)); windowPosition.setToEnd(); tray.hideDialog(); + if (Math.abs(getLastDragDeltaX()) >= changeDirectionXThreshold && config.behavior_direction_switching >= 2) + changeMobilitySign((int) Math.signum(getLastDragDeltaX())); } } @Override protected void onMouseUp() { if (isMouseDragging()) { - // Update the z-axis of the character - cha.position.reset(cha.position.end().x, cha.position.end().y, getMouseIntention()); - if (cha.getPlaying() != null && cha.getPlaying().mobility() != 0) { - AnimData anim = cha.getPlaying(); - cha.setAnimation(anim.derive(Math.abs(anim.mobility()) * getMouseIntention())); - } - if (tray.keepAnim != null && tray.keepAnim.mobility() != 0) { - AnimData anim = tray.keepAnim; - tray.keepAnim = anim.derive(Math.abs(anim.mobility()) * getMouseIntention()); - } + if (Math.abs(getLastDragDeltaX()) >= changeDirectionXThreshold && config.behavior_direction_switching >= 1) + changeMobilitySign((int) Math.signum(getLastDragDeltaX())); } else if (!isMouseAtSolidPixel()) { // Transfer mouse event - RelativeWindowPosition rwp = getRelativeWindowPositionAt(getMouseX(), getMouseY()); + RelativeWindowPosition rwp = getUnderlyingRWP(); if (rwp != null) rwp.sendMouseEvent(switch (getMouseButton()) { case Input.Buttons.LEFT -> HWndCtrl.MouseEvent.LBUTTONUP; @@ -279,6 +291,19 @@ protected void onMouseUp() { } } + @Override + protected void onMouseMoved() { + if (!isMouseAtSolidPixel()) { + // Transfer mouse event + RelativeWindowPosition rwp = getUnderlyingRWP(); + if (rwp != null) + rwp.sendMouseEvent(HWndCtrl.MouseEvent.MOUSEMOVE); + } else { + if (Math.abs(getLastMoveDeltaX()) >= changeDirectionXThreshold && config.behavior_direction_switching >= 3) + changeMobilitySign((int) Math.signum(getLastMoveDeltaX())); + } + } + @Override protected void onKeyDown(int keycode) { if (tray.keepAnim != null) { // Switch animation in action mode @@ -286,15 +311,19 @@ protected void onKeyDown(int keycode) { if (isUpPressed()) { do { data = behavior.prevAnim(); - } while (data.animClip().type == AnimClip.AnimType.MOVE); // Skip Move Animation - tray.keepAnim = data; - Logger.debug("Animation", "Switch to previous " + data); + } while (data != null && data.animClip().type == AnimClip.AnimType.MOVE); // Skip Move Animation + if (data != null) { + tray.keepAnim = data; + Logger.debug("Animation", "Switch to previous " + data); + } } else if (isDownPressed()) { do { data = behavior.nextAnim(); - } while (data.animClip().type == AnimClip.AnimType.MOVE); - tray.keepAnim = data; - Logger.debug("Animation", "Switch to next " + data); + } while (data != null && data.animClip().type == AnimClip.AnimType.MOVE); + if (data != null) { + tray.keepAnim = data; + Logger.debug("Animation", "Switch to next " + data); + } } } } @@ -303,16 +332,6 @@ protected void onKeyDown(int keycode) { protected void onKeyUp(int keycode) { } - @Override - protected void onMouseMoved() { - if (!isMouseAtSolidPixel()) { - // Transfer mouse event - RelativeWindowPosition rwp = getRelativeWindowPositionAt(getMouseX(), getMouseY()); - if (rwp != null) - rwp.sendMouseEvent(HWndCtrl.MouseEvent.MOUSEMOVE); - } - } - private boolean isMouseAtSolidPixel() { int pixel = cha.getPixel(getMouseX(), cha.camera.getHeight() - getMouseY() - 1); return (pixel & 0x000000FF) > 0; @@ -322,32 +341,31 @@ private boolean isMouseAtSolidPixel() { private void updateWindow() { if (hWndMine == null) return; - // Tool window style - if (config.window_style_toolwindow && !isToolwindowStyle) { - // Make sure ArkPets has been set as foreground window once - for (int i = 0; i < 1; i++) { - if (hWndMine.isForeground()) { - hWndMine.setTaskbar(false); - Logger.info("Window", "SetForegroundWindow succeeded"); - isToolwindowStyle = true; - break; - } - hWndMine.setForeground(); - } - } // Transparent style - hWndMine.setTransparent(isAlwaysTransparent); + hWndTransparentSetter.setValue(isAlwaysTransparent); + if (hWndTransparentSetter.isChanged()) { + hWndMine.setTransparent(hWndTransparentSetter.getValue()); + } // Window position - hWndMine.setWindowPosition(hWndTopmostGetter.getValue(), - (int) windowPosition.now().x, (int) windowPosition.now().y, - cha.camera.getWidth(), cha.camera.getHeight()); + HWndCtrl.WindowRect rect = new HWndCtrl.WindowRect( + (int) windowPosition.now().y, + (int) windowPosition.now().y + cha.camera.getHeight(), + (int) windowPosition.now().x, + (int) windowPosition.now().x + cha.camera.getWidth()); + HWndCtrl top = hWndTopmostGetter.getValue(); + hWndPosSetter.setValue(rect); + if (hWndPosSetter.isChanged()) { + rect = hWndPosSetter.getValue(); + hWndMine.setWindowPosition(top, + rect.left(), rect.top(), rect.width(), rect.height()); + } } - private RelativeWindowPosition getRelativeWindowPositionAt(int x, int y) { + private RelativeWindowPosition getUnderlyingRWP() { if (hWndList == null) return null; - int absX = x + (int) (windowPosition.now().x); - int absY = y + (int) (windowPosition.now().y); + int absX = getMouseX() + (int) (windowPosition.now().x); + int absY = getMouseY() + (int) (windowPosition.now().y); for (HWndCtrl hWndCtrl : hWndList) { if (coreTitleManager.getNumber(hWndCtrl) < 0) if (hWndCtrl.posLeft <= absX && hWndCtrl.posRight > absX) @@ -482,11 +500,13 @@ private void registerDebugger() { }); registerKeyTyped('P', () -> Logger.debug("Debugger", "Showing plane info\n" + plane.getDebugMsg())); registerKeyTyped('S', () -> { - String name = "temp/snapshot-" + System.currentTimeMillis() + ".png"; - Pixmap snapshot = Pixmap.createFromFrameBuffer(0, 0, cha.camera.getWidth(), cha.camera.getHeight()); - PixmapIO.writePNG(new FileHandle(name), snapshot); - snapshot.dispose(); - Logger.debug("Debugger", "Snapshot saved to `" + name + "`"); + FileHandle dir = new FileHandle(tempDirPath); + dir.mkdirs(); + FileHandle file = dir.child("snapshot-" + System.currentTimeMillis() + ".png"); + PixmapWrapper pw = PixmapWrapper.fromCamera(cha.camera); + pw.savePixmap(file, true); + pw.dispose(); + Logger.debug("Debugger", "Saved snapshot to: " + file.path()); }); registerKeyTyped('W', () -> { StringBuilder builder = new StringBuilder("Showing window list\n"); diff --git a/core/src/cn/harryh/arkpets/Const.java b/core/src/cn/harryh/arkpets/Const.java index 59978d0b..8dd174cf 100644 --- a/core/src/cn/harryh/arkpets/Const.java +++ b/core/src/cn/harryh/arkpets/Const.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets; @@ -23,7 +23,7 @@ */ public final class Const { // App version - public static final Version appVersion = new Version(3, 9, 2); + public static final Version appVersion = new Version(3, 11, 1); public static final Version datasetLowestVersion = new Version(2, 2, 0); // App name @@ -46,8 +46,8 @@ public final class Const { public static final float skelBaseScale = 0.3f; // Behavior presets - public static final int behaviorBaseWeight = 320; - public static final float droppedThreshold = 10f; + public static final float changeDirectionXThreshold = 4f; + public static final float droppedYThreshold = 10f; // Duration presets public static final Duration durationFast = new Duration(150); @@ -111,6 +111,7 @@ public static class PathConfig { public static final String urlReadme = "https://github.com/isHarryh/Ark-Pets#readme"; public static final String urlLicense = "https://github.com/isHarryh/Ark-Pets"; public static final String urlMirrorChyan = "https://mirrorchyan.com/?source=" + appName + "Gui"; + public static final String urlWikiPrefix = "https://prts.wiki/w/"; public static final String tempDirPath = "temp/"; public static final String fileModelsZipName = "ArkModels"; public static final String fileModelsDataPath = "models_data.json"; @@ -146,23 +147,27 @@ public static class FontsConfig { private static final String fontFileBold = "/fonts/SourceHanSansCN-Bold.otf"; public static void loadFontsToJavafx() { - javafx.scene.text.Font.loadFont(FontsConfig.class.getResourceAsStream(fontFileRegular), - javafx.scene.text.Font.getDefault().getSize()); - javafx.scene.text.Font.loadFont(FontsConfig.class.getResourceAsStream(fontFileBold), - javafx.scene.text.Font.getDefault().getSize()); + if (System.getProperty("arkpets.usesystemfont") == null) { + javafx.scene.text.Font.loadFont(FontsConfig.class.getResourceAsStream(fontFileRegular), + javafx.scene.text.Font.getDefault().getSize()); + javafx.scene.text.Font.loadFont(FontsConfig.class.getResourceAsStream(fontFileBold), + javafx.scene.text.Font.getDefault().getSize()); + } } public static void loadFontsToSwing() { - try { - InputStream in = Objects.requireNonNull(FontsConfig.class.getResourceAsStream(fontFileRegular)); - java.awt.Font font = java.awt.Font.createFont(java.awt.Font.TRUETYPE_FONT, in); - if (font != null) { - UIManager.put("Label.font", font.deriveFont(10f).deriveFont(Font.ITALIC)); - UIManager.put("Menu.font", font.deriveFont(11f)); - UIManager.put("MenuItem.font", font.deriveFont(11f)); + if (System.getProperty("arkpets.usesystemfont") == null) { + try { + InputStream in = Objects.requireNonNull(FontsConfig.class.getResourceAsStream(fontFileRegular)); + java.awt.Font font = java.awt.Font.createFont(java.awt.Font.TRUETYPE_FONT, in); + if (font != null) { + UIManager.put("Label.font", font.deriveFont(10f).deriveFont(Font.ITALIC)); + UIManager.put("Menu.font", font.deriveFont(11f)); + UIManager.put("MenuItem.font", font.deriveFont(11f)); + } + } catch (FontFormatException | IOException e) { + Logger.error("System", "Failed to load tray menu font, details see below.", e); } - } catch (FontFormatException | IOException e) { - Logger.error("System", "Failed to load tray menu font, details see below.", e); } } } diff --git a/core/src/cn/harryh/arkpets/animations/AnimClip.java b/core/src/cn/harryh/arkpets/animations/AnimClip.java index 9d113576..8e15af7b 100644 --- a/core/src/cn/harryh/arkpets/animations/AnimClip.java +++ b/core/src/cn/harryh/arkpets/animations/AnimClip.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.animations; @@ -28,18 +28,18 @@ public class AnimClip { */ public enum AnimType { NONE("", 5), - DEFAULT("^Default.?$", 5), - IDLE("^((Idle)|(Relax)).?$", 5), - MOVE("^Move.?$", 5), + DEFAULT("^Default.{0,2}$", 5), + IDLE("^((Id.?le)|(Relax)).{0,2}$", 5), + MOVE("^Move.{0,2}$", 5), SIT("^Sit$", 50), SLEEP("^Sleep$", 25), SPECIAL("^Special$", 5), INTERACT("^Interact$", 5), - ATTACK("^((Attack)|(Combat)).?$", 5), - SKILL("^Skill.?$", 5), - START("^Start.?$", 5), - DIE("^Die.?$", 5), - REVIVE("^((Revive)|(Reborn)).?$", 5); + ATTACK("^((Attack)|(Combat)).{0,2}$", 5), + SKILL("^Skill.{0,2}$", 5), + START("^Start.{0,2}$", 5), + DIE("^Die.{0,2}$", 5), + REVIVE("^((Revive)|(Reborn)).{0,2}$", 5); /** The regex pattern of this type of animation, which is case-insensitive. */ public final Pattern pattern; @@ -232,8 +232,8 @@ private RecognitionResult recognizeType(ArrayList elements) { } private RecognitionResult recognizeStage(ArrayList elements) { - final AnimType[] exMatchingTypes = new AnimType[]{AnimType.IDLE, AnimType.MOVE/*, AnimType.ATTACK*/}; - final Pattern exMatchingPattern = Pattern.compile("\\d", Pattern.CASE_INSENSITIVE); + final AnimType[] exMatchingTypes = new AnimType[]{AnimType.IDLE, AnimType.MOVE, AnimType.ATTACK}; + final Pattern exMatchingPattern = Pattern.compile("0?(\\d)", Pattern.CASE_INSENSITIVE); for (var iterator = elements.listIterator(); iterator.hasNext(); ) { String s = iterator.next(); /* Simple matching */ @@ -252,9 +252,12 @@ private RecognitionResult recognizeStage(ArrayList elements) if (!separation.fragments().isEmpty()) s1 = separation.fragments().get(0); else if (iterator.hasNext()) s1 = elements.get(iterator.nextIndex()); else break; - if (exMatchingPattern.matcher(s1).matches()) { + + Matcher s1Matcher = exMatchingPattern.matcher(s1); + if (s1Matcher.matches()) { // NO iterator.remove(); - return new RecognitionResult<>(new AnimStage('C' + s1), s1); + String n = s1Matcher.group(1); + return new RecognitionResult<>(new AnimStage('C' + n), s1); } } } diff --git a/core/src/cn/harryh/arkpets/animations/AnimClipGroup.java b/core/src/cn/harryh/arkpets/animations/AnimClipGroup.java index d270ca36..1604099f 100644 --- a/core/src/cn/harryh/arkpets/animations/AnimClipGroup.java +++ b/core/src/cn/harryh/arkpets/animations/AnimClipGroup.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.animations; @@ -7,6 +7,7 @@ import cn.harryh.arkpets.animations.AnimClip.AnimStage; import cn.harryh.arkpets.animations.AnimClip.AnimType; import com.esotericsoftware.spine.Animation; +import com.esotericsoftware.spine.AnimationStateData; import java.util.*; @@ -99,7 +100,7 @@ public AnimClip get(int index) { * A steamed animation is a series of animation which may consist of the {@code BEGIN} animation, the {@code LOOP} * animation and the {@code END} animation. * @param type The specified animation type. - * @return The animation data whose animation clip will be none if not found. + * @return A new animation data, or {@code null} if not found. */ public AnimData getStreamedAnimData(AnimType type) { AnimClipGroup found = this.findAnimations(type); @@ -116,14 +117,14 @@ public AnimData getStreamedAnimData(AnimType type) { result = result.join(new AnimData(end)); return result; } - return new AnimData(null); + return null; } /** Draws a loop animation data from this animation clip group. *


* A loop animation is a single animation which could be played in loop and typically could be interrupted. * @param type The specified animation type. - * @return The animation data whose animation clip will be none if not found. + * @return A new animation data, or {@code null} if not found. */ public AnimData getLoopAnimData(AnimType type) { AnimClipGroup found = this.findAnimations(type); @@ -132,14 +133,14 @@ public AnimData getLoopAnimData(AnimType type) { AnimClip center = loop != null ? loop : none; if (center != null) return new AnimData(center, null, true, false, 0); - return new AnimData(null); + return null; } /** Draws a strict animation data from this animation clip group. *
* A strict animation is a single animation which couldn't be interrupted and typically should be played once. * @param type The specified animation type. - * @return The animation data whose animation clip will be none if not found. + * @return A new animation data, or {@code null} if not found. */ public AnimData getStrictAnimData(AnimType type) { AnimClipGroup found = this.findAnimations(type); @@ -148,7 +149,18 @@ public AnimData getStrictAnimData(AnimType type) { AnimClip center = loop != null ? loop : none; if (center != null) return new AnimData(center, null, false, true); - return new AnimData(null); + return null; + } + + /** Applies complete animation mixing to the target {@link AnimationStateData}. + * @param target The target AnimationStateData to have the mixing applied. + * @param duration The duration of each animation mixing. + */ + public void applyCompleteAnimMix(AnimationStateData target, float duration) { + animClipList.forEach(i -> animClipList.forEach(j -> { + if (!i.fullName.equals(j.fullName)) + target.setMix(i.fullName, j.fullName, duration); + })); } protected void sortStages() { diff --git a/core/src/cn/harryh/arkpets/animations/AnimComposer.java b/core/src/cn/harryh/arkpets/animations/AnimComposer.java index 90aea8a7..621407b2 100644 --- a/core/src/cn/harryh/arkpets/animations/AnimComposer.java +++ b/core/src/cn/harryh/arkpets/animations/AnimComposer.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2024, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.animations; @@ -17,12 +17,12 @@ public AnimComposer(AnimationState boundState) { state.addListener(new AnimationState.AnimationStateAdapter() { @Override public void complete(AnimationState.TrackEntry entry) { - if (composer.playing != null && !composer.playing.isEmpty() && entry.getAnimation() != null) { + if (composer.playing != null && entry.getAnimation() != null) { if (entry.getAnimation().getName().equals(composer.playing.animClip().fullName)) { AnimData completed = composer.playing; if (!completed.isLoop()) { composer.reset(); - if (completed.animNext() != null && !completed.animNext().isEmpty()) { + if (completed.animNext() != null) { composer.offer(completed.animNext()); } } @@ -33,8 +33,8 @@ public void complete(AnimationState.TrackEntry entry) { } public boolean offer(AnimData animData) { - if (animData != null && !animData.isEmpty()) { - if (playing == null || playing.isEmpty() || (!playing.isStrict() && !playing.equals(animData))) { + if (animData != null) { + if (playing == null || (!playing.isStrict() && !playing.equals(animData))) { playing = animData; state.setAnimation(coreTrackId, playing.name(), playing.isLoop()); onApply(playing); diff --git a/core/src/cn/harryh/arkpets/animations/AnimData.java b/core/src/cn/harryh/arkpets/animations/AnimData.java index b3bb8855..effbcade 100644 --- a/core/src/cn/harryh/arkpets/animations/AnimData.java +++ b/core/src/cn/harryh/arkpets/animations/AnimData.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.animations; @@ -20,6 +20,11 @@ public record AnimData( boolean isStrict, int mobility ) { + public AnimData { + if (animClip == null) + throw new IllegalArgumentException("animClip cannot be null"); + } + /** Animation data record (simplified constructor). * @param animClip The animation clip of THIS animation data. */ @@ -56,12 +61,8 @@ public AnimData join(AnimData animNext) { return new AnimData(this.animClip, this.animNext.join(animNext), this.isLoop, this.isStrict, this.mobility); } - public boolean isEmpty() { - return animClip == null; - } - public String name() { - return isEmpty() ? null : animClip.fullName; + return animClip.fullName; } @Override diff --git a/core/src/cn/harryh/arkpets/animations/AnimDataWeight.java b/core/src/cn/harryh/arkpets/animations/AnimDataWeight.java index 14b3e47b..7a4dc420 100644 --- a/core/src/cn/harryh/arkpets/animations/AnimDataWeight.java +++ b/core/src/cn/harryh/arkpets/animations/AnimDataWeight.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.animations; diff --git a/core/src/cn/harryh/arkpets/animations/Behavior.java b/core/src/cn/harryh/arkpets/animations/Behavior.java index 954ce8a5..d7d20045 100644 --- a/core/src/cn/harryh/arkpets/animations/Behavior.java +++ b/core/src/cn/harryh/arkpets/animations/Behavior.java @@ -1,34 +1,38 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.animations; +import cn.harryh.arkpets.animations.StochasticMatrix.StochasticState; import cn.harryh.arkpets.utils.Cached; -import java.util.Arrays; - abstract public class Behavior { - protected AnimClipGroup animList; - protected AnimDataWeight[] actionList; - protected Cached actionAutoGetter; - private int idxRec; + protected final Cached actionAutoGetter; + protected StochasticMatrix currentMatrix; + protected StochasticState currentState; private static final double minAnimCacheAge = 0.5; /** Character Behavior Controller Instance. - * @param animList The animation clip list. */ - public Behavior(AnimClipGroup animList) { - actionList = null; - this.animList = animList; + public Behavior() { actionAutoGetter = new Cached<>(); - actionAutoGetter.setValueProducer(this::getRandomAction); + actionAutoGetter.setValueProducer(() -> { + StochasticState newState = currentMatrix.transitedAnimOf(currentState); + if (newState == null) { + return currentMatrix.getStateAnim(currentState); + } else { + currentState = newState; + return currentMatrix.getStateAnim(newState); + } + }); actionAutoGetter.setCacheAgeProducer(() -> { AnimData cache = actionAutoGetter.getCachedValue(); return cache == null ? minAnimCacheAge : Math.max(minAnimCacheAge, cache.animClip().duration); }); - idxRec = 0; + currentMatrix = null; + currentState = null; } /** Checks whether the random animation is expired or empty. @@ -38,7 +42,7 @@ public final boolean isAutoAnimExpired() { return actionAutoGetter.isExpired(); } - /** Gets a random animation. + /** Gets a random animation. This method has caching mechanism. * @return AnimData object. */ public final AnimData autoAnim() { @@ -49,81 +53,70 @@ public final AnimData autoAnim() { * @return AnimData object. */ public final AnimData nextAnim() { - if (actionList.length > 0) { - idxRec = idxRec >= actionList.length - 1 ? 0 : idxRec + 1; - return actionList[idxRec].anim(); - } - return new AnimData(null); + if (currentMatrix.isAllDisabled()) return null; + AnimData newAnim = currentMatrix.nextAnimOf(currentState); + currentState = currentState.next(); + return newAnim; } /** Gets the previous animation. - * @return AnimData object. + * @return Animation data, or {@code null} if not available. */ public final AnimData prevAnim() { - if (actionList.length > 0) { - idxRec = idxRec <= 0 ? actionList.length - 1 : idxRec - 1; - return actionList[idxRec].anim(); - } - return new AnimData(null); - } - - private AnimData getRandomAction() { - if (actionList.length > 0) { - // Calculate the sum of all action's weight - int weightSum = Arrays.stream(actionList).mapToInt(AnimDataWeight::weight).sum(); - // Random select a weight - int weightSelect = (int) Math.ceil(Math.random() * weightSum); - // Figure out which action is selected - int weight = 0; - for (AnimDataWeight animDataWeight : actionList) { - weight += animDataWeight.weight(); - if (weightSelect <= weight) - return animDataWeight.anim(); - } - } - return new AnimData(null); + if (currentMatrix.isAllDisabled()) return null; + AnimData newAnim = currentMatrix.prevAnimOf(currentState); + currentState = currentState.prev(); + return newAnim; } /** Gets the default animation. - * @return AnimData object. + * @return Animation data, or {@code null} if not available. */ public AnimData defaultAnim() { - return new AnimData(null); + return null; } /** Gets the walk animation. * @param mobility 1=GoRight, -1=GoLeft. - * @return AnimData object. + * @return Animation data, or {@code null} if not available. */ public AnimData walkAnim(int mobility) { - return new AnimData(null); + return null; } /** Gets the animation when mouse-down. - * @return AnimData object. + * @return Animation data, or {@code null} if not available. */ public AnimData clickStart() { - return new AnimData(null); + return null; } /** Gets the animation when mouse-up. - * @return AnimData object. + * @return Animation data, or {@code null} if not available. */ public AnimData clickEnd() { - return new AnimData(null); + return null; } /** Gets the animation when the user starts dragging. - * @return AnimData object. + * @return Animation data, or {@code null} if not available. */ public AnimData dragging() { - return new AnimData(null); + return null; } /** Gets the animation when character dropped. - * @return AnimData object. + * @return Animation data, or {@code null} if not available. */ public AnimData dropped() { - return new AnimData(null); + return null; + } + + public int[][] getDebugMatrix() { + return currentMatrix.getDebugMatrix(); + } + + public StochasticState getCurrentMatrixState() { + return currentState; } } diff --git a/core/src/cn/harryh/arkpets/animations/GeneralBehavior.java b/core/src/cn/harryh/arkpets/animations/GeneralBehavior.java index 20f5a0b6..4a85d357 100644 --- a/core/src/cn/harryh/arkpets/animations/GeneralBehavior.java +++ b/core/src/cn/harryh/arkpets/animations/GeneralBehavior.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.animations; @@ -6,11 +6,10 @@ import cn.harryh.arkpets.ArkConfig; import cn.harryh.arkpets.animations.AnimClip.AnimStage; import cn.harryh.arkpets.animations.AnimClip.AnimType; +import cn.harryh.arkpets.animations.StochasticMatrix.StochasticState; import java.util.*; -import static cn.harryh.arkpets.Const.behaviorBaseWeight; - public class GeneralBehavior extends Behavior { protected ArkConfig config; @@ -19,19 +18,16 @@ public class GeneralBehavior extends Behavior { protected Iterator stageItr; protected final ArrayList stageList; protected final HashMap stageAnimMap; - protected final HashMap stageAnimWeightMap; + protected final HashMap stageAnimWeightMap; public GeneralBehavior(ArkConfig config, AnimClipGroup animList) { - super(animList); + super(); this.config = config; - stageAnimMap = this.animList.clusterByStage(); + stageAnimMap = animList.clusterByStage(); + stageAnimMap.entrySet().removeIf(e -> e.getValue().getLoopAnimData(AnimType.IDLE) == null); stageAnimWeightMap = new HashMap<>(); - for (AnimStage key : stageAnimMap.keySet()) { - AnimDataWeight[] temp = getActionList(stageAnimMap.get(key)); - if (temp.length > 0) - stageAnimWeightMap.put(key, temp); - } + stageAnimMap.forEach((k, v) -> stageAnimWeightMap.put(k, getMatrix(v))); stageList = new ArrayList<>(stageAnimWeightMap.keySet().stream().toList()); stageList.sort(Comparator.comparing(AnimStage::id)); @@ -39,16 +35,56 @@ public GeneralBehavior(ArkConfig config, AnimClipGroup animList) { throw new NoSuchElementException("Animation stage map was empty because no animation's name was matched."); stageItr = stageList.iterator(); - actionList = new AnimDataWeight[0]; nextStage(); } + private StochasticMatrix getMatrix(AnimClipGroup animClips) { + StochasticMatrix mat = new StochasticMatrix(StochasticMatrix.DEFAULT_WEIGHTS); + + // Bind and disable states based on config + AnimData idleAnim, sitAnim, sleepAnim, moveAnim, specialAnim; + if ((idleAnim = animClips.getLoopAnimData(AnimType.IDLE)) != null) { + mat.bind(StochasticState.IDLE, idleAnim); + } else { + mat.disable(StochasticState.IDLE); + } + if ((sitAnim = animClips.getLoopAnimData(AnimType.SIT)) != null && config.behavior_allow_sit) { + mat.bind(StochasticState.SIT, sitAnim); + } else { + mat.disable(StochasticState.SIT); + } + if ((sleepAnim = animClips.getLoopAnimData(AnimType.SLEEP)) != null && config.behavior_allow_sleep) { + mat.bind(StochasticState.SLEEP, sleepAnim); + } else { + mat.disable(StochasticState.SLEEP); + } + if ((moveAnim = animClips.getLoopAnimData(AnimType.MOVE)) != null && config.behavior_allow_walk) { + mat.bind(StochasticState.MOVE_L, moveAnim.derive(-1)); + mat.bind(StochasticState.MOVE_R, moveAnim.derive(+1)); + } else { + mat.disable(StochasticState.MOVE_L); + mat.disable(StochasticState.MOVE_R); + } + if ((specialAnim = animClips.getStrictAnimData(AnimType.SPECIAL)) != null && config.behavior_allow_special) { + mat.bind(StochasticState.SPECIAL, specialAnim.join(idleAnim == null ? specialAnim : idleAnim)); + } else { + mat.disable(StochasticState.SPECIAL); + } + + // Scale idle weights based on activation value + float factor = 1 + (8 - Math.max(0, Math.min(16, config.behavior_ai_activation))) / 8f; + mat.scale(StochasticState.IDLE, factor); + + return mat; + } + public void nextStage() { if (!stageItr.hasNext()) stageItr = stageList.iterator(); stageCur = stageItr.next(); stageAnimList = stageAnimMap.get(stageCur); - actionList = stageAnimWeightMap.get(stageCur); + currentMatrix = stageAnimWeightMap.get(stageCur); + currentState = StochasticState.IDLE; actionAutoGetter.removeCachedValue(); } @@ -60,37 +96,6 @@ public AnimStage getCurrentStage() { return stageCur; } - private AnimDataWeight[] getActionList(AnimClipGroup animList) { - ArrayList actionList = new ArrayList<>(List.of( - new AnimDataWeight( - animList.getLoopAnimData(AnimType.IDLE), - Math.round(behaviorBaseWeight / (float) Math.sqrt(config.behavior_ai_activation)) - ), - new AnimDataWeight( - animList.getLoopAnimData(AnimType.SIT), - config.behavior_allow_sit ? 1 << 6 : 0 - ), - new AnimDataWeight( - animList.getLoopAnimData(AnimType.SLEEP), - config.behavior_allow_sleep ? 1 << 5 : 0 - ), - new AnimDataWeight( - animList.getLoopAnimData(AnimType.MOVE).derive(+1), - config.behavior_allow_walk ? 1 << 5 : 0 - ), - new AnimDataWeight( - animList.getLoopAnimData(AnimType.MOVE).derive(-1), - config.behavior_allow_walk ? 1 << 5 : 0), - new AnimDataWeight( - animList.getStrictAnimData(AnimType.SPECIAL) - .join(animList.getLoopAnimData(AnimType.IDLE)), - config.behavior_allow_special ? 1 << 4 : 0 - ) - )); - actionList.removeIf(e -> e.anim().isEmpty()); - return actionList.toArray(new AnimDataWeight[0]); - } - @Override public AnimData defaultAnim() { return stageAnimList.getLoopAnimData(AnimType.IDLE); @@ -98,14 +103,16 @@ public AnimData defaultAnim() { @Override public AnimData walkAnim(int mobility) { - return stageAnimList.getLoopAnimData(AnimType.MOVE).derive(mobility); + AnimData anim = stageAnimList.getLoopAnimData(AnimType.MOVE); + return anim == null ? null : anim.derive(mobility); } @Override public AnimData clickEnd() { AnimData a1 = stageAnimList.getStreamedAnimData(AnimType.ATTACK); AnimData a2 = stageAnimList.getStreamedAnimData(AnimType.INTERACT); - AnimData a3 = a2.isEmpty() ? a1 : a2; + AnimData a3 = a2 == null ? a1 : a2; + if (a3 == null) return defaultAnim(); return new AnimData(a3.animClip(), a3.animNext(), false, true, a3.mobility()).join(defaultAnim()); } diff --git a/core/src/cn/harryh/arkpets/animations/StochasticMatrix.java b/core/src/cn/harryh/arkpets/animations/StochasticMatrix.java new file mode 100644 index 00000000..01477c03 --- /dev/null +++ b/core/src/cn/harryh/arkpets/animations/StochasticMatrix.java @@ -0,0 +1,148 @@ +/** Copyright (c) 2022-2026, Harry Huang, Litwak913 + * At GPL-3.0 License + */ +package cn.harryh.arkpets.animations; + +import java.util.stream.IntStream; + + +/** The stochastic matrix (markov matrix) to manage the transition of auto-played animations in a natural way. + */ +public class StochasticMatrix { + protected final StochasticMatrixRow[] weights; + protected final boolean[] disabled; + protected final AnimData[] binds; + + public static int[][] DEFAULT_WEIGHTS = new int[][]{ + // IDLE SIT SLEEP MOVE_L MOVE_R SPECIAL + {40, 20, 10, 10, 10, 10}, // IDLE -> ? + {30, 40, 20, 10, 10, 10}, // SIT -> ? + {20, 20, 60, 0, 0, 0}, // SLEEP -> ? + {40, 10, 0, 20, 20, 10}, // MOVE_L -> ? + {40, 10, 0, 20, 20, 10}, // MOVE_R -> ? + {50, 20, 10, 10, 10, 0} // SPECIAL -> ? + }; + + /** One stochastic state corresponding to an auto-played animation. + */ + public enum StochasticState { + IDLE, + SIT, + SLEEP, + MOVE_L, + MOVE_R, + SPECIAL; + + public StochasticState next() { + int ord = (ordinal() + 1) % StochasticState.values().length; + return StochasticState.values()[ord]; + } + + public StochasticState prev() { + int ord = (ordinal() - 1 + StochasticState.values().length) % StochasticState.values().length; + return StochasticState.values()[ord]; + } + } + + /** One row of the stochastic matrix. Each element represents the weight of transition to the corresponding state. + * @param weights The weights array of the states. + * @param disabledRef The reference to the disabled states array. If a state is disabled, its weight is ignored. + */ + public record StochasticMatrixRow(int[] weights, boolean[] disabledRef) { + public StochasticMatrixRow { + if (weights.length != StochasticState.values().length) + throw new IllegalArgumentException("Weights length mismatch"); + } + + public StochasticState random() { + int sum = IntStream.range(0, weights.length).filter(i -> !disabledRef[i]).map(i -> weights[i]).sum(); + int rnd = (int) (Math.random() * sum); + int acc = 0; + for (int i = 0; i < weights.length; i++) { + if (disabledRef[i]) + continue; + acc += weights[i]; + if (rnd < acc) + return StochasticState.values()[i]; + } + return null; + } + } + + /** Initializes the stochastic matrix with given weights. + * @param weights The weights 2D array. Each row corresponds to a state, + * and each column corresponds to the weight of transition to another state. + */ + public StochasticMatrix(int[][] weights) { + if (weights.length != StochasticState.values().length) + throw new IllegalArgumentException("Weights length mismatch"); + this.weights = new StochasticMatrixRow[weights.length]; + this.disabled = new boolean[StochasticState.values().length]; + this.binds = new AnimData[StochasticState.values().length]; + for (int i = 0; i < weights.length; i++) + this.weights[i] = new StochasticMatrixRow(weights[i], this.disabled); + } + + public AnimData nextAnimOf(StochasticState state) { + StochasticState newState = state; + for (int i = 0; i < StochasticState.values().length; i++) { + newState = newState.next(); + if (!disabled[newState.ordinal()]) + return binds[newState.ordinal()]; + } + return binds[state.ordinal()]; + } + + public AnimData prevAnimOf(StochasticState state) { + StochasticState newState = state; + for (int i = 0; i < StochasticState.values().length; i++) { + newState = newState.prev(); + if (!disabled[newState.ordinal()]) + return binds[newState.ordinal()]; + } + return binds[state.ordinal()]; + } + + public StochasticState transitedAnimOf(StochasticState state) { + return weights[state.ordinal()].random(); + } + + public AnimData getStateAnim(StochasticState state) { + return binds[state.ordinal()]; + } + + public void bind(StochasticState state, AnimData anim) { + binds[state.ordinal()] = anim; + } + + public void scale(StochasticState state, float factor) { + if (factor < 0) + throw new IllegalArgumentException("Scale factor cannot be positive"); + int ord = state.ordinal(); + for (int i = 0; i < weights[ord].weights.length; i++) + weights[ord].weights[i] = Math.round(weights[ord].weights[i] * factor); + } + + public void disable(StochasticState state) { + disabled[state.ordinal()] = true; + } + + public boolean isAllDisabled() { + for (boolean d : disabled) + if (!d) + return false; + return true; + } + + public int[][] getDebugMatrix() { + int[][] matrix = new int[StochasticState.values().length][StochasticState.values().length]; + for (int i = 0; i < weights.length; i++) { + StochasticMatrixRow row = weights[i]; + for (int j = 0; j < row.weights.length; j++) { + if (row.disabledRef[j]) matrix[i][j] = -row.weights[j]; + else matrix[i][j] = row.weights[j]; + } + } + return matrix; + } +} diff --git a/core/src/cn/harryh/arkpets/assets/ModelItem.java b/core/src/cn/harryh/arkpets/assets/ModelItem.java index 25556ed1..37d0fbe0 100644 --- a/core/src/cn/harryh/arkpets/assets/ModelItem.java +++ b/core/src/cn/harryh/arkpets/assets/ModelItem.java @@ -1,12 +1,12 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.assets; import cn.harryh.arkpets.utils.Logger; -import com.alibaba.fastjson.JSONArray; -import com.alibaba.fastjson.JSONObject; -import com.alibaba.fastjson.annotation.JSONField; +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import com.alibaba.fastjson2.annotation.JSONField; import com.github.promeg.pinyinhelper.Pinyin; import java.io.File; @@ -188,7 +188,7 @@ public ModelAssetAccessor(JSONObject fileMap) { list.addAll(temp); map.put(fileType, new ArrayList<>(temp)); } - } catch (com.alibaba.fastjson.JSONException | com.alibaba.fastjson2.JSONException ex) { + } catch (com.alibaba.fastjson2.JSONException ex) { String oneFile; // Try to get as string if ((oneFile = fileMap.getString(fileType)) != null) { list.add(oneFile); diff --git a/core/src/cn/harryh/arkpets/assets/ModelItemGroup.java b/core/src/cn/harryh/arkpets/assets/ModelItemGroup.java index 250e7eeb..55c98d1a 100644 --- a/core/src/cn/harryh/arkpets/assets/ModelItemGroup.java +++ b/core/src/cn/harryh/arkpets/assets/ModelItemGroup.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.assets; diff --git a/core/src/cn/harryh/arkpets/assets/ModelsDataset.java b/core/src/cn/harryh/arkpets/assets/ModelsDataset.java index 1840395c..1c20b5b4 100644 --- a/core/src/cn/harryh/arkpets/assets/ModelsDataset.java +++ b/core/src/cn/harryh/arkpets/assets/ModelsDataset.java @@ -1,11 +1,11 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.assets; import cn.harryh.arkpets.utils.Version; -import com.alibaba.fastjson.JSONObject; -import com.alibaba.fastjson.annotation.JSONField; +import com.alibaba.fastjson2.JSONObject; +import com.alibaba.fastjson2.annotation.JSONField; import java.io.File; import java.io.Serializable; diff --git a/core/src/cn/harryh/arkpets/assets/SkeletonLoader.java b/core/src/cn/harryh/arkpets/assets/SkeletonLoader.java new file mode 100644 index 00000000..19274481 --- /dev/null +++ b/core/src/cn/harryh/arkpets/assets/SkeletonLoader.java @@ -0,0 +1,318 @@ +/** Copyright (c) 2022-2026, Harry Huang + * At GPL-3.0 License + */ +package cn.harryh.arkpets.assets; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.badlogic.gdx.files.FileHandle; +import com.badlogic.gdx.graphics.Texture.TextureFilter; +import com.badlogic.gdx.graphics.g2d.TextureAtlas; +import com.badlogic.gdx.graphics.g2d.TextureAtlas.TextureAtlasData; +import com.esotericsoftware.spine.SkeletonBinary; +import com.esotericsoftware.spine.SkeletonData; +import com.esotericsoftware.spine.SkeletonJson; + +import java.io.*; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import static cn.harryh.arkpets.Const.PathConfig.tempDirPath; + + +public class SkeletonLoader { + protected final FileHandle file; + protected final boolean isJson; + protected final long headerSize; + + public final String hash; + public final String version; + public final float x, y, width, height; + public final boolean nonEssential; + public final float fps; + public final String images_path, audio_path; + public final List strings; + + protected static final int MAX_SKELETON_FILE_SIZE = 64 << 20; + + /** Initializes a Spine skeleton loader from a file handle. + * @param file The file handle of a skeleton file which can be either JSON format or binary format. + * @throws IOException If I/O error occurs. + */ + public SkeletonLoader(FileHandle file) throws IOException { + this.file = file; + if (file.length() > MAX_SKELETON_FILE_SIZE) { + throw new IOException("Skeleton file is to large"); + } + isJson = isJson(file); + + try (InputStream is = file.read()) { + if (isJson) { + // As JSON: + String content = new String(is.readAllBytes(), StandardCharsets.UTF_8); + JSONObject jsonData = Objects.requireNonNull(JSON.parseObject(content), + "Cannot load skel json"); + JSONObject skelData = Objects.requireNonNull(jsonData.getJSONObject("skeleton"), + "Cannot find skeleton data in skel json"); + hash = skelData.getString("hash"); + version = skelData.getString("spine"); + x = skelData.getFloatValue("x"); + y = skelData.getFloatValue("y"); + width = skelData.getFloatValue("width"); + height = skelData.getFloatValue("height"); + nonEssential = false; + fps = skelData.containsKey("fps") ? skelData.getFloatValue("fps") : 30.0f; + images_path = skelData.getString("images"); + audio_path = skelData.getString("audio"); + strings = Collections.emptyList(); + headerSize = 0; + } else { + // As binary: + long[] bytesRead = {0}; + hash = readString(is, bytesRead); + version = readString(is, bytesRead); + x = readFloat(is, bytesRead); + y = readFloat(is, bytesRead); + width = readFloat(is, bytesRead); + height = readFloat(is, bytesRead); + nonEssential = readBoolean(is, bytesRead); + float fps; + String imagesPath, audioPath; + if (nonEssential) { + fps = readFloat(is, bytesRead); + imagesPath = readString(is, bytesRead); + if (imagesPath.isEmpty()) + imagesPath = null; + audioPath = readString(is, bytesRead); + if (audioPath.isEmpty()) + audioPath = null; + } else { + fps = 30.0f; + imagesPath = null; + audioPath = null; + } + int stringCount = readVarInt(is, bytesRead); + ArrayList strings = new ArrayList<>(stringCount); + for (int i = 0; i < stringCount; i++) { + strings.add(readString(is, bytesRead)); + } + this.strings = Collections.unmodifiableList(strings); + this.fps = fps; + this.images_path = imagesPath; + this.audio_path = audioPath; + headerSize = bytesRead[0]; + } + } + } + + /** Gets whether the skeleton file is in JSON format. + * @return True if the skeleton file is in JSON format; false if in binary format. + */ + public boolean isJson() { + return isJson; + } + + /** Checks if the skeleton file needs to be fixed. + * @return True if it needs to be fixed; false otherwise. + * @see ArkPets Issue #150 + */ + public boolean needFix() { + if (isJson) { + return false; + } + for (String s : strings) { + if (s.matches(".*\\s")) { + return true; + } + } + return false; + } + + /** Returns a skeleton loader instance with fixed skeleton. + * @return A new skeleton loader instance with fixed skeleton, or this instance if no fix is needed. + * @throws IOException If I/O error occurs. + * @see #needFix() + */ + public SkeletonLoader fixed() throws IOException { + if (!needFix()) { + return this; + } + File tempFile = File.createTempFile("fixed_", "_" + file.name(), new File(tempDirPath)); + tempFile.deleteOnExit(); + FileHandle tempHandle = new FileHandle(tempFile); + try (OutputStream os = tempHandle.write(false)) { + // Write header + writeString(os, ""); // Empty hash + writeString(os, version); + writeFloat(os, x); + writeFloat(os, y); + writeFloat(os, width); + writeFloat(os, height); + writeBoolean(os, nonEssential); + if (nonEssential) { + writeFloat(os, fps); + writeString(os, images_path != null ? images_path : ""); + writeString(os, audio_path != null ? audio_path : ""); + } + // Write strings pool + List fixedStrings = strings.stream() + .map(s -> s.replaceAll("\\s+$", "")) + .toList(); + writeVarInt(os, fixedStrings.size()); + for (String s : fixedStrings) { + writeString(os, s); + } + // Write the remaining data + try (InputStream is = file.read()) { + long toSkip = headerSize; + while (toSkip > 0) { + long skipped = is.skip(toSkip); + if (skipped <= 0) { + throw new IOException("Failed to skip header"); + } + toSkip -= skipped; + } + byte[] buffer = new byte[8192]; + int read; + while ((read = is.read(buffer)) != -1) { + os.write(buffer, 0, read); + } + } + } + return new SkeletonLoader(tempHandle); + } + + /** Loads the skeleton data with the given texture atlas. + * @param atlas The texture atlas to be attached to the skeleton data. + * @param scale The scale factor to be applied to the skeleton data. + * @return A skeleton data instance. + */ + public SkeletonData loadSkeletonDataWith(TextureAtlas atlas, float scale) { + if (isJson) { + SkeletonJson json = new SkeletonJson(atlas); + json.setScale(scale); + return json.readSkeletonData(file); + } else { + SkeletonBinary binary = new SkeletonBinary(atlas); + binary.setScale(scale); + return binary.readSkeletonData(file); + } + } + + /** Loads the skeleton data with the texture atlas from the given atlas file. + * @param atlasFile The file handle of the texture atlas file. + * @param scale The scale factor to be applied to the skeleton data. + * @param forceMipmap Whether to force enable mipmapping for the atlas textures. + * @return A skeleton data instance. + */ + public SkeletonData loadSkeletonDataWith(FileHandle atlasFile, float scale, boolean forceMipmap) { + TextureAtlasData atlasData = new TextureAtlasData(atlasFile, atlasFile.parent(), false); + if (forceMipmap) { + for (TextureAtlasData.Page page : atlasData.getPages()) { + page.minFilter = TextureFilter.MipMapLinearLinear; + page.useMipMaps = true; + } + } + return loadSkeletonDataWith(new TextureAtlas(atlasData), scale); + } + + protected static boolean isJson(FileHandle file) throws IOException { + try (InputStream is = file.read()) { + byte[] buffer = new byte[1024]; + int bytesRead = is.read(buffer); + if (bytesRead == -1) { + throw new IOException("Empty skeleton file"); + } + for (int i = 0; i < bytesRead; i++) { + char c = (char) buffer[i]; + if (c != ' ' && c != '\n' && c != '\r' && c != '\t') { + return c == '{'; + } + } + throw new IOException("Cannot determine skeleton file format"); + } + } + + protected static boolean readBoolean(InputStream is, long[] bytesRead) throws IOException { + int b = is.read(); + if (b == -1) { + throw new EOFException("Unexpected end of stream"); + } + bytesRead[0] += 1; + return b != 0; + } + + protected static float readFloat(InputStream is, long[] bytesRead) throws IOException { + byte[] bytes = new byte[4]; + int read = is.read(bytes); + if (read != 4) { + throw new EOFException("Unexpected end of stream"); + } + bytesRead[0] += 4; + ByteBuffer bb = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); + return bb.getFloat(); + } + + protected static int readVarInt(InputStream is, long[] bytesRead) throws IOException { + int result = 0; + for (int i = 0; i < 5; i++) { + int b = is.read(); + if (b == -1) { + throw new EOFException("Unexpected end of stream"); + } + bytesRead[0] += 1; + result |= (b & 0x7F) << (i * 7); + if ((b & 0x80) == 0) { + break; + } + } + return result; + } + + protected static String readString(InputStream is, long[] bytesRead) throws IOException { + int length = readVarInt(is, bytesRead); + if (length <= 1) { + return ""; + } + byte[] b = new byte[length - 1]; + int read = is.read(b); + if (read != length - 1) { + throw new EOFException("Unexpected end of stream"); + } + bytesRead[0] += length - 1; + return new String(b, StandardCharsets.UTF_8); + } + + protected static void writeBoolean(OutputStream os, boolean value) throws IOException { + os.write(value ? 1 : 0); + } + + protected static void writeFloat(OutputStream os, float value) throws IOException { + ByteBuffer bb = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN); + bb.putFloat(value); + os.write(bb.array()); + } + + protected static void writeVarInt(OutputStream os, int value) throws IOException { + do { + int b = value & 0x7F; + value >>>= 7; + if (value != 0) { + b |= 0x80; + } + os.write(b); + } while (value != 0); + } + + protected static void writeString(OutputStream os, String value) throws IOException { + byte[] bytes = value.getBytes(StandardCharsets.UTF_8); + int length = bytes.length + 1; + writeVarInt(os, length); + os.write(bytes); + } +} diff --git a/core/src/cn/harryh/arkpets/concurrent/PortUtils.java b/core/src/cn/harryh/arkpets/concurrent/PortUtils.java index b34c86f8..cd0c4641 100644 --- a/core/src/cn/harryh/arkpets/concurrent/PortUtils.java +++ b/core/src/cn/harryh/arkpets/concurrent/PortUtils.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang, Half Nothing +/** Copyright (c) 2022-2026, Harry Huang, Half Nothing * At GPL-3.0 License */ package cn.harryh.arkpets.concurrent; diff --git a/core/src/cn/harryh/arkpets/concurrent/ProcessPool.java b/core/src/cn/harryh/arkpets/concurrent/ProcessPool.java index 96cebe83..51417a45 100644 --- a/core/src/cn/harryh/arkpets/concurrent/ProcessPool.java +++ b/core/src/cn/harryh/arkpets/concurrent/ProcessPool.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang, Half Nothing +/** Copyright (c) 2022-2026, Harry Huang, Half Nothing * At GPL-3.0 License */ package cn.harryh.arkpets.concurrent; diff --git a/core/src/cn/harryh/arkpets/concurrent/SocketClient.java b/core/src/cn/harryh/arkpets/concurrent/SocketClient.java index 5049efab..d78a916b 100644 --- a/core/src/cn/harryh/arkpets/concurrent/SocketClient.java +++ b/core/src/cn/harryh/arkpets/concurrent/SocketClient.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang, Half Nothing +/** Copyright (c) 2022-2026, Harry Huang, Half Nothing * At GPL-3.0 License */ package cn.harryh.arkpets.concurrent; diff --git a/core/src/cn/harryh/arkpets/concurrent/SocketData.java b/core/src/cn/harryh/arkpets/concurrent/SocketData.java index c6927f11..a042fd0b 100644 --- a/core/src/cn/harryh/arkpets/concurrent/SocketData.java +++ b/core/src/cn/harryh/arkpets/concurrent/SocketData.java @@ -1,10 +1,10 @@ -/** Copyright (c) 2022-2025, Harry Huang, Half Nothing +/** Copyright (c) 2022-2026, Harry Huang, Half Nothing * At GPL-3.0 License */ package cn.harryh.arkpets.concurrent; -import com.alibaba.fastjson.JSONObject; -import com.alibaba.fastjson.annotation.JSONField; +import com.alibaba.fastjson2.JSONObject; +import com.alibaba.fastjson2.annotation.JSONField; import java.io.Serializable; import java.nio.charset.Charset; diff --git a/core/src/cn/harryh/arkpets/concurrent/SocketServer.java b/core/src/cn/harryh/arkpets/concurrent/SocketServer.java index 1a55c729..e70f942f 100644 --- a/core/src/cn/harryh/arkpets/concurrent/SocketServer.java +++ b/core/src/cn/harryh/arkpets/concurrent/SocketServer.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang, Half Nothing +/** Copyright (c) 2022-2026, Harry Huang, Half Nothing * At GPL-3.0 License */ package cn.harryh.arkpets.concurrent; diff --git a/core/src/cn/harryh/arkpets/concurrent/SocketSession.java b/core/src/cn/harryh/arkpets/concurrent/SocketSession.java index dcab2cab..3b7b0d4e 100644 --- a/core/src/cn/harryh/arkpets/concurrent/SocketSession.java +++ b/core/src/cn/harryh/arkpets/concurrent/SocketSession.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang, Half Nothing +/** Copyright (c) 2022-2026, Harry Huang, Half Nothing * At GPL-3.0 License */ package cn.harryh.arkpets.concurrent; diff --git a/core/src/cn/harryh/arkpets/natives/CoreGraphics.java b/core/src/cn/harryh/arkpets/natives/CoreGraphics.java index 813ab7df..2b5baadd 100644 --- a/core/src/cn/harryh/arkpets/natives/CoreGraphics.java +++ b/core/src/cn/harryh/arkpets/natives/CoreGraphics.java @@ -31,6 +31,10 @@ public interface CoreGraphics extends Library { CFDictionaryRef CGSessionCopyCurrentDictionary(); + int CGMainDisplayID(); + + CGRect.ByValue CGDisplayBounds(int display); + @Structure.FieldOrder({"origin", "size"}) class CGRect extends Structure { @@ -48,6 +52,9 @@ public static class ByValue extends CGRect implements Structure.ByValue { class CGPoint extends Structure { public double x; public double y; + + public static class ByValue extends CGPoint implements Structure.ByValue { + } } @Structure.FieldOrder({"width", "height"}) diff --git a/core/src/cn/harryh/arkpets/natives/HIServices.java b/core/src/cn/harryh/arkpets/natives/HIServices.java new file mode 100644 index 00000000..a7702b01 --- /dev/null +++ b/core/src/cn/harryh/arkpets/natives/HIServices.java @@ -0,0 +1,14 @@ +package cn.harryh.arkpets.natives; + +import com.sun.jna.Library; +import com.sun.jna.Native; +import com.sun.jna.Pointer; + + +public interface HIServices extends Library { + HIServices INSTANCE = Native.load("ApplicationServices", HIServices.class); + + Pointer AXUIElementCreateSystemWide(); + + boolean AXIsProcessTrusted(); +} diff --git a/core/src/cn/harryh/arkpets/natives/TaskbarList.java b/core/src/cn/harryh/arkpets/natives/TaskbarList.java new file mode 100644 index 00000000..4ee46666 --- /dev/null +++ b/core/src/cn/harryh/arkpets/natives/TaskbarList.java @@ -0,0 +1,69 @@ +package cn.harryh.arkpets.natives; + +import com.sun.jna.Pointer; +import com.sun.jna.platform.win32.*; +import com.sun.jna.platform.win32.COM.COMUtils; +import com.sun.jna.platform.win32.COM.Unknown; +import com.sun.jna.ptr.PointerByReference; + + +/** The Windows taskbar controller that implements the ITaskbarList COM interface. + * @see ITaskbarList + */ +public class TaskbarList extends Unknown { + private static final Guid.GUID CLSID = new Guid.GUID("{56FDF344-FD6D-11d0-958A-006097C9A090}"); + private static final Guid.GUID IID = new Guid.GUID("{56FDF342-FD6D-11d0-958A-006097C9A090}"); + private boolean initialized = false; + + private TaskbarList(Pointer ptr) { + super(ptr); + } + + public static TaskbarList create() { + Ole32.INSTANCE.CoInitialize(null); + PointerByReference p = new PointerByReference(); + WinNT.HRESULT hr = Ole32.INSTANCE.CoCreateInstance(CLSID, Pointer.NULL, WTypes.CLSCTX_INPROC_SERVER, IID, p); + COMUtils.checkRC(hr); + return new TaskbarList(p.getValue()); + } + + public static TaskbarList createAndInit() { + TaskbarList taskbarList = create(); + taskbarList.HrInit(); + return taskbarList; + } + + /** Initializes the taskbar list object. + * This method must be called before any other ITaskbarList methods can be called. + */ + public void HrInit() { + int res = this._invokeNativeInt(3, new Object[]{this.getPointer()}); + COMUtils.checkRC(new WinNT.HRESULT(res)); + initialized = true; + } + + /** Adds an item to the taskbar. + * @param hwnd The handle of the window to be added. + */ + public void AddTab(WinDef.HWND hwnd) { + if (!initialized) + throw new IllegalStateException("TaskbarList not initialized."); + int res = this._invokeNativeInt(4, new Object[]{this.getPointer(), hwnd}); + COMUtils.checkRC(new WinNT.HRESULT(res)); + } + + /** Deletes an item from the taskbar. + * @param hwnd The handle of the window to be deleted. + */ + public void DeleteTab(WinDef.HWND hwnd) { + if (!initialized) + throw new IllegalStateException("TaskbarList not initialized."); + int res = this._invokeNativeInt(5, new Object[]{this.getPointer(), hwnd}); + COMUtils.checkRC(new WinNT.HRESULT(res)); + } + + @Override + public String toString() { + return "TaskbarList{" + "pointer=" + this.getPointer() + '}'; + } +} diff --git a/core/src/cn/harryh/arkpets/natives/WaylandClient.java b/core/src/cn/harryh/arkpets/natives/WaylandClient.java new file mode 100644 index 00000000..17662076 --- /dev/null +++ b/core/src/cn/harryh/arkpets/natives/WaylandClient.java @@ -0,0 +1,54 @@ +package cn.harryh.arkpets.natives; + +import com.sun.jna.*; + + +public interface WaylandClient extends Library { + WaylandClient INSTANCE = Native.load("wayland-client", WaylandClient.class); + + int wl_display_dispatch(Pointer display); + + int wl_display_roundtrip(Pointer display); + + int wl_proxy_get_version(Pointer proxy); + + Pointer wl_proxy_marshal_flags(Pointer proxy, int opcode, Pointer iface, int version, int flags, Object... obj); + + int wl_proxy_add_listener(Pointer proxy, Pointer implementation, Pointer data); + + void wl_proxy_destroy(Pointer compositor); + + default void wl_region_destroy(Pointer wl_region) { + wl_proxy_marshal_flags(wl_region, 0, null, wl_proxy_get_version(wl_region), 1); + } + + default void wl_surface_set_input_region(Pointer wl_surface, Pointer wl_region) { + wl_proxy_marshal_flags(wl_surface, 5, null, wl_proxy_get_version(wl_surface), 0, wl_region); + } + + default void wl_surface_commit(Pointer wl_surface) { + wl_proxy_marshal_flags(wl_surface, 6, null, wl_proxy_get_version(wl_surface), 0); + } + + default Pointer wl_compositor_create_region(Pointer wl_compositor) { + return wl_proxy_marshal_flags(wl_compositor, 1, WaylandInterface.wl_region_interface, wl_proxy_get_version(wl_compositor), 0, (Object) null); + } + + default Pointer wl_display_get_registry(Pointer wl_display) { + return wl_proxy_marshal_flags(wl_display, 1, WaylandInterface.wl_registry_interface, wl_proxy_get_version(wl_display), 0, (Object) null); + } + + @Structure.FieldOrder({"global", "global_remove"}) + class wl_registry_listener extends Structure { + public GlobalCallback global; + public GlobalRemoveCallback global_remove; + } + + interface GlobalCallback extends Callback { + void callback(Pointer data, Pointer registry, int name, String iface, int version); + } + + interface GlobalRemoveCallback extends Callback { + void callback(Pointer data, Pointer registry, int name); + } +} diff --git a/core/src/cn/harryh/arkpets/natives/WaylandHelper.java b/core/src/cn/harryh/arkpets/natives/WaylandHelper.java new file mode 100644 index 00000000..88a89d70 --- /dev/null +++ b/core/src/cn/harryh/arkpets/natives/WaylandHelper.java @@ -0,0 +1,55 @@ +package cn.harryh.arkpets.natives; + +import cn.harryh.arkpets.utils.Logger; +import com.sun.jna.Pointer; +import org.lwjgl.glfw.GLFWNativeWayland; + + +public class WaylandHelper { + private static boolean ready; + private static boolean unavailable; + private static WaylandClient.wl_registry_listener listener; // Prevent callback + private static Pointer compositor; + private static Pointer region; + + private static void init() { + Pointer display = new Pointer(GLFWNativeWayland.glfwGetWaylandDisplay()); + Pointer reg = WaylandClient.INSTANCE.wl_display_get_registry(display); + listener = new WaylandClient.wl_registry_listener(); + listener.global = (data, registry, name, iface, version) -> { + if (iface.equals("wl_compositor")) { + compositor = WaylandClient.INSTANCE.wl_proxy_marshal_flags(registry, 0, + WaylandInterface.wl_compositor_interface, version, 0, name, "wl_compositor", version, null); + } + }; + listener.global_remove = (data, registry, name) -> { + if (compositor != null) { + WaylandClient.INSTANCE.wl_proxy_destroy(compositor); + compositor = null; + } + }; + listener.write(); + WaylandClient.INSTANCE.wl_proxy_add_listener(reg, listener.getPointer(), null); + WaylandClient.INSTANCE.wl_display_dispatch(display); + WaylandClient.INSTANCE.wl_display_roundtrip(display); + if (compositor == null) { + Logger.warn("System", "Failed to bind wl_compositor"); + unavailable = true; + return; + } + region = WaylandClient.INSTANCE.wl_compositor_create_region(compositor); + ready = true; + } + + public static void setTransparent(Pointer surface, boolean enable) { + if (unavailable) return; + if (!ready) init(); + if (enable) { + WaylandClient.INSTANCE.wl_surface_set_input_region(surface, region); + WaylandClient.INSTANCE.wl_surface_commit(surface); + } else { + WaylandClient.INSTANCE.wl_surface_set_input_region(surface, null); + WaylandClient.INSTANCE.wl_surface_commit(surface); + } + } +} diff --git a/core/src/cn/harryh/arkpets/natives/WaylandInterface.java b/core/src/cn/harryh/arkpets/natives/WaylandInterface.java new file mode 100644 index 00000000..57eb4cc3 --- /dev/null +++ b/core/src/cn/harryh/arkpets/natives/WaylandInterface.java @@ -0,0 +1,18 @@ +package cn.harryh.arkpets.natives; + +import com.sun.jna.NativeLibrary; +import com.sun.jna.Pointer; + + +public class WaylandInterface { + public static final Pointer wl_registry_interface; + public static final Pointer wl_compositor_interface; + public static final Pointer wl_region_interface; + + static { + NativeLibrary lib = NativeLibrary.getInstance("wayland-client"); + wl_registry_interface = lib.getGlobalVariableAddress("wl_registry_interface"); + wl_compositor_interface = lib.getGlobalVariableAddress("wl_compositor_interface"); + wl_region_interface = lib.getGlobalVariableAddress("wl_region_interface"); + } +} diff --git a/core/src/cn/harryh/arkpets/natives/XextExtension.java b/core/src/cn/harryh/arkpets/natives/XextExtension.java deleted file mode 100644 index c9cd5efb..00000000 --- a/core/src/cn/harryh/arkpets/natives/XextExtension.java +++ /dev/null @@ -1,15 +0,0 @@ -package cn.harryh.arkpets.natives; - -import com.sun.jna.Native; -import com.sun.jna.Pointer; -import com.sun.jna.platform.unix.X11; -import com.sun.jna.ptr.IntByReference; - - -public interface XextExtension extends X11.Xext { - XextExtension INSTANCE = Native.load("Xext", XextExtension.class); - - void XShapeCombineRegion(X11.Display dpy, X11.Window dest, int destKind, int xOff, int yOff, Pointer r, int op); - - boolean XShapeQueryExtension(X11.Display dpy, IntByReference event_basep,IntByReference error_basep); -} \ No newline at end of file diff --git a/core/src/cn/harryh/arkpets/platform/HWndCtrl.java b/core/src/cn/harryh/arkpets/platform/HWndCtrl.java index 83f2e62f..c8c29ecb 100644 --- a/core/src/cn/harryh/arkpets/platform/HWndCtrl.java +++ b/core/src/cn/harryh/arkpets/platform/HWndCtrl.java @@ -1,8 +1,11 @@ -/** Copyright (c) 2022-2024, Harry Huang, Litwak913 +/** Copyright (c) 2022-2026, Harry Huang, Litwak913 * At GPL-3.0 License */ package cn.harryh.arkpets.platform; +import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Graphics; +import org.lwjgl.glfw.GLFW; + import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -15,6 +18,7 @@ public abstract class HWndCtrl { public final int posRight; public final int windowWidth; public final int windowHeight; + protected long glfwHandle; public HWndCtrl(String windowText, WindowRect windowRect) { this.windowText = windowText; @@ -77,11 +81,6 @@ public float getCenterY() { */ public abstract void setTaskbar(boolean enable); - /** Sets the window's layered style. - * @param enable Whether to enable the window's layered style. - */ - public abstract void setLayered(boolean enable); - /** Sets the window's topmost style. * @param enable Whether to enable the topmost style. */ @@ -90,7 +89,10 @@ public float getCenterY() { /** Sets the window's ability to be passed through. * @param enable Whether the window can be passed through. */ - public abstract void setTransparent(boolean enable); + public void setTransparent(boolean enable) { + if (glfwHandle == 0) return; + GLFW.glfwSetWindowAttrib(glfwHandle, GLFW.GLFW_MOUSE_PASSTHROUGH, enable ? 1 : 0); + } /** Sends a mouse event message to the window. * @param msg The window message value. @@ -99,6 +101,13 @@ public float getCenterY() { */ public abstract void sendMouseEvent(MouseEvent msg, int x, int y); + /** Attach a GLFW window handle to the window. + * @param graphics The Lwjgl3Graphics instance. + */ + public void attachGLFWWindow(Lwjgl3Graphics graphics) { + this.glfwHandle = graphics.getWindow().getWindowHandle(); + } + @Override public abstract boolean equals(Object o); @@ -159,6 +168,10 @@ public record WindowRect(int top, int bottom, int left, int right) { public WindowRect() { this(0, 0, 0, 0); } + + public int width() { return right - left; } + + public int height() { return bottom - top; } } public enum MouseEvent { @@ -171,4 +184,7 @@ public enum MouseEvent { MBUTTONDOWN, MBUTTONUP, } + + public record MousePoint(int x, int y) { + } } diff --git a/core/src/cn/harryh/arkpets/platform/HWndCtrlFactory.java b/core/src/cn/harryh/arkpets/platform/HWndCtrlFactory.java new file mode 100644 index 00000000..0494aecb --- /dev/null +++ b/core/src/cn/harryh/arkpets/platform/HWndCtrlFactory.java @@ -0,0 +1,41 @@ +package cn.harryh.arkpets.platform; + +import java.util.List; + + +public abstract class HWndCtrlFactory { + /** Finds a window. + * @param className The window's class name. + * @param windowText The window's title. + * @return The HWndCtrl, which may be null indicates not found. + */ + public abstract HWndCtrl findWindow(String className, String windowText); + + /** Gets the list of current windows. + * @param onlyVisible Whether exclude the invisible window. + * @return An ArrayList consists of HWndCtrls. + */ + public abstract List getWindowList(boolean onlyVisible); + + /** Gets the topmost window. + * @return The topmost window's HWndCtrl. + */ + public abstract HWndCtrl getTopmostWindow(); + + /** Gets the mouse position. + * @return The MousePoint record. + */ + public abstract HWndCtrl.MousePoint getMousePos(); + + /** Frees all the resources. + */ + public abstract void free(); + + /** Return current WindowSystem should enable resize. + */ + public abstract boolean needResize(); + + /** Return current WindowSystem should enable decoration. + */ + public abstract boolean needDecorated(); +} diff --git a/core/src/cn/harryh/arkpets/platform/KWinHWndCtrl.java b/core/src/cn/harryh/arkpets/platform/KWinHWndCtrl.java index 7cc41d2b..465e3700 100644 --- a/core/src/cn/harryh/arkpets/platform/KWinHWndCtrl.java +++ b/core/src/cn/harryh/arkpets/platform/KWinHWndCtrl.java @@ -1,28 +1,21 @@ package cn.harryh.arkpets.platform; -import cn.harryh.arkpets.Const; -import cn.harryh.arkpets.natives.KWinInterface; -import cn.harryh.arkpets.natives.KWinPluginInterface; -import cn.harryh.arkpets.utils.Logger; -import org.freedesktop.dbus.connections.impl.DBusConnection; -import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder; -import org.freedesktop.dbus.exceptions.DBusException; +import cn.harryh.arkpets.rpc.KWinInterface; import org.freedesktop.dbus.types.UInt32; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; +import static cn.harryh.arkpets.platform.KWinHWndCtrlFactory.dBusInterface; -public class KWinHWndCtrl extends HWndCtrl { +public class KWinHWndCtrl extends WaylandHWndCtrl { protected final String hWnd; protected KWinInterface.DetailsStruct details; - private static DBusConnection dBusConnection; - private static KWinInterface dBusInterface; protected KWinHWndCtrl(KWinInterface.DetailsStruct details) { - super(details.title, new WindowRect(details.y, details.y + details.h.intValue(), details.x, details.x + details.w.intValue())); + super(details.title, new WindowRect( + (details.y), + (details.y + details.h.intValue()), + (details.x), + (details.x + details.w.intValue()))); this.hWnd = details.id; this.details = details; } @@ -54,12 +47,8 @@ public void setForeground() { @Override public void setWindowPosition(HWndCtrl insertAfter, int x, int y, int w, int h) { - dBusInterface.MoveResize(hWnd, x, y, new UInt32(w), new UInt32(h)); - } - - @Override - public void setTransparent(boolean enable) { - + dBusInterface.MoveResize(hWnd, (x), (y), + new UInt32((w)), new UInt32((h))); } @Override @@ -67,11 +56,6 @@ public void setTaskbar(boolean enable) { dBusInterface.Stick(hWnd, enable); } - @Override - public void setLayered(boolean enable) { - - } - @Override public void setTopmost(boolean enable) { dBusInterface.Above(hWnd, enable); @@ -82,64 +66,6 @@ public void sendMouseEvent(MouseEvent msg, int x, int y) { } - protected static void init() { - try { - dBusConnection = DBusConnectionBuilder.forSessionBus().build(); - Logger.info("System", "Connected to DBus"); - checkAndEnablePlugin(); - dBusInterface = dBusConnection.getRemoteObject("org.kde.KWin", "/ArkPets", KWinInterface.class); - Logger.info("System", "KDE Integration plugin version " + dBusInterface.Version()); - } catch (DBusException e) { - throw new RuntimeException(e); - } - } - - private static void checkAndEnablePlugin() throws DBusException { - KWinPluginInterface pi = dBusConnection.getRemoteObject("org.kde.KWin", "/Plugins", KWinPluginInterface.class); - String pluginName = Const.kdePluginName + Const.kdePluginVersion; - List available = pi.getAvailablePlugins(); - List enabled = pi.getLoadedPlugins(); - if (!available.contains(pluginName)) throw new RuntimeException("KDE Integration plugin not found."); - if (!enabled.contains(pluginName)) { - boolean result = pi.LoadPlugin(pluginName); - if (!result) throw new RuntimeException("Failed to enable KDE integration plugin."); - try { - Thread.sleep(500); // wait for loaded - } catch (InterruptedException ignored) { - } - } - } - - protected static void free() { - try { - dBusConnection.close(); - Logger.info("System", "Disconnected from DBus"); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - protected static KWinHWndCtrl find(String className, String windowName) { - return dBusInterface.List().stream().map(KWinHWndCtrl::new).filter((i) -> { - if (className == null) { - return i.windowText != null && i.windowText.equals(windowName); - } else { - return i.details.wclass.equals(className) && i.windowText.equals(windowName); - } - }).findAny().orElse(null); - } - - protected static List getWindowList(boolean onlyVisible) { - List list = new ArrayList<>(dBusInterface.List().stream().map(KWinHWndCtrl::new).filter(w -> !onlyVisible || w.isVisible()).toList()); - Collections.reverse(list); - return list; - } - - protected static KWinHWndCtrl getTopmostWindow() { - List list = dBusInterface.List(); - return new KWinHWndCtrl(list.get(list.size() - 1)); - } - @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/core/src/cn/harryh/arkpets/platform/KWinHWndCtrlFactory.java b/core/src/cn/harryh/arkpets/platform/KWinHWndCtrlFactory.java new file mode 100644 index 00000000..6ceb577d --- /dev/null +++ b/core/src/cn/harryh/arkpets/platform/KWinHWndCtrlFactory.java @@ -0,0 +1,97 @@ +package cn.harryh.arkpets.platform; + +import cn.harryh.arkpets.Const; +import cn.harryh.arkpets.rpc.KWinInterface; +import cn.harryh.arkpets.rpc.KWinPluginInterface; +import cn.harryh.arkpets.utils.Logger; +import org.freedesktop.dbus.connections.impl.DBusConnection; +import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder; +import org.freedesktop.dbus.exceptions.DBusException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + + +public class KWinHWndCtrlFactory extends HWndCtrlFactory{ + static DBusConnection dBusConnection; + static KWinInterface dBusInterface; + + public KWinHWndCtrlFactory() { + try { + dBusConnection = DBusConnectionBuilder.forSessionBus().build(); + Logger.info("System", "Connected to DBus"); + checkAndEnablePlugin(); + dBusInterface = dBusConnection.getRemoteObject("org.kde.KWin", "/ArkPets", KWinInterface.class); + Logger.info("System", "KDE Integration plugin version " + dBusInterface.Version()); + } catch (DBusException e) { + throw new RuntimeException(e); + } + } + + @Override + public HWndCtrl findWindow(String className, String windowName) { + return dBusInterface.List().stream().map(KWinHWndCtrl::new).filter((i) -> { + if (className == null) { + return i.windowText != null && i.windowText.equals(windowName); + } else { + return i.details.wclass.equals(className) && i.windowText.equals(windowName); + } + }).findAny().orElse(null); + } + + @Override + public List getWindowList(boolean onlyVisible) { + List list = new ArrayList<>(dBusInterface.List().stream().map(KWinHWndCtrl::new).filter(w -> !onlyVisible || w.isVisible()).toList()); + Collections.reverse(list); + return list; } + + @Override + public HWndCtrl getTopmostWindow() { + List list = dBusInterface.List(); + return new KWinHWndCtrl(list.get(list.size() - 1)); + } + + @Override + public HWndCtrl.MousePoint getMousePos() { + KWinInterface.PointStruct pos = dBusInterface.Mouse(); + return new HWndCtrl.MousePoint(pos.x, pos.y); + } + + @Override + public void free() { + try { + dBusConnection.close(); + Logger.info("System", "Disconnected from DBus"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public boolean needResize() { + return true; + } + + @Override + public boolean needDecorated() { + return false; + } + + private static void checkAndEnablePlugin() throws DBusException { + KWinPluginInterface pi = dBusConnection.getRemoteObject("org.kde.KWin", "/Plugins", KWinPluginInterface.class); + String pluginName = Const.kdePluginName + Const.kdePluginVersion; + List available = pi.getAvailablePlugins(); + List enabled = pi.getLoadedPlugins(); + if (!available.contains(pluginName)) throw new RuntimeException("KDE Integration plugin not found."); + if (!enabled.contains(pluginName)) { + boolean result = pi.LoadPlugin(pluginName); + if (!result) throw new RuntimeException("Failed to enable KDE integration plugin."); + try { + Thread.sleep(500); // wait for loaded + } catch (InterruptedException ignored) { + } + } + } +} diff --git a/core/src/cn/harryh/arkpets/platform/MutterHWndCtrl.java b/core/src/cn/harryh/arkpets/platform/MutterHWndCtrl.java index 872e35f3..dfec246a 100644 --- a/core/src/cn/harryh/arkpets/platform/MutterHWndCtrl.java +++ b/core/src/cn/harryh/arkpets/platform/MutterHWndCtrl.java @@ -1,30 +1,21 @@ package cn.harryh.arkpets.platform; -import cn.harryh.arkpets.Const; -import cn.harryh.arkpets.natives.MutterInterface; -import cn.harryh.arkpets.natives.MutterPluginInterface; -import cn.harryh.arkpets.utils.Logger; -import org.freedesktop.dbus.connections.impl.DBusConnection; -import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder; -import org.freedesktop.dbus.exceptions.DBusException; +import cn.harryh.arkpets.rpc.MutterInterface; import org.freedesktop.dbus.types.UInt32; -import org.freedesktop.dbus.types.Variant; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; +import static cn.harryh.arkpets.platform.MutterHWndCtrlFactory.dBusInterface; -public class MutterHWndCtrl extends HWndCtrl { +public class MutterHWndCtrl extends WaylandHWndCtrl { protected final UInt32 hWnd; protected MutterInterface.DetailsStruct details; - private static DBusConnection dBusConnection; - private static MutterInterface dBusInterface; protected MutterHWndCtrl(MutterInterface.DetailsStruct details) { - super(details.title, new WindowRect(details.y, details.y + details.h.intValue(), details.x, details.x + details.w.intValue())); + super(details.title, new WindowRect( + (details.y), + (details.y + details.h.intValue()), + (details.x), + (details.x + details.w.intValue()))); this.hWnd = details.id; this.details = details; } @@ -56,12 +47,8 @@ public void setForeground() { @Override public void setWindowPosition(HWndCtrl insertAfter, int x, int y, int w, int h) { - dBusInterface.MoveResize(hWnd, x, y, new UInt32(w), new UInt32(h)); - } - - @Override - public void setTransparent(boolean enable) { - + dBusInterface.MoveResize(hWnd, (x), (y), + new UInt32((w)), new UInt32((h))); } @Override @@ -69,11 +56,6 @@ public void setTaskbar(boolean enable) { dBusInterface.Stick(hWnd, !enable); } - @Override - public void setLayered(boolean enable) { - - } - @Override public void setTopmost(boolean enable) { dBusInterface.Above(hWnd, enable); @@ -84,64 +66,6 @@ public void sendMouseEvent(MouseEvent msg, int x, int y) { } - protected static void init() { - try { - dBusConnection = DBusConnectionBuilder.forSessionBus().build(); - Logger.info("System", "Connected to DBus"); - checkAndEnablePlugin(); - dBusInterface = dBusConnection.getRemoteObject("org.gnome.Shell", "/org/gnome/Shell/Extensions/ArkPets", MutterInterface.class); - Logger.info("System", "GNOME Integration extension version " + dBusInterface.Version()); - } catch (DBusException e) { - throw new RuntimeException(e); - } - } - - protected static void free() { - try { - dBusConnection.close(); - Logger.info("System", "Disconnected from DBus"); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private static void checkAndEnablePlugin() throws DBusException { - MutterPluginInterface pi = dBusConnection.getRemoteObject("org.gnome.Shell", "/org/gnome/Shell", MutterPluginInterface.class); - Map> ext = pi.GetExtensionInfo(Const.gnomePluginName); - if (ext.isEmpty()) throw new RuntimeException("GNOME Integration plugin not found."); - Boolean enable = (Boolean) ext.get("enabled").getValue(); - if (!enable) { - Logger.info("System","Enabling GNOME Integration plugin"); - boolean result = pi.EnableExtension(Const.gnomePluginName); - if (!result) throw new RuntimeException("Failed to enable GNOME integration plugin."); - try { - Thread.sleep(500); // wait for loaded - } catch (InterruptedException ignored) { - } - } - } - - protected static MutterHWndCtrl find(String className, String windowName) { - return dBusInterface.List().stream().map(MutterHWndCtrl::new).filter((i) -> { - if (className == null) { - return i.windowText != null && i.windowText.equals(windowName); - } else { - return i.details.wclass.equals(className) && i.windowText.equals(windowName); - } - }).findAny().orElse(null); - } - - protected static List getWindowList(boolean onlyVisible) { - List list = new ArrayList<>(dBusInterface.List().stream().map(MutterHWndCtrl::new).filter(w -> !onlyVisible || w.isVisible()).toList()); - Collections.reverse(list); - return list; - } - - protected static MutterHWndCtrl getTopmostWindow() { - List list = dBusInterface.List(); - return new MutterHWndCtrl(list.get(list.size() - 1)); - } - @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/core/src/cn/harryh/arkpets/platform/MutterHWndCtrlFactory.java b/core/src/cn/harryh/arkpets/platform/MutterHWndCtrlFactory.java new file mode 100644 index 00000000..f2cd3403 --- /dev/null +++ b/core/src/cn/harryh/arkpets/platform/MutterHWndCtrlFactory.java @@ -0,0 +1,101 @@ +package cn.harryh.arkpets.platform; + +import cn.harryh.arkpets.Const; +import cn.harryh.arkpets.rpc.MutterInterface; +import cn.harryh.arkpets.rpc.MutterPluginInterface; +import cn.harryh.arkpets.utils.Logger; +import org.freedesktop.dbus.connections.impl.DBusConnection; +import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder; +import org.freedesktop.dbus.exceptions.DBusException; +import org.freedesktop.dbus.types.Variant; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + + +public class MutterHWndCtrlFactory extends HWndCtrlFactory{ + private static DBusConnection dBusConnection; + + static MutterInterface dBusInterface; + + public MutterHWndCtrlFactory() { + try { + dBusConnection = DBusConnectionBuilder.forSessionBus().build(); + Logger.info("System", "Connected to DBus"); + checkAndEnablePlugin(); + dBusInterface = dBusConnection.getRemoteObject("org.gnome.Shell", "/org/gnome/Shell/Extensions/ArkPets", MutterInterface.class); + Logger.info("System", "GNOME Integration extension version " + dBusInterface.Version()); + } catch (DBusException e) { + throw new RuntimeException(e); + } + } + + @Override + public HWndCtrl findWindow(String className, String windowName) { + return dBusInterface.List().stream().map(MutterHWndCtrl::new).filter((i) -> { + if (className == null) { + return i.windowText != null && i.windowText.equals(windowName); + } else { + return i.details.wclass.equals(className) && i.windowText.equals(windowName); + } + }).findAny().orElse(null); + } + + @Override + public List getWindowList(boolean onlyVisible) { + List list = new ArrayList<>(dBusInterface.List().stream().map(MutterHWndCtrl::new).filter(w -> !onlyVisible || w.isVisible()).toList()); + Collections.reverse(list); + return list; + } + + @Override + public HWndCtrl getTopmostWindow() { + List list = dBusInterface.List(); + return new MutterHWndCtrl(list.get(list.size() - 1)); + } + + @Override + public HWndCtrl.MousePoint getMousePos() { + MutterInterface.PointStruct pos = dBusInterface.Mouse(); + return new HWndCtrl.MousePoint((pos.x), (pos.y)); + } + + @Override + public void free() { + try { + if (dBusConnection != null) dBusConnection.close(); + Logger.info("System", "Disconnected from DBus"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public boolean needResize() { + return true; + } + + @Override + public boolean needDecorated() { + return false; + } + + private static void checkAndEnablePlugin() throws DBusException { + MutterPluginInterface pi = dBusConnection.getRemoteObject("org.gnome.Shell", "/org/gnome/Shell", MutterPluginInterface.class); + Map> ext = pi.GetExtensionInfo(Const.gnomePluginName); + if (ext.isEmpty()) throw new RuntimeException("GNOME Integration plugin not found."); + Boolean enable = (Boolean) ext.get("enabled").getValue(); + if (!enable) { + Logger.info("System","Enabling GNOME Integration plugin"); + boolean result = pi.EnableExtension(Const.gnomePluginName); + if (!result) throw new RuntimeException("Failed to enable GNOME integration plugin."); + try { + Thread.sleep(500); // wait for loaded + } catch (InterruptedException ignored) { + } + } + } +} diff --git a/core/src/cn/harryh/arkpets/platform/NullHWndCtrl.java b/core/src/cn/harryh/arkpets/platform/NullHWndCtrl.java index 5226562d..a20d3f1c 100644 --- a/core/src/cn/harryh/arkpets/platform/NullHWndCtrl.java +++ b/core/src/cn/harryh/arkpets/platform/NullHWndCtrl.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2024, Harry Huang, Litwak913 +/** Copyright (c) 2022-2026, Harry Huang, Litwak913 * At GPL-3.0 License */ package cn.harryh.arkpets.platform; @@ -62,18 +62,10 @@ public void setWindowPosition(HWndCtrl insertAfter, int x, int y, int w, int h) public void setTaskbar(boolean enable) { } - @Override - public void setLayered(boolean enable) { - } - @Override public void setTopmost(boolean enable) { } - @Override - public void setTransparent(boolean enable) { - } - @Override public void sendMouseEvent(MouseEvent msg, int x, int y) { } diff --git a/core/src/cn/harryh/arkpets/platform/NullHWndCtrlFactory.java b/core/src/cn/harryh/arkpets/platform/NullHWndCtrlFactory.java new file mode 100644 index 00000000..ab7f4a41 --- /dev/null +++ b/core/src/cn/harryh/arkpets/platform/NullHWndCtrlFactory.java @@ -0,0 +1,49 @@ +package cn.harryh.arkpets.platform; + +import java.util.List; + + +public class NullHWndCtrlFactory extends HWndCtrlFactory{ + private boolean startupFind; + + @Override + public HWndCtrl findWindow(String className, String windowText) { + if (windowText.equals("ArkPets")) { + if (!startupFind) { + startupFind = true; + return null; + } + } + return new NullHWndCtrl(); + } + + @Override + public List getWindowList(boolean onlyVisible) { + return List.of(); + } + + @Override + public HWndCtrl getTopmostWindow() { + return new NullHWndCtrl(); + } + + @Override + public HWndCtrl.MousePoint getMousePos() { + return new HWndCtrl.MousePoint(0, 0); + } + + @Override + public void free() { + + } + + @Override + public boolean needResize() { + return true; + } + + @Override + public boolean needDecorated() { + return true; + } +} diff --git a/core/src/cn/harryh/arkpets/platform/QuartzHWndCtrl.java b/core/src/cn/harryh/arkpets/platform/QuartzHWndCtrl.java index 4ed2f5e5..8232a43e 100644 --- a/core/src/cn/harryh/arkpets/platform/QuartzHWndCtrl.java +++ b/core/src/cn/harryh/arkpets/platform/QuartzHWndCtrl.java @@ -1,9 +1,10 @@ package cn.harryh.arkpets.platform; -import cn.harryh.arkpets.Const; import cn.harryh.arkpets.natives.CoreGraphics; import cn.harryh.arkpets.natives.ObjCHelper; -import com.sun.jna.Platform; +import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Graphics; +import com.sun.jna.Memory; +import com.sun.jna.Native; import com.sun.jna.Pointer; import com.sun.jna.platform.mac.CoreFoundation; import com.sun.jna.platform.mac.CoreFoundation.CFArrayRef; @@ -11,26 +12,24 @@ import com.sun.jna.platform.mac.CoreFoundation.CFNumberRef; import com.sun.jna.platform.mac.CoreFoundation.CFStringRef; -import java.util.ArrayList; -import java.util.List; import java.util.Objects; import static cn.harryh.arkpets.natives.CoreGraphics.*; +import static cn.harryh.arkpets.platform.QuartzHWndCtrlFactory.currentScreenRect; +import static org.lwjgl.glfw.GLFWNativeCocoa.glfwGetCocoaWindow; public class QuartzHWndCtrl extends HWndCtrl { private static Pointer nsApp; - private final IgnoreMouseCallback igcb = new IgnoreMouseCallback(); private final FrameCallback fcb = new FrameCallback(); + private final LevelCallback lcb = new LevelCallback(); + private final long windowID; private Pointer nsWin; - private Pointer nsScreen; - private final long layer; + long layer; // 0:Uncheck 1:Checked,Available -1:Checked,Unavailable - private byte nsWinUnavailable; - private boolean trans; private final CGRect.ByValue newRect = new CGRect.ByValue(); public QuartzHWndCtrl(CFDictionaryRef dict) { @@ -41,8 +40,11 @@ public QuartzHWndCtrl(CFDictionaryRef dict) { @Override public boolean isForeground() { - //todo - return true; + if (nsWin == null) return false; + return ObjCHelper.msgSend.invokeInt(new Object[]{ + nsWin, + ObjCHelper.sel("isKeyWindow") + }) == 1; } @Override @@ -58,24 +60,27 @@ public boolean close(int timeout) { @Override public HWndCtrl updated() { - //todo - /*QuartzHWndCtrl hwnd = null; - CFIndex index = new CFIndex(1); - LongByReference wid = new LongByReference(windowID); - CFArrayRef arr = CFExt.INSTANCE.CFArrayCreate(null,new Pointer[] {wid.getPointer()},index,null); + QuartzHWndCtrl hwnd; + CoreFoundation.CFIndex index = new CoreFoundation.CFIndex(1); + Memory carr = new Memory(Native.getNativeSize(Integer.class)); + carr.write(0,new int[] {(int) windowID},0,1); + CFArrayRef arr = CoreFoundation.INSTANCE.CFArrayCreate(null,carr,index,null); if (arr != null) { CFArrayRef win = CoreGraphics.INSTANCE.CGWindowListCreateDescriptionFromArray(arr); arr.release(); CFDictionaryRef dict = new CFDictionaryRef(win.getValueAtIndex(0)); hwnd=new QuartzHWndCtrl(dict); win.release(); - }*/ - return new NullHWndCtrl(); + carr.close(); + return hwnd; + } else { + return new NullHWndCtrl(); + } } @Override public void setForeground() { - getNSWindow(windowID); + if (nsWin == null) return; ObjCHelper.msgSend.invokeVoid(new Object[]{ nsWin, ObjCHelper.sel("orderFrontRegardless:") @@ -84,12 +89,11 @@ public void setForeground() { @Override public void setWindowPosition(HWndCtrl insertAfter, int x, int y, int w, int h) { - getNSWindow(windowID); - CGRect rect = getScreenSize(); + if (nsWin == null) return; newRect.origin.x = x; - newRect.origin.y = rect.size.height - y - h; - newRect.size.width = w; - newRect.size.height = h; + newRect.origin.y = currentScreenRect.getValue().size.height - y - h; + newRect.size.width = (w); + newRect.size.height = (h); ObjCHelper.msgSend.invokeVoid(new Object[]{ nsWin, ObjCHelper.sel("performSelectorOnMainThread:withObject:waitUntilDone:"), @@ -109,30 +113,14 @@ public void setTaskbar(boolean enable) { }); } - @Override - public void setLayered(boolean enable) { - // not necessary in macOS. - } - @Override public void setTopmost(boolean enable) { - getNSWindow(windowID); - ObjCHelper.msgSend.invokeVoid(new Object[]{ - nsWin, - ObjCHelper.sel("setLevel:"), - enable ? NSStatusWindowLevel : NSNormalWindowLevel - }); - } - - @Override - public void setTransparent(boolean enable) { - if (trans == enable) return; - getNSWindow(windowID); - trans = enable; + if(nsWin == null) return; + layer = enable ? NSStatusWindowLevel : NSNormalWindowLevel; ObjCHelper.msgSend.invokeVoid(new Object[]{ nsWin, ObjCHelper.sel("performSelectorOnMainThread:withObject:waitUntilDone:"), - ObjCHelper.sel("apRunOnAppKitIgnoreMouse"), + ObjCHelper.sel("apRunOnAppKitLevel"), null, 1 }); @@ -143,66 +131,11 @@ public void sendMouseEvent(MouseEvent msg, int x, int y) { } - protected static void init() { - CFDictionaryRef server = CoreGraphics.INSTANCE.CGSessionCopyCurrentDictionary(); - if (server == null) { - throw new RuntimeException("No window server connection."); - } else { - CoreFoundation.INSTANCE.CFRelease(server); - } - ObjCHelper.init(); - } - - protected static void free() { - - } - - protected static List getWindowList(boolean onlyVisible) { - ArrayList list = new ArrayList<>(); - //todo - int opt; - if (onlyVisible) { - opt = kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements; - } else { - opt = kCGWindowListExcludeDesktopElements; - } - CFArrayRef windows = CoreGraphics.INSTANCE.CGWindowListCopyWindowInfo(opt, 0); - int numWindows = windows.getCount(); - for (int i = 0; i < numWindows; i++) { - Pointer result = windows.getValueAtIndex(i); - CFDictionaryRef windowRef = new CFDictionaryRef(result); - QuartzHWndCtrl win = new QuartzHWndCtrl(windowRef); - if (!onlyVisible || (win.layer >= 0 && win.layer != 20)) { - list.add(win); - } - } - windows.release(); - return list; - } - - protected static QuartzHWndCtrl find(String className, String windowText) { - CFArrayRef windows = CoreGraphics.INSTANCE.CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements, 0); - int numWindows = windows.getCount(); - QuartzHWndCtrl win = null; - for (int i = 0; i < numWindows; i++) { - Pointer result = windows.getValueAtIndex(i); - CFDictionaryRef windowRef = new CFDictionaryRef(result); - String cname = getWindowName(windowRef.getValue(kCGWindowOwnerName)); - String wname = getWindowName(windowRef.getValue(kCGWindowName)); - if (className == null) { - if (wname.equals(windowText) || cname.equals(windowText)) { - win = new QuartzHWndCtrl(windowRef); - break; - } - } else { - if (cname.equals(className) && wname.equals(windowText)) { - win = new QuartzHWndCtrl(windowRef); - break; - } - } - } - windows.release(); - return win; + @Override + public void attachGLFWWindow(Lwjgl3Graphics graphics) { + super.attachGLFWWindow(graphics); + nsWin = new Pointer(glfwGetCocoaWindow(glfwHandle)); + registerMethods(nsWin); } @Override @@ -228,52 +161,20 @@ private void checkNSApp() { } } - private void getNSWindow(long CGWindowId) { - checkNSApp(); - if (nsWinUnavailable == 0) { - Pointer nswin = ObjCHelper.msgSend.invokePointer(new Object[]{ - nsApp, - ObjCHelper.sel("windowWithWindowNumber:"), - CGWindowId - }); - if (nswin == null) { - nsWinUnavailable = -1; - } - this.nsWin = nswin; - nsWinUnavailable = 1; - registerMethods(nsWin); - } - } - private void registerMethods(Pointer nsWin) { Pointer cls = ObjCHelper.msgSend.invokePointer(new Object[]{ nsWin, ObjCHelper.sel("class") }); ObjCHelper.addRunOnAppKitMethod(cls, fcb, "Frame"); - ObjCHelper.addRunOnAppKitMethod(cls, igcb, "IgnoreMouse"); - ObjCHelper.msgSend.invokeVoid(new Object[]{ - nsWin, - ObjCHelper.sel("performSelectorOnMainThread:withObject:waitUntilDone:"), - ObjCHelper.sel("apRunOnAppKitIgnoreMouse"), - null, - 1 - }); - } - - private void getNSScreen() { - if (this.nsWinUnavailable != 1 || this.nsScreen != null) return; - this.nsScreen = ObjCHelper.msgSend.invokePointer(new Object[]{ - nsWin, - ObjCHelper.sel("screen") - }); + ObjCHelper.addRunOnAppKitMethod(cls, lcb, "Level"); } - private static String getWindowName(Pointer value) { + static String getWindowName(Pointer value) { return value == null ? "" : new CFStringRef(value).stringValue(); } - private static String getWindowName(Pointer own, Pointer title) { + static String getWindowName(Pointer own, Pointer title) { String ownName; String titleName; ownName = own == null ? "" : new CFStringRef(own).stringValue(); @@ -299,7 +200,10 @@ private static WindowRect getWindowRect(Pointer value) { } private CGRect getScreenSize() { - getNSScreen(); + /*Pointer nsScreen = ObjCHelper.msgSend.invokePointer(new Object[]{ + nsWin, + ObjCHelper.sel("screen") + }); if (Const.isARM) { return (CGRect.ByValue) ObjCHelper.msgSend.invoke(CGRect.ByValue.class, new Object[]{ nsScreen, @@ -313,28 +217,29 @@ private CGRect getScreenSize() { ObjCHelper.sel("frame") }); return rect; - } + }*/ + return CoreGraphics.INSTANCE.CGDisplayBounds(CoreGraphics.INSTANCE.CGMainDisplayID()); } - private class IgnoreMouseCallback implements ObjCHelper.ThreadCallback { + private class FrameCallback implements ObjCHelper.ThreadCallback { @Override public void callback(Pointer id, Pointer _cmd) { ObjCHelper.msgSend.invokeVoid(new Object[]{ - id, - ObjCHelper.sel("setIgnoresMouseEvents:"), - trans ? 1 : 0 + nsWin, + ObjCHelper.sel("setFrame:display:animate:"), + newRect, + 1, 0 }); } } - private class FrameCallback implements ObjCHelper.ThreadCallback { + private class LevelCallback implements ObjCHelper.ThreadCallback { @Override public void callback(Pointer id, Pointer _cmd) { ObjCHelper.msgSend.invokeVoid(new Object[]{ nsWin, - ObjCHelper.sel("setFrame:display:animate:"), - newRect, - 1, 0 + ObjCHelper.sel("setLevel:"), + layer }); } } diff --git a/core/src/cn/harryh/arkpets/platform/QuartzHWndCtrlFactory.java b/core/src/cn/harryh/arkpets/platform/QuartzHWndCtrlFactory.java new file mode 100644 index 00000000..9549a5ac --- /dev/null +++ b/core/src/cn/harryh/arkpets/platform/QuartzHWndCtrlFactory.java @@ -0,0 +1,111 @@ +package cn.harryh.arkpets.platform; + +import cn.harryh.arkpets.natives.CoreGraphics; +import cn.harryh.arkpets.natives.ObjCHelper; +import cn.harryh.arkpets.utils.Cached; +import com.sun.jna.Pointer; +import com.sun.jna.platform.mac.CoreFoundation; + +import java.util.ArrayList; +import java.util.List; + +import static cn.harryh.arkpets.natives.CoreGraphics.*; +import static cn.harryh.arkpets.natives.CoreGraphics.kCGWindowName; +import static cn.harryh.arkpets.platform.QuartzHWndCtrl.getWindowName; + + +public class QuartzHWndCtrlFactory extends HWndCtrlFactory{ + static Cached currentScreenRect; + + public QuartzHWndCtrlFactory() { + CoreFoundation.CFDictionaryRef server = CoreGraphics.INSTANCE.CGSessionCopyCurrentDictionary(); + if (server == null) { + throw new RuntimeException("No window server connection."); + } else { + CoreFoundation.INSTANCE.CFRelease(server); + } + ObjCHelper.init(); + currentScreenRect = new Cached<>(); + currentScreenRect.setCacheAge(1.0); + currentScreenRect.setValueProducer(() -> CoreGraphics.INSTANCE.CGDisplayBounds(CoreGraphics.INSTANCE.CGMainDisplayID())); + } + + @Override + public HWndCtrl findWindow(String className, String windowText) { + CoreFoundation.CFArrayRef windows = CoreGraphics.INSTANCE.CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements, 0); + int numWindows = windows.getCount(); + QuartzHWndCtrl win = null; + for (int i = 0; i < numWindows; i++) { + Pointer result = windows.getValueAtIndex(i); + CoreFoundation.CFDictionaryRef windowRef = new CoreFoundation.CFDictionaryRef(result); + String cname = getWindowName(windowRef.getValue(kCGWindowOwnerName)); + String wname = getWindowName(windowRef.getValue(kCGWindowName)); + if (className == null) { + if (wname.equals(windowText) || cname.equals(windowText)) { + win = new QuartzHWndCtrl(windowRef); + break; + } + } else { + if (cname.equals(className) && wname.equals(windowText)) { + win = new QuartzHWndCtrl(windowRef); + break; + } + } + } + windows.release(); + return win; + } + + @Override + public List getWindowList(boolean onlyVisible) { + ArrayList list = new ArrayList<>(); + //todo + int opt; + if (onlyVisible) { + opt = kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements; + } else { + opt = kCGWindowListExcludeDesktopElements; + } + CoreFoundation.CFArrayRef windows = CoreGraphics.INSTANCE.CGWindowListCopyWindowInfo(opt, 0); + int numWindows = windows.getCount(); + for (int i = 0; i < numWindows; i++) { + Pointer result = windows.getValueAtIndex(i); + CoreFoundation.CFDictionaryRef windowRef = new CoreFoundation.CFDictionaryRef(result); + QuartzHWndCtrl win = new QuartzHWndCtrl(windowRef); + if (!onlyVisible || (win.layer >= 0 && win.layer != 20)) { + list.add(win); + } + } + windows.release(); + return list; + } + + @Override + public HWndCtrl getTopmostWindow() { + return new NullHWndCtrl(); // todo + } + + @Override + public HWndCtrl.MousePoint getMousePos() { + CGPoint.ByValue point = (CGPoint.ByValue) ObjCHelper.msgSend.invoke(CGPoint.ByValue.class,new Object[]{ + ObjCHelper.cls("NSEvent"), + ObjCHelper.sel("mouseLocation") + }); + return new HWndCtrl.MousePoint((int) Math.floor(point.x), (int) Math.floor(currentScreenRect.getValue().size.height - point.y)); + } + + @Override + public void free() { + + } + + @Override + public boolean needResize() { + return false; + } + + @Override + public boolean needDecorated() { + return false; + } +} diff --git a/core/src/cn/harryh/arkpets/platform/User32HWndCtrl.java b/core/src/cn/harryh/arkpets/platform/User32HWndCtrl.java index 46b26b4a..9cf50e1b 100644 --- a/core/src/cn/harryh/arkpets/platform/User32HWndCtrl.java +++ b/core/src/cn/harryh/arkpets/platform/User32HWndCtrl.java @@ -1,17 +1,12 @@ -/** Copyright (c) 2022-2024, Harry Huang, Litwak913 +/** Copyright (c) 2022-2026, Harry Huang, Litwak913 * At GPL-3.0 License */ package cn.harryh.arkpets.platform; +import cn.harryh.arkpets.natives.TaskbarList; import com.sun.jna.Native; -import com.sun.jna.Pointer; -import com.sun.jna.platform.win32.User32; -import com.sun.jna.platform.win32.WinDef; +import com.sun.jna.platform.win32.*; import com.sun.jna.platform.win32.WinDef.HWND; -import com.sun.jna.platform.win32.WinDef.RECT; -import com.sun.jna.platform.win32.WinUser; - -import java.util.ArrayList; public class User32HWndCtrl extends HWndCtrl { @@ -35,6 +30,8 @@ public class User32HWndCtrl extends HWndCtrl { private static final int MK_RBUTTON = 0x0002; private static final int MK_MBUTTON = 0x0010; + private static TaskbarList taskbarList = null; + /** HWnd Controller instance. * @param hWnd The handle of the window. @@ -44,18 +41,6 @@ protected User32HWndCtrl(HWND hWnd) { this.hWnd = hWnd; } - /** Finds a window. - * @param className The class name of the window. - * @param windowName The title of the window. - */ - public static HWndCtrl find(String className, String windowName) { - HWND hwnd = User32.INSTANCE.FindWindow(className, windowName); - if (hwnd != null) { - return new User32HWndCtrl(hwnd); - } - return null; - } - @Override public boolean isForeground() { return hWnd.equals(User32.INSTANCE.GetForegroundWindow()); @@ -89,18 +74,17 @@ public void setWindowPosition(HWndCtrl insertAfter, int x, int y, int w, int h) @Override public void setTaskbar(boolean enable) { // On Windows, this is implemented by toggle app-window ex-style and tool-window ex-style. - if (enable) + // Furthermore, we introduced ITaskbarList operation to make the behavior more reliable. + if (taskbarList == null) { + taskbarList = TaskbarList.createAndInit(); + } + if (enable) { setWindowExStyle((getWindowExStyle() & ~User32HWndCtrl.WS_EX_TOOLWINDOW) | User32HWndCtrl.WS_EX_APPWINDOW); - else + taskbarList.AddTab(hWnd); + } else { setWindowExStyle((getWindowExStyle() | User32HWndCtrl.WS_EX_TOOLWINDOW) & ~User32HWndCtrl.WS_EX_APPWINDOW); - } - - @Override - public void setLayered(boolean enable) { - if (enable) - setWindowExStyle(getWindowExStyle() | User32HWndCtrl.WS_EX_LAYERED); - else - setWindowExStyle(getWindowExStyle() & ~User32HWndCtrl.WS_EX_LAYERED); + taskbarList.DeleteTab(hWnd); + } } @Override @@ -111,14 +95,6 @@ public void setTopmost(boolean enable) { setWindowExStyle(getWindowExStyle() & ~User32HWndCtrl.WS_EX_TOPMOST); } - @Override - public void setTransparent(boolean enable) { - if (enable) - setWindowExStyle(getWindowExStyle() | User32HWndCtrl.WS_EX_TRANSPARENT); - else - setWindowExStyle(getWindowExStyle() & ~User32HWndCtrl.WS_EX_TRANSPARENT); - } - @Override public void sendMouseEvent(MouseEvent msg, int x, int y) { int wmsg = switch (msg) { @@ -141,36 +117,6 @@ public void sendMouseEvent(MouseEvent msg, int x, int y) { User32.INSTANCE.SendMessage(hWnd, wmsg, new WinDef.WPARAM(wParam), new WinDef.LPARAM(lParam)); } - /** Gets the current list of windows. - * @param only_visible Whether exclude the invisible window. - * @return An ArrayList consists of HWndCtrls. - */ - public static ArrayList getWindowList(boolean only_visible) { - ArrayList windowList = new ArrayList<>(); - User32.INSTANCE.EnumWindows((hWnd, arg1) -> { - if (User32.INSTANCE.IsWindow(hWnd) && (!only_visible || isVisible(hWnd))) - windowList.add(new User32HWndCtrl(hWnd)); - return true; - }, null); - return windowList; - } - - /** Gets the current list of windows. (Advanced) - * @param only_visible Whether exclude the invisible window. - * @param exclude_ws_ex Exclude the specific window-style-extra. - * @return An ArrayList consists of HWndCtrls. - */ - public static ArrayList getWindowList(boolean only_visible, long exclude_ws_ex) { - ArrayList windowList = new ArrayList<>(); - User32.INSTANCE.EnumWindows((hWnd, arg1) -> { - if (User32.INSTANCE.IsWindow(hWnd) && (!only_visible || isVisible(hWnd)) - && (User32.INSTANCE.GetWindowLong(hWnd, WinUser.GWL_EXSTYLE) & exclude_ws_ex) != exclude_ws_ex) - windowList.add(new User32HWndCtrl(hWnd)); - return true; - }, null); - return windowList; - } - /** Gets the value of the window's extended styles. * @return EX_STYLE value. * @see WinUser @@ -185,13 +131,7 @@ protected int getWindowExStyle() { */ protected void setWindowExStyle(int newLong) { User32.INSTANCE.SetWindowLong(hWnd, WinUser.GWL_EXSTYLE, newLong); - } - - /** Gets the topmost window. - * @return The topmost window's HWndCtrl. - */ - protected static User32HWndCtrl getTopmostWindow() { - return new User32HWndCtrl(new HWND(Pointer.createConstant(-1))); + User32.INSTANCE.SetWindowPos(hWnd, null, 0, 0, 0, 0, WinUser.SWP_NOSIZE | WinUser.SWP_NOMOVE | WinUser.SWP_NOZORDER | WinUser.SWP_FRAMECHANGED); } protected static String getWindowText(HWND hWnd) { @@ -201,12 +141,12 @@ protected static String getWindowText(HWND hWnd) { } protected static WindowRect getWindowRect(HWND hWnd) { - RECT rect = new RECT(); + WinDef.RECT rect = new WinDef.RECT(); User32.INSTANCE.GetWindowRect(hWnd, rect); return new WindowRect(rect.top, rect.bottom, rect.left, rect.right); } - protected static boolean isVisible(HWND hWnd) { + static boolean isVisible(HWND hWnd) { try { if (!User32.INSTANCE.IsWindowVisible(hWnd) || !User32.INSTANCE.IsWindowEnabled(hWnd)) return false; diff --git a/core/src/cn/harryh/arkpets/platform/User32HWndCtrlFactory.java b/core/src/cn/harryh/arkpets/platform/User32HWndCtrlFactory.java new file mode 100644 index 00000000..a022518b --- /dev/null +++ b/core/src/cn/harryh/arkpets/platform/User32HWndCtrlFactory.java @@ -0,0 +1,76 @@ +package cn.harryh.arkpets.platform; + +import com.sun.jna.Pointer; +import com.sun.jna.platform.win32.User32; +import com.sun.jna.platform.win32.WinDef; +import com.sun.jna.platform.win32.WinUser; + +import java.util.ArrayList; +import java.util.List; + +import static cn.harryh.arkpets.platform.User32HWndCtrl.isVisible; + + +public class User32HWndCtrlFactory extends HWndCtrlFactory{ + @Override + public HWndCtrl findWindow(String className, String windowText) { + WinDef.HWND hwnd = User32.INSTANCE.FindWindow(className, windowText); + if (hwnd != null) { + return new User32HWndCtrl(hwnd); + } + return null; + } + + @Override + public List getWindowList(boolean onlyVisible) { + ArrayList windowList = new ArrayList<>(); + User32.INSTANCE.EnumWindows((hWnd, arg1) -> { + if (User32.INSTANCE.IsWindow(hWnd) && (!onlyVisible || isVisible(hWnd))) + windowList.add(new User32HWndCtrl(hWnd)); + return true; + }, null); + return windowList; + } + + /** Gets the current list of windows. (Advanced) + * @param only_visible Whether exclude the invisible window. + * @param exclude_ws_ex Exclude the specific window-style-extra. + * @return An ArrayList consists of HWndCtrls. + */ + public ArrayList getWindowList(boolean only_visible, long exclude_ws_ex) { + ArrayList windowList = new ArrayList<>(); + User32.INSTANCE.EnumWindows((hWnd, arg1) -> { + if (User32.INSTANCE.IsWindow(hWnd) && (!only_visible || isVisible(hWnd)) + && (User32.INSTANCE.GetWindowLong(hWnd, WinUser.GWL_EXSTYLE) & exclude_ws_ex) != exclude_ws_ex) + windowList.add(new User32HWndCtrl(hWnd)); + return true; + }, null); + return windowList; + } + + @Override + public HWndCtrl getTopmostWindow() { + return new User32HWndCtrl(new WinDef.HWND(Pointer.createConstant(-1))); + } + + @Override + public HWndCtrl.MousePoint getMousePos() { + WinDef.POINT point = new WinDef.POINT(); + boolean result = User32.INSTANCE.GetCursorPos(point); + if (!result) return new HWndCtrl.MousePoint(0, 0); + return new HWndCtrl.MousePoint(point.x, point.y); + } + + @Override + public void free() {} + + @Override + public boolean needResize() { + return false; + } + + @Override + public boolean needDecorated() { + return false; + } +} diff --git a/core/src/cn/harryh/arkpets/platform/WaylandHWndCtrl.java b/core/src/cn/harryh/arkpets/platform/WaylandHWndCtrl.java new file mode 100644 index 00000000..32a26b8b --- /dev/null +++ b/core/src/cn/harryh/arkpets/platform/WaylandHWndCtrl.java @@ -0,0 +1,22 @@ +package cn.harryh.arkpets.platform; + +import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Graphics; +import com.sun.jna.Pointer; + +import static org.lwjgl.glfw.GLFWNativeWayland.glfwGetWaylandWindow; + + +public abstract class WaylandHWndCtrl extends HWndCtrl { + protected Pointer surface; + + public WaylandHWndCtrl(String windowText, WindowRect windowRect) { + super(windowText, windowRect); + } + + @Override + public void attachGLFWWindow(Lwjgl3Graphics graphics) { + super.attachGLFWWindow(graphics); + surface = new Pointer(glfwGetWaylandWindow(glfwHandle)); + } +} + diff --git a/core/src/cn/harryh/arkpets/platform/WindowSystem.java b/core/src/cn/harryh/arkpets/platform/WindowSystem.java index 0fab237b..2c3a27c1 100644 --- a/core/src/cn/harryh/arkpets/platform/WindowSystem.java +++ b/core/src/cn/harryh/arkpets/platform/WindowSystem.java @@ -1,13 +1,11 @@ -/** Copyright (c) 2022-2024, Harry Huang, Litwak913 +/** Copyright (c) 2022-2026, Harry Huang, Litwak913 * At GPL-3.0 License */ package cn.harryh.arkpets.platform; import cn.harryh.arkpets.Const; import cn.harryh.arkpets.utils.Logger; -import com.sun.jna.Platform; -import java.util.ArrayList; import java.util.List; @@ -21,6 +19,7 @@ public enum WindowSystem { NULL; private static WindowSystem PLATFORM = null; + private static HWndCtrlFactory factory; public static WindowSystem detectWindowSystem() { if (Const.isWindows) { @@ -55,10 +54,12 @@ public static void init(WindowSystem platform) { } Logger.info("System", "Using " + PLATFORM.toString() + " Window System"); switch (PLATFORM) { - case MUTTER -> MutterHWndCtrl.init(); - case KWIN -> KWinHWndCtrl.init(); - case X11 -> X11HWndCtrl.init(); - case QUARTZ -> QuartzHWndCtrl.init(); + case USER32 -> factory = new User32HWndCtrlFactory(); + case MUTTER -> factory = new MutterHWndCtrlFactory(); + case KWIN -> factory = new KWinHWndCtrlFactory(); + case X11 -> factory = new X11HWndCtrlFactory(); + case QUARTZ -> factory = new QuartzHWndCtrlFactory(); + case NULL -> factory = new NullHWndCtrlFactory(); } } @@ -75,26 +76,7 @@ public static WindowSystem getWindowSystem() { * @return The HWndCtrl, which may be null indicates not found. */ public static HWndCtrl findWindow(String className, String windowText) { - switch (PLATFORM) { - case USER32 -> { - return User32HWndCtrl.find(className, windowText); - } - case MUTTER -> { - return MutterHWndCtrl.find(className, windowText); - } - case KWIN -> { - return KWinHWndCtrl.find(className, windowText); - } - case X11 -> { - return X11HWndCtrl.find(className, windowText); - } - case QUARTZ -> { - return QuartzHWndCtrl.find(className, windowText); - } - default -> { - return NullHWndCtrl.find(className, windowText); - } - } + return factory.findWindow(className, windowText); } /** Gets the list of current windows. @@ -102,91 +84,40 @@ public static HWndCtrl findWindow(String className, String windowText) { * @return An ArrayList consists of HWndCtrls. */ public static List getWindowList(boolean onlyVisible) { - switch (PLATFORM) { - case USER32 -> { - return User32HWndCtrl.getWindowList(onlyVisible); - } - case MUTTER -> { - return MutterHWndCtrl.getWindowList(onlyVisible); - } - case KWIN -> { - return KWinHWndCtrl.getWindowList(onlyVisible); - } - case X11 -> { - return X11HWndCtrl.getWindowList(onlyVisible); - } - case QUARTZ -> { - return QuartzHWndCtrl.getWindowList(onlyVisible); - } - default -> { - return new ArrayList<>(); - } - } + return factory.getWindowList(onlyVisible); } /** Gets the topmost window. * @return The topmost window's HWndCtrl. */ public static HWndCtrl getTopmostWindow() { - switch (PLATFORM) { - case USER32 -> { - return User32HWndCtrl.getTopmostWindow(); - } - case MUTTER -> { - return MutterHWndCtrl.getTopmostWindow(); - } - case KWIN -> { - return KWinHWndCtrl.getTopmostWindow(); - } - case X11 -> { - return X11HWndCtrl.getTopmost(); - } - default -> { - return new NullHWndCtrl(); - } - } + return factory.getTopmostWindow(); + } + + /** Gets the mouse position. + * @return The MousePoint record. + */ + public static HWndCtrl.MousePoint getMousePos() { + return factory.getMousePos(); } /** Frees all the resources. */ public static void free() { - switch (PLATFORM) { - case MUTTER -> MutterHWndCtrl.free(); - case KWIN -> KWinHWndCtrl.free(); - case X11 -> X11HWndCtrl.free(); - case QUARTZ -> QuartzHWndCtrl.free(); - } + if (factory == null) return; + factory.free(); + factory = null; } /** Return current WindowSystem should enable resize. */ public static boolean needResize() { - switch (PLATFORM) { - case X11, MUTTER, KWIN -> { - return true; - } - default -> { - return false; - } - } + return factory.needResize(); } /** Return current WindowSystem should enable decoration. */ public static boolean needDecorated() { - return PLATFORM == NULL; - } - - /** Return current WindowSystem information request rate. - */ - public static int getRequestRate() { - switch (PLATFORM) { - case MUTTER,KWIN,X11,QUARTZ -> { // IPC - return 6; - } - default -> { - return 4; - } - } + return factory.needDecorated(); } } diff --git a/core/src/cn/harryh/arkpets/platform/X11HWndCtrl.java b/core/src/cn/harryh/arkpets/platform/X11HWndCtrl.java index 9d216e8b..e565b753 100644 --- a/core/src/cn/harryh/arkpets/platform/X11HWndCtrl.java +++ b/core/src/cn/harryh/arkpets/platform/X11HWndCtrl.java @@ -2,26 +2,16 @@ import cn.harryh.arkpets.natives.X11Extension; import cn.harryh.arkpets.natives.X11Helper; -import cn.harryh.arkpets.natives.XextExtension; -import cn.harryh.arkpets.utils.Logger; -import com.sun.jna.Pointer; import com.sun.jna.platform.unix.X11; import com.sun.jna.ptr.IntByReference; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; +import static cn.harryh.arkpets.platform.X11HWndCtrlFactory.display; public class X11HWndCtrl extends HWndCtrl { - private static X11.Display display; private static final X11Extension x11 = X11Extension.INSTANCE; - private static final XextExtension xext = XextExtension.INSTANCE; protected final X11.Window hWnd; - private static boolean shapeAvailable; - private boolean transparentEnable; - public static final int STATE_REMOVE = 0; public static final int STATE_ADD = 1; @@ -30,65 +20,6 @@ public X11HWndCtrl(X11Extension.Window hWnd) { this.hWnd = hWnd; } - public static void init() { - display = x11.XOpenDisplay(null); - - if (display == null) { - throw new RuntimeException("Cannot open X display"); - } else { - Logger.info("System", "Connected to X display"); - } - IntByReference evt = new IntByReference(); - IntByReference err = new IntByReference(); - boolean xshape = xext.XShapeQueryExtension(display, evt, err); - if (!xshape) { - Logger.warn("System", "No XShape extension"); - shapeAvailable = false; - } else { - shapeAvailable = true; - } - } - - public static HWndCtrl find(String className, String windowName) { - X11Extension.Window[] wids = getWindows(); - for (X11Extension.Window win : wids) { - String wtitle = winText(win); - String wclass = X11Helper.getUtf8Property(display, win, X11.XA_STRING, X11.XA_WM_CLASS); - if (className == null) { - if (wtitle.equals(windowName)) { - return new X11HWndCtrl(win); - } - } else { - if (wclass.equals(className) && wtitle.equals(windowName)) { - return new X11HWndCtrl(win); - } - } - } - return null; - } - - public static List getWindowList(boolean onlyVisible) { - ArrayList windowList = new ArrayList<>(); - X11Extension.Window[] wins = getWindows(); - for (X11.Window win : wins) { - if (!onlyVisible || visible(win)) { - windowList.add(new X11HWndCtrl(win)); - } - } - Collections.reverse(windowList); - return windowList; - } - - public static HWndCtrl getTopmost() { - List list = getWindowList(true); - return list.isEmpty() ? null : list.get(0); - } - - public static void free() { - x11.XCloseDisplay(display); - Logger.info("System", "Disconnected from X display"); - } - protected static WindowRect getWindowRect(X11Extension.Window hWnd) { X11.WindowByReference junkRoot = new X11.WindowByReference(); IntByReference junkX = new IntByReference(); @@ -156,23 +87,8 @@ public void setWindowPosition(HWndCtrl insertAfter, int x, int y, int w, int h) // 0000 0000 1111 0001 //clientMsg(hWnd,"_NET_MOVERESIZE_WINDOW",3840,x,y,w,h); - x11.XSync(display, false); x11.XMoveResizeWindow(display, hWnd, x, y, w, h); - } - - @Override - public void setTransparent(boolean transparent) { - if (!shapeAvailable) return; - if (transparentEnable != transparent) { - if (transparent) { - Pointer reg = x11.XCreateRegion(); - xext.XShapeCombineRegion(display,hWnd, X11.Xext.ShapeInput,0,0,reg, X11.Xext.ShapeSet); - x11.XDestroyRegion(reg); - } else { - xext.XShapeCombineMask(display,hWnd, X11.Xext.ShapeInput,0,0,null, X11.Xext.ShapeSet); - } - transparentEnable = transparent; - } + x11.XSync(display, false); } @Override @@ -186,11 +102,6 @@ public void setTaskbar(boolean enable) { } } - @Override - public void setLayered(boolean enable) { - // unnecessary in X11. - } - @Override public void setTopmost(boolean enable) { if (enable) { @@ -219,26 +130,13 @@ public int hashCode() { return hWnd.hashCode(); } - private static X11Extension.Window[] getWindows() { - X11Extension.Window rootWindow = x11.XDefaultRootWindow(display); - byte[] bytes = X11Helper.getProperty(display, rootWindow, X11Extension.XA_WINDOW, X11Helper.getAtom(display, "_NET_CLIENT_LIST_STACKING")); - - X11Extension.Window[] windowList = new X11Extension.Window[bytes.length / X11.Window.SIZE]; - - for (int i = 0; i < windowList.length; i++) { - windowList[i] = new X11.Window(X11Helper.bytesToInt(bytes, X11.XID.SIZE * i)); - } - - return windowList; - } - - private static String winText(X11Extension.Window hWnd) { + static String winText(X11Extension.Window hWnd) { String title; title = X11Helper.getUtf8Property(display, hWnd, X11Helper.getAtom(display, "UTF8_STRING"), X11Helper.getAtom(display, "_NET_WM_NAME")); return title; } - private static boolean visible(X11.Window hWnd) { + static boolean visible(X11.Window hWnd) { X11.XWindowAttributes attr = new X11.XWindowAttributes(); x11.XGetWindowAttributes(display, hWnd, attr); if (attr.map_state != X11.IsViewable) { diff --git a/core/src/cn/harryh/arkpets/platform/X11HWndCtrlFactory.java b/core/src/cn/harryh/arkpets/platform/X11HWndCtrlFactory.java new file mode 100644 index 00000000..c492bacc --- /dev/null +++ b/core/src/cn/harryh/arkpets/platform/X11HWndCtrlFactory.java @@ -0,0 +1,121 @@ +package cn.harryh.arkpets.platform; + +import cn.harryh.arkpets.natives.X11Extension; +import cn.harryh.arkpets.natives.X11Helper; +import cn.harryh.arkpets.utils.Logger; +import com.sun.jna.platform.unix.X11; +import com.sun.jna.ptr.IntByReference; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static cn.harryh.arkpets.platform.X11HWndCtrl.visible; +import static cn.harryh.arkpets.platform.X11HWndCtrl.winText; + + +public class X11HWndCtrlFactory extends HWndCtrlFactory{ + private static final X11Extension x11 = X11Extension.INSTANCE; + + private static final X11.XErrorHandler handler = new ErrorHandler(); + + static X11.Display display; + + + public X11HWndCtrlFactory() { + display = x11.XOpenDisplay(null); + + if (display == null) { + throw new RuntimeException("Cannot open X display"); + } else { + Logger.info("System", "Connected to X display"); + } + + x11.XSetErrorHandler(handler); + } + + @Override + public HWndCtrl findWindow(String className, String windowName) { + X11Extension.Window[] wids = getWindows(); + for (X11Extension.Window win : wids) { + String wtitle = winText(win); + String wclass = X11Helper.getUtf8Property(display, win, X11.XA_STRING, X11.XA_WM_CLASS); + if (className == null) { + if (wtitle.equals(windowName)) { + return new X11HWndCtrl(win); + } + } else { + if (wclass.equals(className) && wtitle.equals(windowName)) { + return new X11HWndCtrl(win); + } + } + } + return null; + } + + @Override + public List getWindowList(boolean onlyVisible) { + ArrayList windowList = new ArrayList<>(); + X11Extension.Window[] wins = getWindows(); + for (X11.Window win : wins) { + if (!onlyVisible || visible(win)) { + windowList.add(new X11HWndCtrl(win)); + } + } + Collections.reverse(windowList); + return windowList; + } + + @Override + public HWndCtrl getTopmostWindow() { + List list = getWindowList(true); + return list.isEmpty() ? null : list.get(0); + } + + @Override + public HWndCtrl.MousePoint getMousePos() { + IntByReference rootX = new IntByReference(); + IntByReference rootY = new IntByReference(); + IntByReference junk = new IntByReference(); + X11.WindowByReference junkW = new X11.WindowByReference(); + x11.XQueryPointer(display,x11.XDefaultRootWindow(display),junkW,junkW,rootX,rootY,junk,junk,junk); + return new HWndCtrl.MousePoint(rootX.getValue(),rootY.getValue()); + } + + @Override + public void free() { + if(display != null) x11.XCloseDisplay(display); + Logger.info("System", "Disconnected from X display"); + } + + @Override + public boolean needResize() { + return true; + } + + @Override + public boolean needDecorated() { + return false; + } + + private static X11Extension.Window[] getWindows() { + X11Extension.Window rootWindow = x11.XDefaultRootWindow(display); + byte[] bytes = X11Helper.getProperty(display, rootWindow, X11Extension.XA_WINDOW, X11Helper.getAtom(display, "_NET_CLIENT_LIST_STACKING")); + + X11Extension.Window[] windowList = new X11Extension.Window[bytes.length / X11.Window.SIZE]; + + for (int i = 0; i < windowList.length; i++) { + windowList[i] = new X11.Window(X11Helper.bytesToInt(bytes, X11.XID.SIZE * i)); + } + + return windowList; + } + + private static class ErrorHandler implements X11.XErrorHandler { + @Override + public int apply(X11.Display display, X11.XErrorEvent errorEvent) { + Logger.error("System","X Error " + errorEvent.toString()); + return 0; + } + } +} diff --git a/core/src/cn/harryh/arkpets/natives/KWinInterface.java b/core/src/cn/harryh/arkpets/rpc/KWinInterface.java similarity index 83% rename from core/src/cn/harryh/arkpets/natives/KWinInterface.java rename to core/src/cn/harryh/arkpets/rpc/KWinInterface.java index 425590b2..9ae2f2e6 100644 --- a/core/src/cn/harryh/arkpets/natives/KWinInterface.java +++ b/core/src/cn/harryh/arkpets/rpc/KWinInterface.java @@ -1,4 +1,4 @@ -package cn.harryh.arkpets.natives; +package cn.harryh.arkpets.rpc; import org.freedesktop.dbus.Struct; import org.freedesktop.dbus.annotations.DBusInterfaceName; @@ -27,6 +27,8 @@ public interface KWinInterface extends DBusInterface { UInt32 Version(); + PointStruct Mouse(); + class DetailsStruct extends Struct { @Position(0) public final int x; @@ -56,4 +58,16 @@ public DetailsStruct(int x, int y, UInt32 w, UInt32 h, String title, String wCla this.id = id; } } + + class PointStruct extends Struct { + @Position(0) + public final int x; + @Position(1) + public final int y; + + public PointStruct(int x, int y) { + this.x = x; + this.y = y; + } + } } \ No newline at end of file diff --git a/core/src/cn/harryh/arkpets/natives/KWinPluginInterface.java b/core/src/cn/harryh/arkpets/rpc/KWinPluginInterface.java similarity index 95% rename from core/src/cn/harryh/arkpets/natives/KWinPluginInterface.java rename to core/src/cn/harryh/arkpets/rpc/KWinPluginInterface.java index 76a61cb9..8d19718f 100644 --- a/core/src/cn/harryh/arkpets/natives/KWinPluginInterface.java +++ b/core/src/cn/harryh/arkpets/rpc/KWinPluginInterface.java @@ -1,4 +1,4 @@ -package cn.harryh.arkpets.natives; +package cn.harryh.arkpets.rpc; import org.freedesktop.dbus.TypeRef; import org.freedesktop.dbus.annotations.DBusBoundProperty; diff --git a/core/src/cn/harryh/arkpets/natives/MutterInterface.java b/core/src/cn/harryh/arkpets/rpc/MutterInterface.java similarity index 83% rename from core/src/cn/harryh/arkpets/natives/MutterInterface.java rename to core/src/cn/harryh/arkpets/rpc/MutterInterface.java index 766fba7e..916e008e 100644 --- a/core/src/cn/harryh/arkpets/natives/MutterInterface.java +++ b/core/src/cn/harryh/arkpets/rpc/MutterInterface.java @@ -1,4 +1,4 @@ -package cn.harryh.arkpets.natives; +package cn.harryh.arkpets.rpc; import org.freedesktop.dbus.Struct; import org.freedesktop.dbus.annotations.DBusInterfaceName; @@ -27,6 +27,8 @@ public interface MutterInterface extends DBusInterface { String Version(); + PointStruct Mouse(); + class DetailsStruct extends Struct { @Position(0) public final int x; @@ -56,4 +58,16 @@ public DetailsStruct(int x, int y, UInt32 w, UInt32 h, String title, String wCla this.id = id; } } + + class PointStruct extends Struct { + @Position(0) + public final int x; + @Position(1) + public final int y; + + public PointStruct(int x, int y) { + this.x = x; + this.y = y; + } + } } \ No newline at end of file diff --git a/core/src/cn/harryh/arkpets/natives/MutterPluginInterface.java b/core/src/cn/harryh/arkpets/rpc/MutterPluginInterface.java similarity index 93% rename from core/src/cn/harryh/arkpets/natives/MutterPluginInterface.java rename to core/src/cn/harryh/arkpets/rpc/MutterPluginInterface.java index 931bdd62..c1314085 100644 --- a/core/src/cn/harryh/arkpets/natives/MutterPluginInterface.java +++ b/core/src/cn/harryh/arkpets/rpc/MutterPluginInterface.java @@ -1,4 +1,4 @@ -package cn.harryh.arkpets.natives; +package cn.harryh.arkpets.rpc; import org.freedesktop.dbus.annotations.DBusBoundProperty; import org.freedesktop.dbus.annotations.DBusInterfaceName; diff --git a/core/src/cn/harryh/arkpets/transitions/EasingFunction.java b/core/src/cn/harryh/arkpets/transitions/EasingFunction.java index b354ceea..5b208308 100644 --- a/core/src/cn/harryh/arkpets/transitions/EasingFunction.java +++ b/core/src/cn/harryh/arkpets/transitions/EasingFunction.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.transitions; diff --git a/core/src/cn/harryh/arkpets/transitions/TernaryFunction.java b/core/src/cn/harryh/arkpets/transitions/TernaryFunction.java index d1879367..5eec50ef 100644 --- a/core/src/cn/harryh/arkpets/transitions/TernaryFunction.java +++ b/core/src/cn/harryh/arkpets/transitions/TernaryFunction.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.transitions; diff --git a/core/src/cn/harryh/arkpets/transitions/Transition.java b/core/src/cn/harryh/arkpets/transitions/Transition.java index e0273b8b..778eb167 100644 --- a/core/src/cn/harryh/arkpets/transitions/Transition.java +++ b/core/src/cn/harryh/arkpets/transitions/Transition.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.transitions; diff --git a/core/src/cn/harryh/arkpets/transitions/TransitionFloat.java b/core/src/cn/harryh/arkpets/transitions/TransitionFloat.java index a8e78a78..9db56891 100644 --- a/core/src/cn/harryh/arkpets/transitions/TransitionFloat.java +++ b/core/src/cn/harryh/arkpets/transitions/TransitionFloat.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.transitions; diff --git a/core/src/cn/harryh/arkpets/transitions/TransitionVector2.java b/core/src/cn/harryh/arkpets/transitions/TransitionVector2.java index 177c5210..560216df 100644 --- a/core/src/cn/harryh/arkpets/transitions/TransitionVector2.java +++ b/core/src/cn/harryh/arkpets/transitions/TransitionVector2.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.transitions; diff --git a/core/src/cn/harryh/arkpets/transitions/TransitionVector3.java b/core/src/cn/harryh/arkpets/transitions/TransitionVector3.java index bc061fd0..59ead77b 100644 --- a/core/src/cn/harryh/arkpets/transitions/TransitionVector3.java +++ b/core/src/cn/harryh/arkpets/transitions/TransitionVector3.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.transitions; diff --git a/core/src/cn/harryh/arkpets/tray/HostTray.java b/core/src/cn/harryh/arkpets/tray/HostTray.java index b664ca5f..69549b6b 100644 --- a/core/src/cn/harryh/arkpets/tray/HostTray.java +++ b/core/src/cn/harryh/arkpets/tray/HostTray.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang, Half Nothing +/** Copyright (c) 2022-2026, Harry Huang, Half Nothing * At GPL-3.0 License */ package cn.harryh.arkpets.tray; diff --git a/core/src/cn/harryh/arkpets/tray/MemberTray.java b/core/src/cn/harryh/arkpets/tray/MemberTray.java index d16c942a..1060294b 100644 --- a/core/src/cn/harryh/arkpets/tray/MemberTray.java +++ b/core/src/cn/harryh/arkpets/tray/MemberTray.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang, Half Nothing +/** Copyright (c) 2022-2026, Harry Huang, Half Nothing * At GPL-3.0 License */ package cn.harryh.arkpets.tray; diff --git a/core/src/cn/harryh/arkpets/tray/MemberTrayImpl.java b/core/src/cn/harryh/arkpets/tray/MemberTrayImpl.java index ece5dd4f..faeed4a0 100644 --- a/core/src/cn/harryh/arkpets/tray/MemberTrayImpl.java +++ b/core/src/cn/harryh/arkpets/tray/MemberTrayImpl.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang, Half Nothing +/** Copyright (c) 2022-2026, Harry Huang, Half Nothing * At GPL-3.0 License */ package cn.harryh.arkpets.tray; diff --git a/core/src/cn/harryh/arkpets/tray/MemberTrayProxy.java b/core/src/cn/harryh/arkpets/tray/MemberTrayProxy.java index 5cb0e793..fbeaac16 100644 --- a/core/src/cn/harryh/arkpets/tray/MemberTrayProxy.java +++ b/core/src/cn/harryh/arkpets/tray/MemberTrayProxy.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang, Half Nothing +/** Copyright (c) 2022-2026, Harry Huang, Half Nothing * At GPL-3.0 License */ package cn.harryh.arkpets.tray; diff --git a/core/src/cn/harryh/arkpets/utils/Cached.java b/core/src/cn/harryh/arkpets/utils/Cached.java index c26b9729..1abbf826 100644 --- a/core/src/cn/harryh/arkpets/utils/Cached.java +++ b/core/src/cn/harryh/arkpets/utils/Cached.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.utils; @@ -47,6 +47,7 @@ public T getValue() { if (valueProducer != null && isExpired()) { setValue(valueProducer.get()); } + valueChangedFlag = false; return cachedValue; } } diff --git a/core/src/cn/harryh/arkpets/utils/DynamicOrthographicCamara.java b/core/src/cn/harryh/arkpets/utils/DynamicOrthographicCamara.java index ddcdfb35..3d16f9e7 100644 --- a/core/src/cn/harryh/arkpets/utils/DynamicOrthographicCamara.java +++ b/core/src/cn/harryh/arkpets/utils/DynamicOrthographicCamara.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.utils; @@ -6,6 +6,7 @@ import com.badlogic.gdx.graphics.OrthographicCamera; import com.badlogic.gdx.graphics.Pixmap; import com.badlogic.gdx.graphics.glutils.FrameBuffer; +import com.badlogic.gdx.graphics.glutils.HdpiUtils; import java.util.Objects; @@ -133,16 +134,30 @@ public int getHeight() { return curInsert.top + curInsert.bottom + initHeight; } + /** Gets the back buffer width. + * @return The back buffer width. + */ + public int getBackWidth() { + return HdpiUtils.toBackBufferX((curInsert.left + curInsert.right + initWidth)); + } + + /** Gets the back buffer height. + * @return The back buffer height. + */ + public int getBackHeight() { + return HdpiUtils.toBackBufferY((curInsert.top + curInsert.bottom + initHeight)); + } + /** Gets the FrameBuffer Object that has the same width and height with this camera. * Note that the returned FrameBuffer Object may be a cached one. * @return The FrameBuffer Object. */ public FrameBuffer getFBO() { if (fbo == null) { - fbo = new FrameBuffer(Pixmap.Format.RGBA8888, getWidth(), getHeight(), false); - } else if (fbo.getWidth() != getWidth() || fbo.getHeight() != getHeight()) { + fbo = new FrameBuffer(Pixmap.Format.RGBA8888, getBackWidth(), getBackHeight(), false); + } else if (fbo.getWidth() != getBackWidth() || fbo.getHeight() != getBackHeight()) { fbo.dispose(); - fbo = new FrameBuffer(Pixmap.Format.RGBA8888, getWidth(), getHeight(), false); + fbo = new FrameBuffer(Pixmap.Format.RGBA8888, getBackWidth(), getBackHeight(), false); } return fbo; } diff --git a/core/src/cn/harryh/arkpets/utils/IOUtils.java b/core/src/cn/harryh/arkpets/utils/IOUtils.java index abe69952..3c85364d 100644 --- a/core/src/cn/harryh/arkpets/utils/IOUtils.java +++ b/core/src/cn/harryh/arkpets/utils/IOUtils.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.utils; @@ -282,15 +282,15 @@ private static void copyFileToZip(ZipOutputStream zos, File sourceFile, String z public static class CommandUtil { /** * Run a command and get the output. - * @param command The command will run. + * @param cmdargs The command will run. * @param env The environment variable. * @param workdir The working directory. * @throws IOException If I/O error occurs. * @return The command output,Return null if failed. */ - public static String runCommand(String command, String[] env, File workdir) throws IOException { + public static String runCommand(String[] cmdargs, String[] env, File workdir) throws IOException { Runtime runtime = Runtime.getRuntime(); - Process process = runtime.exec(command, env, workdir); + Process process = runtime.exec(cmdargs, env, workdir); try { process.waitFor(); } catch (InterruptedException ignore) { diff --git a/core/src/cn/harryh/arkpets/utils/InputApplicationAdaptor.java b/core/src/cn/harryh/arkpets/utils/InputApplicationAdaptor.java index 2689ded1..4db33d8a 100644 --- a/core/src/cn/harryh/arkpets/utils/InputApplicationAdaptor.java +++ b/core/src/cn/harryh/arkpets/utils/InputApplicationAdaptor.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.utils; @@ -17,7 +17,10 @@ abstract public class InputApplicationAdaptor extends ApplicationAdapter impleme private int mouseDeltaX = 0; private int mouseDeltaY = 0; private int mouseButton = 0; - private int mouseIntention = 1; + private int lastDragDeltaX = 0; + private int lastDragDeltaY = 0; + private int lastMoveDeltaX = 0; + private int lastMoveDeltaY = 0; private boolean isMouseDragging = false; private boolean isMouseDown = false; @@ -78,8 +81,20 @@ public final int getMouseButton() { return mouseButton; } - public final int getMouseIntention() { - return mouseIntention; + public final int getLastDragDeltaX() { + return lastDragDeltaX; + } + + public final int getLastDragDeltaY() { + return lastDragDeltaY; + } + + public final int getLastMoveDeltaX() { + return lastMoveDeltaX; + } + + public final int getLastMoveDeltaY() { + return lastMoveDeltaY; } public final double getLastActiveDeltaTime() { @@ -136,6 +151,10 @@ public boolean touchDown(int screenX, int screenY, int pointer, int button) { mouseY = screenY; mouseDeltaX = 0; mouseDeltaY = 0; + lastDragDeltaX = 0; + lastDragDeltaY = 0; + lastMoveDeltaX = 0; + lastMoveDeltaY = 0; mouseButton = button; isMouseDown = true; onMouseDown(); @@ -168,8 +187,9 @@ public boolean touchDragged(int screenX, int screenY, int pointer) { if (pointer <= 0) { mouseDeltaX = screenX - mouseX; mouseDeltaY = screenY - mouseY; + lastDragDeltaX = mouseDeltaX * lastDragDeltaX <= 0 ? mouseDeltaX : lastDragDeltaX + mouseDeltaX; + lastDragDeltaY = mouseDeltaY * lastDragDeltaY <= 0 ? mouseDeltaY : lastDragDeltaY + mouseDeltaY; isMouseDragging = true; - mouseIntention = mouseDeltaX == 0 ? mouseIntention : mouseDeltaX > 0 ? 1 : -1; onMouseDrag(); } return false; @@ -178,8 +198,12 @@ public boolean touchDragged(int screenX, int screenY, int pointer) { @Deprecated @Override public boolean mouseMoved(int screenX, int screenY) { + int dx = screenX - mouseX; + int dy = screenY - mouseY; mouseX = screenX; mouseY = screenY; + lastMoveDeltaX = dx * lastMoveDeltaX <= 0 ? dx : lastMoveDeltaX + dx; + lastMoveDeltaY = dy * lastMoveDeltaY <= 0 ? dy : lastMoveDeltaY + dy; onMouseMoved(); return true; } diff --git a/core/src/cn/harryh/arkpets/utils/Logger.java b/core/src/cn/harryh/arkpets/utils/Logger.java index 22871b7c..b986e49f 100644 --- a/core/src/cn/harryh/arkpets/utils/Logger.java +++ b/core/src/cn/harryh/arkpets/utils/Logger.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.utils; diff --git a/core/src/cn/harryh/arkpets/utils/Monitor.java b/core/src/cn/harryh/arkpets/utils/Monitor.java index 189ee2c7..f6e88deb 100644 --- a/core/src/cn/harryh/arkpets/utils/Monitor.java +++ b/core/src/cn/harryh/arkpets/utils/Monitor.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.utils; diff --git a/core/src/cn/harryh/arkpets/utils/PixmapWrapper.java b/core/src/cn/harryh/arkpets/utils/PixmapWrapper.java new file mode 100644 index 00000000..ed681b6c --- /dev/null +++ b/core/src/cn/harryh/arkpets/utils/PixmapWrapper.java @@ -0,0 +1,155 @@ +/** Copyright (c) 2022-2026, Harry Huang + * At GPL-3.0 License + */ +package cn.harryh.arkpets.utils; + +import com.badlogic.gdx.files.FileHandle; +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.graphics.Pixmap; +import com.badlogic.gdx.graphics.PixmapIO; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.zip.Deflater; + + +public class PixmapWrapper { + protected Pixmap pixmap; + + protected static HashMap cmapCache = new HashMap<>(4); + + public PixmapWrapper(Pixmap pixmap) { + this.pixmap = pixmap; + } + + /** Gets the internal pixmap. + * @return The internal pixmap itself. + */ + public Pixmap getPixmap() { + if (pixmap == null) throw new IllegalStateException("Pixmap is null or has been disposed"); + return pixmap; + } + + /** Saves the internal pixmap to a PNG file. + * @param file The target file handle. + * @param flipY Whether to flip the pixmap vertically when saving. + */ + public void savePixmap(FileHandle file, boolean flipY) { + if (pixmap == null) throw new IllegalStateException("Pixmap is null or has been disposed"); + PixmapIO.writePNG(file, pixmap, Deflater.DEFAULT_COMPRESSION, flipY); + } + + /** Draws an unfilled rectangle on the pixmap. + * @param color The color of the rectangle's border. + * @param x The x coordinate of the rectangle's top-left corner. + * @param y The y coordinate of the rectangle's top-left corner. + * @param width The width of the rectangle. + * @param height The height of the rectangle. + * @param thickness The thickness of the rectangle's border. + */ + public void drawUnfilledRectangle(Color color, int x, int y, int width, int height, int thickness) { + if (pixmap == null) throw new IllegalStateException("Pixmap is null or has been disposed"); + if (thickness < 1) throw new IllegalArgumentException("Thickness must be at least 1"); + if (width < 1) throw new IllegalArgumentException("Width must be at least 1"); + if (height < 1) throw new IllegalArgumentException("Height must be at least 1"); + thickness = Math.min(thickness, Math.min(width / 2, height / 2)); + + pixmap.setColor(color); + if (thickness == 1) { + pixmap.drawRectangle(x, y, width, height); + } else { + // Top + pixmap.fillRectangle(x, y, width, thickness); + // Bottom + pixmap.fillRectangle(x, y + height - thickness, width, thickness); + // Left + pixmap.fillRectangle(x, y + thickness, thickness, height - 2 * thickness); + // Right + pixmap.fillRectangle(x + width - thickness, y + thickness, thickness, height - 2 * thickness); + } + } + + /** Draws the color map on the pixmap by the given style and channel. + * @param style The style name of the color map. See {@link #getCmap(String)} for supported style names. + * @param channel The channel to be mapped. Supported values: "r", "g", "b", "a" or their upper-cased one. + */ + public void drawCmap(String style, String channel) { + if (pixmap == null) throw new IllegalStateException("Pixmap is null or has been disposed"); + + Color[] cmap = getCmap(style); + int[] cmapInts = Arrays.stream(cmap).mapToInt(Color::rgba8888).toArray(); + + int shift; + int mask; + switch (channel) { + case "r", "R" -> { mask = 0xFF000000; shift = 24; } + case "g", "G" -> { mask = 0x00FF0000; shift = 16; } + case "b", "B" -> { mask = 0x0000FF00; shift = 8; } + case "a", "A" -> { mask = 0x000000FF; shift = 0; } + default -> throw new IllegalArgumentException("Unsupported channel: " + channel); + } + + int width = pixmap.getWidth(); + int height = pixmap.getHeight(); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int pixel = pixmap.getPixel(x, y); + int index = (pixel & mask) >>> shift; + pixmap.drawPixel(x, y, cmapInts[index]); + } + } + } + + /** Disposes the internal pixmap. + * After disposing, the PixmapWrapper object cannot be used anymore. + */ + public void dispose() { + if (pixmap != null && !pixmap.isDisposed()) { + pixmap.dispose(); + pixmap = null; + } + } + + /** Creates a PixmapWrapper object from the given DynamicOrthographicCamara's framebuffer. + * @param camara The given DynamicOrthographicCamara. + * @return The created PixmapWrapper object. + */ + public static PixmapWrapper fromCamera(DynamicOrthographicCamara camara) { + return new PixmapWrapper( + Pixmap.createFromFrameBuffer(0, 0, camara.getWidth(), camara.getHeight()) + ); + } + + /** Gets the color map by the given style name. + * @param style The style name. Currently supported styles: "tab16", "tab16t". + * @return The color mapping array (length 256). Note that the returned array is cached. + */ + public static Color[] getCmap(String style) { + if (cmapCache.containsKey(style)) { + return cmapCache.get(style); + } else { + // Build camp + if (style.equals("tab16") || style.equals("tab16t")) { + int[] tab16Hex = { + 0xFFFFFF, 0xD3D3D3, 0xA8A8A8, 0x5FA0D0, + 0xAFD2E1, 0xC3EDAF, 0xF5E66E, 0xF8B85E, + 0xFA8A4E, 0xFD5B3D, 0xFF2D2D, 0xEBA9D8, + 0xF5CEEF, 0xF0BCC8, 0xDC7264, 0xC82800 + }; + Color[] tab16Colors = new Color[256]; + for (int i = 0; i < 16; i++) { + Color c = new Color(tab16Hex[i] << 8 | 0xFF); + Arrays.fill(tab16Colors, i * 16, (i + 1) * 16, c); + } + cmapCache.put(style, tab16Colors); + if (style.equals("tab16t")) { + tab16Colors[0] = new Color(0, 0, 0, 0); + } + return tab16Colors; + } else { + throw new IllegalArgumentException("Unsupported cmap style: " + style); + } + } + } +} diff --git a/core/src/cn/harryh/arkpets/utils/Plane.java b/core/src/cn/harryh/arkpets/utils/Plane.java index 0de73f75..c8a0b223 100644 --- a/core/src/cn/harryh/arkpets/utils/Plane.java +++ b/core/src/cn/harryh/arkpets/utils/Plane.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.utils; @@ -8,7 +8,7 @@ import java.util.ArrayList; -import static cn.harryh.arkpets.Const.droppedThreshold; +import static cn.harryh.arkpets.Const.droppedYThreshold; public class Plane { @@ -166,7 +166,7 @@ public float getY() { public boolean getDropped() { if (dropped) { dropped = false; // Reset - if (droppedHeight >= droppedThreshold) { + if (droppedHeight >= droppedYThreshold) { droppedHeight = 0; // Reset return true; } @@ -178,7 +178,7 @@ public boolean getDropped() { * @return true=dropping. */ public boolean getDropping() { - return Math.abs(position.y - borderBottom()) > droppedThreshold; + return Math.abs(position.y - borderBottom()) > droppedYThreshold; } /** Gets the debug message. diff --git a/core/src/cn/harryh/arkpets/utils/SecretUtils.java b/core/src/cn/harryh/arkpets/utils/SecretUtils.java index edb8664a..c90435d7 100644 --- a/core/src/cn/harryh/arkpets/utils/SecretUtils.java +++ b/core/src/cn/harryh/arkpets/utils/SecretUtils.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.utils; diff --git a/core/src/cn/harryh/arkpets/utils/Version.java b/core/src/cn/harryh/arkpets/utils/Version.java index a1b61564..92b043f3 100644 --- a/core/src/cn/harryh/arkpets/utils/Version.java +++ b/core/src/cn/harryh/arkpets/utils/Version.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.utils; diff --git a/desktop/build.gradle b/desktop/build.gradle index 6d7a6300..dad3d4b3 100644 --- a/desktop/build.gradle +++ b/desktop/build.gradle @@ -8,6 +8,18 @@ eclipse.project.name = appName + "-desktop" import org.gradle.internal.os.OperatingSystem +Task getDistTask() { + switch (OperatingSystem.current()) { + case OperatingSystem.LINUX: + return distAppImage + case OperatingSystem.WINDOWS: + return distExe + case OperatingSystem.MAC_OS: + return distDmg + } + return null +} + processResources { includeEmptyDirs = false excludes = [ @@ -19,12 +31,16 @@ processResources { } // Runs the app without debug. -task run(dependsOn: classes, type: JavaExec, group: 'execute') { - mainClass = project.mainClassName +tasks.register("run", JavaExec) { + dependsOn = ["classes"] + group = "execute" + + mainClass = mainClassName classpath = sourceSets.main.runtimeClasspath standardInput = System.in - workingDir = project.assetsDir - ignoreExitValue = true + workingDir = assetsDir + + setIgnoreExitValue(true) if (OperatingSystem.current() == OperatingSystem.MAC_OS) { jvmArgs += "-XstartOnFirstThread" // Required to run on macOS @@ -32,13 +48,17 @@ task run(dependsOn: classes, type: JavaExec, group: 'execute') { } // Runs the app within debug. -task debug(dependsOn: classes, type: JavaExec, group: 'execute') { +tasks.register("debug", JavaExec) { + dependsOn = ["classes"] + group = "execute" + mainClass = project.mainClassName classpath = sourceSets.main.runtimeClasspath standardInput = System.in workingDir = project.assetsDir - ignoreExitValue = true - debug = true + + setIgnoreExitValue(true) + setDebug(true) } @@ -46,34 +66,30 @@ task debug(dependsOn: classes, type: JavaExec, group: 'execute') { ext { // Environment vars - rootDir = file('.').absolutePath + rootDir = project.rootDir javaHome = System.getProperty('java.home') osName = System.getProperty('os.name').toLowerCase(Locale.ROOT).split(' ')[0] osPathSep = File.pathSeparatorChar // Distribution related vars - jarLibDir = "${buildDir}/libs" - jarLibName = "${project.name}-${project.version}" - jlinkRuntimeDir = "${buildDir}/jlink" - jlinkRuntimeImg = "${jlinkRuntimeDir}/runtime" + jarLibDir = layout.buildDirectory.dir("libs").get().toString() + jarLibName = project.name + "-" + project.version + jlinkDir = layout.buildDirectory.dir("jlink").get().toString() jlinkModuleList = "java.base,java.desktop,java.logging,java.management,java.scripting,jdk.crypto.ec,jdk.localedata,jdk.unsupported" jlinkLocalesList = "en-US,zh-CN" - jpackageDir = "${buildDir}/jpackage" + jpackageDir = layout.buildDirectory.dir("jpackage").get().toString() issFileRel = "docs/scripts/ExePacking.iss" - distDir = "${buildDir}/dist" - distName = "${project.ext.appName}-v${project.version}" + macPackage = "docs/scripts/macPackage.sh" + appImagePackage = "docs/scripts/appImagePackage.sh" + distDir = layout.buildDirectory.dir("dist").get().toString() + distName = project.appName + "-v" + project.version + copyrightText = "Copyright (C) 2022-2026 Harry Huang" } // Generates a distributable JAR file for the app. -task distJar(dependsOn: classes, type: Jar, group: 'dist') { - duplicatesStrategy(DuplicatesStrategy.EXCLUDE) - manifest { - attributes 'Main-Class': project.mainClassName - } - dependsOn configurations.runtimeClasspath - from { - configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } - } - with jar +tasks.register("distJar", Jar) { + dependsOn = ["classes"] + group = "dist" + doLast() { copy { from "${jarLibDir}/${jarLibName}.jar" @@ -81,18 +97,32 @@ task distJar(dependsOn: classes, type: Jar, group: 'dist') { rename "${jarLibName}.jar", "${distName}.jar" } } + + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + manifest { + attributes 'Main-Class': mainClassName + } + dependsOn configurations.runtimeClasspath + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } + with jar } // Creates a customized Java Runtime Environment for the app. -task jlink(dependsOn: distJar, type: Exec, group: 'dist') { - doFirst() { delete jlinkRuntimeDir } +tasks.register("jlink", Exec) { + dependsOn = ["distJar"] + group = "dist" + + doFirst() { delete jlinkDir } + workingDir project.projectDir - inputs.property("runtime", jlinkRuntimeImg) + inputs.property("runtime", "${jlinkDir}/runtime") commandLine = [ "${javaHome}/bin/jlink", '--module-path', "${javaHome}/jmods", '--add-modules', jlinkModuleList, - '--output', jlinkRuntimeImg, + '--output', "${jlinkDir}/runtime", '--strip-debug', '--no-header-files', '--no-man-pages', @@ -100,11 +130,14 @@ task jlink(dependsOn: distJar, type: Exec, group: 'dist') { '--compress=1', '--include-locales', jlinkLocalesList ] as List - outputs.dir(jlinkRuntimeDir) + outputs.dir(jlinkDir) } // Packs the app into an EXE. -task jpackage(dependsOn: jlink, type: Exec, group: 'dist') { +tasks.register("jpackage", Exec) { + dependsOn = ["jlink"] + group = "dist" + doFirst() { fileTree(jarLibDir).size() if (fileTree(jarLibDir).size() > 1) @@ -113,49 +146,58 @@ task jpackage(dependsOn: jlink, type: Exec, group: 'dist') { } doLast() { copy { - from "${project.rootDir}/LICENSE" + from "${rootDir}/LICENSE" into jpackageDir } - delete jlinkRuntimeDir } + workingDir project.projectDir def commands = [ "${javaHome}/bin/jpackage", '--input', jarLibDir, '--dest', jpackageDir, '--type', 'app-image', - '--name', project.appName, - '--vendor', project.appAuthor, + '--name', appName, + '--vendor', appAuthor, '--app-version', project.version, - '--main-class', project.mainClassName, + '--main-class', mainClassName, '--main-jar', jar.archiveFile.get().asFile.getName(), - '--runtime-image', jlinkRuntimeImg + '--runtime-image', "${jlinkDir}/runtime", + '--copyright', copyrightText ] if (osName.contains('windows')) { commands << '--icon' - commands << "${project.assetsDir}/icons/icon.ico" + commands << "${assetsDir}/icons/icon.ico" } else if (osName.contains('linux')) { commands << '--icon' - commands << "${project.assetsDir}/icons/icon.png" + commands << "${assetsDir}/icons/icon.png" } else if (osName.contains('mac')) { - commands << '--java-options' - commands << "-XstartOnFirstThread" + commands << '--icon' + commands << "${project.assetsDir}/icons/icon.icns" + commands << '--mac-app-category' + commands << 'entertainment' } - commandLine = commands + commandLine = commands as List } // Generates a distributable ZIP file for the app. -task distZip(dependsOn: jpackage, type: Zip, group: 'dist') { +tasks.register("distZip", Zip) { + dependsOn = ["jpackage"] + group = "dist" + from(jpackageDir) { include("**") } - from(project.rootDir) { include("README.md") } + from(rootDir) { include("README.md") } archiveFileName = "${distName}.zip" destinationDirectory = file(distDir) } // Generates a distributable EXE file for the app, using Inno Setup. // Note that you must install Inno Setup in your environment and add it to PATH before running this task. -task distExe(dependsOn: jpackage, type: Exec, group: 'dist') { - workingDir project.rootDir +tasks.register("distExe", Exec) { + dependsOn = ["jpackage"] + group = "dist" + + workingDir rootDir def commands = [ "iscc", "/Q", @@ -164,16 +206,35 @@ task distExe(dependsOn: jpackage, type: Exec, group: 'dist') { commandLine = commands } +// Generates a distributable DMG file for the app, using create-dmg. +// Note that you must install create-dmg in your environment and add it to PATH before running this task. +task distDmg(dependsOn: jpackage, type: Exec, group: 'dist') { + workingDir project.rootDir + def commands = [macPackage, project.version] + commandLine = commands +} + +// Generates a distributable AppImage file for the app, using linuxdeploy and appimagetool. +// Note that you must install linuxdeploy and appimagetool in your environment and add it to PATH before running this task. +task distAppImage(dependsOn: jpackage, type: Exec, group: 'dist') { + workingDir project.rootDir + def commands = [appImagePackage, project.version] + commandLine = commands +} + // Generates ALL kinds of distributing files. -task distAll(dependsOn: [distJar, distZip, distExe], group: 'dist') { +tasks.register("distAll") { + dependsOn = ["distJar", "distZip", getDistTask()] + group = "dist" + doLast() { - println("All files were successfully generated, see: ${new File(distDir as String).absolutePath}") + logger.lifecycle("All files were successfully generated, see: ${new File(distDir as String).absolutePath}") try { - delete "${buildDir}/tmp" - delete jpackageDir delete jarLibDir + delete jlinkDir + delete jpackageDir } catch (Exception ignored) { - println("Unable to delete temp files.") + logger.lifecycle("Unable to delete temp files.") } } } diff --git a/desktop/src/cn/harryh/arkpets/ArkHomeFX.java b/desktop/src/cn/harryh/arkpets/ArkHomeFX.java index 76c42fec..0d978e32 100644 --- a/desktop/src/cn/harryh/arkpets/ArkHomeFX.java +++ b/desktop/src/cn/harryh/arkpets/ArkHomeFX.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets; @@ -15,6 +15,7 @@ import cn.harryh.arkpets.utils.FXMLHelper; import cn.harryh.arkpets.utils.FXMLHelper.LoadFXMLResult; import cn.harryh.arkpets.utils.GuiComponents.Toast; +import cn.harryh.arkpets.utils.GuiPrefabs; import cn.harryh.arkpets.utils.Logger; import javafx.application.Application; import javafx.application.Platform; @@ -137,6 +138,8 @@ public void start(Stage stage) throws Exception { // Post initialization. rootModule.configNetwork(); rootModule.moduleWrapperComposer.activate(0); + if (isMac) + GuiPrefabs.disableNSWindowRestore(GuiPrefabs.getStageNativeHandle(stage)); Logger.info("Launcher", "Finished starting"); }, Duration.ZERO, durationFast); @@ -159,26 +162,28 @@ public void popLoading(EventHandler handler) { public void popBrowser(URI uri) { Logger.info("Launcher", "Request to open URI: " + uri); - try { - if ("file".equalsIgnoreCase(uri.getScheme())) { - // File URI - File localFile = new File(uri); - if (!localFile.isDirectory()) - throw new IOException("Given file URI should be a directory"); - SwingUtilities.invokeLater(() -> { - try { - Desktop.getDesktop().open(localFile); - } catch (IOException e) { - Logger.error("Launcher", "Failed to open the file URI, details see below.", e); - } - }); - } else { - // Other types of URI (like HTTP/HTTPS) - Desktop.getDesktop().browse(uri); + new Thread(() -> { + try { + if ("file".equalsIgnoreCase(uri.getScheme())) { + // File URI + File localFile = new File(uri); + if (!localFile.isDirectory()) + throw new IOException("Given file URI should be a directory"); + SwingUtilities.invokeLater(() -> { + try { + Desktop.getDesktop().open(localFile); + } catch (IOException e) { + Logger.error("Launcher", "Failed to open the file URI, details see below.", e); + } + }); + } else { + // Other types of URI (like HTTP/HTTPS) + Desktop.getDesktop().browse(uri); + } + } catch (IOException e) { + Logger.error("Launcher", "Failed to open the URI, details see below.", e); } - } catch (IOException e) { - Logger.error("Launcher", "Failed to open the URI, details see below.", e); - } + }).start(); } public void popBrowser(String uri) { diff --git a/desktop/src/cn/harryh/arkpets/BootstrapLauncher.java b/desktop/src/cn/harryh/arkpets/BootstrapLauncher.java index 73999898..3aa6aa1f 100644 --- a/desktop/src/cn/harryh/arkpets/BootstrapLauncher.java +++ b/desktop/src/cn/harryh/arkpets/BootstrapLauncher.java @@ -1,10 +1,9 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets; import cn.harryh.arkpets.controllers.Titlebar; -import cn.harryh.arkpets.guitasks.envchecker.WinGraphicsEnvCheckTask; import cn.harryh.arkpets.platform.WindowSystem; import cn.harryh.arkpets.utils.ArgPending; import cn.harryh.arkpets.utils.Logger; @@ -19,6 +18,7 @@ import javax.swing.*; import java.awt.*; +import java.io.File; import java.nio.charset.Charset; import java.util.Objects; @@ -28,11 +28,19 @@ public class BootstrapLauncher { private static boolean useCustomGLFW; private static boolean isDirectStart = false; + private static File customConfig; public static void main(String[] args) { // Disable assistive technologies System.setProperty("javax.accessibility.assistive_technologies", ""); ArgPending.argCache = args; + // Config + new ArgPending("--config", args) { + protected void process(String command, String addition) { + customConfig = new File(addition); + } + }; + ArkConfig appConfig = Objects.requireNonNull(customConfig == null ? ArkConfig.getConfig() : ArkConfig.getConfig(customConfig)); // Logger new ArgPending("--direct-start", args) { protected void process(String command, String addition) { @@ -44,7 +52,6 @@ protected void process(String command, String addition) { } else { Logger.initialize(LogConfig.logDesktopPath, LogConfig.logDesktopMaxKeep); } - ArkConfig appConfig = Objects.requireNonNull(ArkConfig.getConfig(), "ArkConfig returns a null instance, please check the config file."); try { Logger.setLevel(appConfig.logging_level); } catch (Exception ignored) { @@ -96,13 +103,6 @@ protected void process(String command, String addition) { Titlebar.forceUiStyle = addition.toLowerCase(); } }; - // Remove NVIDIA settings when uninstall on windows. - new ArgPending("--remove-nvidia", ArgPending.argCache) { - @Override - protected void process(String command, String addition) { - new WinGraphicsEnvCheckTask().removeNvidiaSettings(); - } - }; // Disable libdecor to avoid glfw and javafx problem on linux. if(isLinux) GLFW.glfwInitHint(GLFW.GLFW_WAYLAND_LIBDECOR, GLFW.GLFW_WAYLAND_DISABLE_LIBDECOR); // Java FX bootstrap @@ -151,6 +151,7 @@ protected void process(String command, String addition) { // Configure window layout config.setDecorated(WindowSystem.needDecorated()); config.setResizable(WindowSystem.needResize()); + config.setWindowSizeLimits(1,1,65535,65535); config.setWindowedMode(coreWidthDefault, coreHeightDefault); config.setWindowPosition(0, 0); // Configure window title @@ -179,7 +180,7 @@ public void invoke(int error, long description) { } }); // Instantiate the App - Lwjgl3Application app = new Lwjgl3Application(new ArkPets(TITLE), config); + Lwjgl3Application app = new Lwjgl3Application(new ArkPets(TITLE, appConfig), config); } catch (Exception e) { WindowSystem.free(); Logger.error("System", "A fatal error occurs in the runtime of Lwjgl3Application, details see below.", e); diff --git a/desktop/src/cn/harryh/arkpets/controllers/AnnounceDialog.java b/desktop/src/cn/harryh/arkpets/controllers/AnnounceDialog.java index 872501eb..aba871e3 100644 --- a/desktop/src/cn/harryh/arkpets/controllers/AnnounceDialog.java +++ b/desktop/src/cn/harryh/arkpets/controllers/AnnounceDialog.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.controllers; @@ -9,16 +9,18 @@ import cn.harryh.arkpets.guitasks.requests.FetchAnnounceTask; import cn.harryh.arkpets.utils.GuiPrefabs; import cn.harryh.arkpets.utils.Logger; +import cn.harryh.arkpets.utils.ScrollUtils; import cn.harryh.arkpets.utils.StringUtils; import cn.harryh.arkpets.utils.markdown.FxmlConvertor; import cn.harryh.arkpets.utils.markdown.FxmlDocumentController; -import com.alibaba.fastjson.JSONObject; -import com.jfoenix.controls.*; +import com.alibaba.fastjson2.JSONObject; +import javafx.beans.binding.StringBinding; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.fxml.FXML; -import javafx.scene.Group; import javafx.scene.control.*; import javafx.scene.effect.DropShadow; import javafx.scene.layout.AnchorPane; @@ -40,12 +42,12 @@ public final class AnnounceDialog implements DialogController { @FXML private AnchorPane dialog; @FXML - private JFXButton dialogReturn; + private Button dialogReturn; @FXML - private JFXListView> annoListView; + private ListView annoListView; @FXML - private JFXButton annoRefetch; + private Button annoRefetch; @FXML private Label annoTitle; @FXML @@ -59,25 +61,43 @@ public final class AnnounceDialog implements DialogController { @FXML private VBox annoContainer; - private JFXListCell selectedAnnoCell; - private AnnounceReadHandler announceReadHandler; private ArkHomeFX app; + private AnnounceItemWrapper selectedAnnounce; + + private final ObservableList annoItemList = FXCollections.observableArrayList(); + @Override public void initializeWith(ArkHomeFX app) { this.app = app; + ScrollUtils.addSmoothScrolling(annoScroll); + this.selectedAnnounce = new AnnounceItemWrapper(); + annoListView.setItems(annoItemList); annoListView.getSelectionModel().getSelectedItems().addListener( - (ListChangeListener>) (observable -> observable.getList().forEach( - (Consumer>) this::selectCell) + (ListChangeListener) (observable -> observable.getList().forEach( + (Consumer) this::selectCell) ) ); + annoListView.setCellFactory(this::createCell); + ScrollUtils.addSmoothScrolling(annoListView); annoRefetch.setOnAction(e -> this.fetchAnnounce(true, () -> {})); announceReadHandler = new AnnounceReadHandler(app.config); + + annoTitle.textProperty().bind(selectedAnnounce.titleProperty); + annoDate.textProperty().bind(selectedAnnounce.dateProperty); + annoDate.visibleProperty().bind(selectedAnnounce.dateProperty.isNotEmpty()); + annoDate.managedProperty().bind(selectedAnnounce.dateProperty.isNotEmpty()); + annoGroup.textProperty().bind(selectedAnnounce.groupProperty); + annoGroup.managedProperty().bind(selectedAnnounce.groupProperty.isNotEmpty()); + annoGroup.visibleProperty().bind(selectedAnnounce.groupProperty.isNotEmpty()); + annoGotoOrigin.visibleProperty().bind(selectedAnnounce.sourceProperty.isNotEmpty()); + annoGotoOrigin.managedProperty().bind(selectedAnnounce.sourceProperty.isNotEmpty()); + annoGotoOrigin.setOnMouseClicked(e -> app.popBrowser(selectedAnnounce.sourceProperty.get())); } @Override @@ -86,12 +106,11 @@ public AnchorPane getDialogPane() { } @Override - public JFXButton getReturnButton() { + public Button getReturnButton() { return dialogReturn; } public void fetchAnnounce(boolean doPopNotice, Runnable onNeedImmediateShow) { - ObservableList annoItemList = FXCollections.observableArrayList(); new FetchAnnounceTask(app.body, doPopNotice ? GuiTaskStyle.COMMON : GuiTaskStyle.HIDDEN, annoItemList).start(); annoListView.getItems().clear(); @@ -100,7 +119,6 @@ public void fetchAnnounce(boolean doPopNotice, Runnable onNeedImmediateShow) { change.next(); if (change.wasAdded() && change.getAddedSize() > 0) { Logger.info("Announce", "Fetched " + change.getAddedSize() + " announcements"); - change.getAddedSubList().forEach(anno -> annoListView.getItems().add(createCell(anno))); announceReadHandler.retainAll(change.getAddedSubList()); } // Handle callback @@ -118,69 +136,26 @@ public void fetchAnnounce(boolean doPopNotice, Runnable onNeedImmediateShow) { }); } - private JFXListCell createCell(AnnounceItem anno) { - double width = annoListView.getPrefWidth() * 0.75; - double offset = width * 0.175; - JFXListCell cell = new JFXListCell<>(); - cell.getStyleClass().addAll("list-item"); - cell.setPrefWidth(width); - cell.setItem(anno); - cell.setId(anno.getAnnoId()); - SVGPath dot = GuiPrefabs.Icons.getIcon(GuiPrefabs.Icons.SVG_DIAMOND, anno.getParsedGroup().color); - dot.setLayoutX(-offset); - dot.setScaleX(0.6); - dot.setScaleY(0.6); - dot.setEffect(new DropShadow(null, GuiPrefabs.COLOR_WHITE, 4.0, 0.5, 0.0, 0.0)); - Label name = new Label(anno.title); - name.getStyleClass().addAll("list-item-label"); - cell.setGraphic(new Group(dot, name)); - refreshCellGraphic(cell); - return cell; + private ListCell createCell(ListView announceItemListView) { + return new AnnounceListCell(announceItemListView.getPrefWidth()); } - private void refreshCellGraphic(JFXListCell cell) { - double width = annoListView.getPrefWidth() * 0.75; - double offset = width * 0.175; - SVGPath dot = (SVGPath) ((Group) (cell.getGraphic())).getChildrenUnmodifiable().get(0); - dot.setVisible(!announceReadHandler.isRead(cell.getItem())); - Label name = (Label) ((Group) (cell.getGraphic())).getChildrenUnmodifiable().get(1); - name.setPrefWidth(width - (announceReadHandler.isRead(cell.getItem()) ? 0 : offset)); - } - - private void selectCell(JFXListCell cell) { - // Reset - if (selectedAnnoCell != null) { - selectedAnnoCell.getStyleClass().setAll("list-item"); - refreshCellGraphic(selectedAnnoCell); - } - selectedAnnoCell = cell; - selectedAnnoCell.getStyleClass().add("list-item-active"); + private void selectCell(AnnounceItem cell) { // Display info - AnnounceItem anno = cell.getItem(); - annoTitle.setText(anno.title); - GuiPrefabs.replaceTextAutoVisibility(annoGroup, switch (anno.getParsedGroup()) { - case DEFAULT -> null; - case INFO -> "普通公告"; - case WARN -> "重要公告"; - case DANGER -> "紧急公告"; - }); - GuiPrefabs.replaceTextAutoVisibility(annoDate, - anno.date != null && !anno.date.isEmpty() ? StringUtils.getSimpleTimeString(anno.getParsedDate()) : ""); - GuiPrefabs.replaceTextAutoVisibility(annoGotoOrigin, anno.source != null ? "查看原文" : null); - annoGotoOrigin.setOnMouseClicked(e -> app.popBrowser(anno.source)); + selectedAnnounce.setAnnounceItem(cell); // Display announcement GuiPrefabs.fadeOutNode(annoContainer, durationFast, e -> { GuiPrefabs.disableScrollPaneCache(annoScroll); annoScroll.setVvalue(0.0); annoContainer.getChildren().clear(); - FxmlDocumentController document = FxmlConvertor.toFxmlController(anno.markdown); + FxmlDocumentController document = FxmlConvertor.toFxmlController(cell.markdown); document.getBodyNode().setMaxWidth(annoScroll.getWidth()); document.setHyperlinkConsumer(string -> app.popBrowser(string)); annoContainer.getChildren().add(document.getBodyNode()); GuiPrefabs.fadeInNode(annoContainer, durationFast, null); }); // Mark as read - announceReadHandler.setRead(anno); + announceReadHandler.setRead(cell); } @@ -240,4 +215,83 @@ public void retainAll(List annoList) { } } } + + + private class AnnounceListCell extends GuiPrefabs.RipperListCell { + private final double width; + private final double offset; + private final SVGPath dot; + private final Label name; + private final static DropShadow dotShadow = new DropShadow(null, GuiPrefabs.COLOR_WHITE, 4.0, 0.5, 0.0, 0.0); + + public AnnounceListCell(double listWidth) { + this.width = listWidth * 0.925; + this.offset = listWidth * 0.175; + + dot = GuiPrefabs.Icons.getIcon(GuiPrefabs.Icons.SVG_DIAMOND, GuiPrefabs.COLOR_BLACK); + dot.setLayoutX(-offset); + dot.setScaleX(0.6); + dot.setScaleY(0.6); + dot.setEffect(dotShadow); + name = new Label(); + name.getStyleClass().addAll("list-item-label"); + + getContent().setAll(dot, name); + setPrefWidth(width); + } + + @Override + protected void updateItem(AnnounceItem anno, boolean empty) { + super.updateItem(anno, empty); + if (empty || anno == null) { + setContentVisible(false); + } else { + setId(anno.getAnnoId()); + name.setText(anno.title); + name.setPrefWidth(width - (announceReadHandler.isRead(anno) ? 0 : offset)); + name.setPrefHeight(35); + dot.setFill(anno.getParsedGroup().color); + dot.setVisible(!announceReadHandler.isRead(anno)); + setContentVisible(true); + } + } + } + + + private static class AnnounceItemWrapper { + private final StringProperty titleProperty = new SimpleStringProperty("暂未选择任何公告"); + private final StringProperty sourceProperty = new SimpleStringProperty(); + private AnnounceItem announceItem; + + private final StringBinding dateProperty = new StringBinding() { + @Override + protected String computeValue() { + if (announceItem == null) + return "日期"; + if (announceItem.date == null || announceItem.date.isEmpty()) + return ""; + return StringUtils.getSimpleTimeString(announceItem.getParsedDate()); + } + }; + private final StringBinding groupProperty = new StringBinding() { + @Override + protected String computeValue() { + if (announceItem == null) return "等级"; + return switch (announceItem.getParsedGroup()) { + case DEFAULT -> null; + case INFO -> "普通公告"; + case WARN -> "重要公告"; + case DANGER -> "紧急公告"; + }; + } + }; + + public void setAnnounceItem(AnnounceItem announceItem) { + this.announceItem = announceItem; + titleProperty.set(announceItem.title); + sourceProperty.set(announceItem.source); + dateProperty.invalidate(); + groupProperty.invalidate(); + } + } } diff --git a/desktop/src/cn/harryh/arkpets/controllers/BehaviorModule.java b/desktop/src/cn/harryh/arkpets/controllers/BehaviorModule.java index 6eee5cda..3f29bdfb 100644 --- a/desktop/src/cn/harryh/arkpets/controllers/BehaviorModule.java +++ b/desktop/src/cn/harryh/arkpets/controllers/BehaviorModule.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2024, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.controllers; @@ -11,7 +11,7 @@ import cn.harryh.arkpets.utils.GuiPrefabs; import cn.harryh.arkpets.utils.Logger; import cn.harryh.arkpets.utils.Monitor; -import com.jfoenix.controls.*; +import cn.harryh.arkpets.utils.ScrollUtils; import javafx.application.Platform; import javafx.concurrent.ScheduledService; import javafx.concurrent.Task; @@ -31,35 +31,37 @@ public final class BehaviorModule implements Controller { private ScrollPane moduleScroll; @FXML - private JFXCheckBox configBehaviorAllowWalk; + private CheckBox configBehaviorAllowWalk; @FXML - private JFXCheckBox configBehaviorAllowSit; + private CheckBox configBehaviorAllowSit; @FXML - private JFXCheckBox configBehaviorAllowSleep; + private CheckBox configBehaviorAllowSleep; @FXML - private JFXCheckBox configBehaviorAllowSpecial; + private CheckBox configBehaviorAllowSpecial; @FXML - private JFXSlider configBehaviorAiActivation; + private Slider configBehaviorAiActivation; @FXML private Label configBehaviorAiActivationValue; @FXML - private JFXSlider configBehaviorWalkSpeed; + private Slider configBehaviorWalkSpeed; @FXML private Label configBehaviorSpeedWalkValue; @FXML - private JFXCheckBox configBehaviorAllowInteract; + private CheckBox configBehaviorAllowInteract; @FXML - private JFXCheckBox configBehaviorDoPeerRepulsion; + private CheckBox configBehaviorDoPeerRepulsion; @FXML - private JFXCheckBox configDeployMultiMonitors; + private ComboBox> configDirectionSwitching; + @FXML + private CheckBox configDeployMultiMonitors; @FXML private Label configDeployMultiMonitorsStatus; @FXML - private JFXSlider configDeployMarginBottom; + private Slider configDeployMarginBottom; @FXML private Label configDeployMarginBottomValue; @FXML - private JFXButton toggleConfigDeployPosition; + private Button toggleConfigDeployPosition; @FXML private HBox wrapperConfigDeployPosition; @FXML @@ -68,40 +70,40 @@ public final class BehaviorModule implements Controller { @FXML private Label configTransitionAnimationLabel; @FXML - private JFXComboBox> configTransitionAnimation; + private ComboBox> configTransitionAnimation; @FXML - private JFXButton configTransitionAnimationHelp; + private Button configTransitionAnimationHelp; @FXML private Label configTransitionDurationLabel; @FXML - private JFXComboBox> configTransitionDuration; + private ComboBox> configTransitionDuration; @FXML - private JFXButton configTransitionDurationHelp; + private Button configTransitionDurationHelp; @FXML private Label configTransitionFunctionLabel; @FXML - private JFXComboBox> configTransitionFunction; + private ComboBox> configTransitionFunction; @FXML - private JFXButton configTransitionFunctionHelp; + private Button configTransitionFunctionHelp; @FXML - private JFXSlider configPhysicGravity; + private Slider configPhysicGravity; @FXML private Label configPhysicGravityValue; @FXML - private JFXSlider configPhysicAirFriction; + private Slider configPhysicAirFriction; @FXML private Label configPhysicAirFrictionValue; @FXML - private JFXSlider configPhysicStaticFriction; + private Slider configPhysicStaticFriction; @FXML private Label configPhysicStaticFrictionValue; @FXML - private JFXSlider configPhysicSpeedLimitX; + private Slider configPhysicSpeedLimitX; @FXML private Label configPhysicSpeedLimitXValue; @FXML - private JFXSlider configPhysicSpeedLimitY; + private Slider configPhysicSpeedLimitY; @FXML private Label configPhysicSpeedLimitYValue; @FXML @@ -114,7 +116,7 @@ public void initializeWith(ArkHomeFX app) { this.app = app; initConfigBehavior(); initScheduledListener(); - + ScrollUtils.addSmoothScrolling(moduleScroll); Platform.runLater(() -> GuiPrefabs.disableScrollPaneCache(moduleScroll)); } @@ -143,7 +145,7 @@ private void initConfigBehavior() { SliderSetup setupBehaviorAiActivation = new SimpleIntegerSliderSetup(configBehaviorAiActivation); setupBehaviorAiActivation .setDisplay(configBehaviorAiActivationValue, "%d 级", "活跃级别 (activation level)") - .setRange(0, 8) + .setRange(0, 16) .setTicks(1, 0) .setSliderValue(app.config.behavior_ai_activation) .setOnChanged((observable, oldValue, newValue) -> { @@ -173,6 +175,16 @@ private void initConfigBehavior() { app.config.save(); }); + new ComboBoxSetup<>(configDirectionSwitching).setItems(new NamedItem<>("禁用", 0), + new NamedItem<>("松开拖拽时", 1), + new NamedItem<>("拖拽时", 2), + new NamedItem<>("光标掠过时", 3)) + .selectValue(app.config.behavior_direction_switching, app.config.behavior_direction_switching + "(自定义)") + .setOnNonNullValueUpdated((observable, oldValue, newValue) -> { + app.config.behavior_direction_switching = newValue.value(); + app.config.save(); + }); + configDeployMultiMonitors.setSelected(app.config.display_multi_monitors); configDeployMultiMonitors.setOnAction(e -> { app.config.display_multi_monitors = configDeployMultiMonitors.isSelected(); diff --git a/desktop/src/cn/harryh/arkpets/controllers/Controller.java b/desktop/src/cn/harryh/arkpets/controllers/Controller.java index 9d1caba8..4245a344 100644 --- a/desktop/src/cn/harryh/arkpets/controllers/Controller.java +++ b/desktop/src/cn/harryh/arkpets/controllers/Controller.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.controllers; diff --git a/desktop/src/cn/harryh/arkpets/controllers/DialogController.java b/desktop/src/cn/harryh/arkpets/controllers/DialogController.java index 6ca019a8..66c70391 100644 --- a/desktop/src/cn/harryh/arkpets/controllers/DialogController.java +++ b/desktop/src/cn/harryh/arkpets/controllers/DialogController.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.controllers; diff --git a/desktop/src/cn/harryh/arkpets/controllers/DownloadDialog.java b/desktop/src/cn/harryh/arkpets/controllers/DownloadDialog.java index d960c852..cbc5aab4 100644 --- a/desktop/src/cn/harryh/arkpets/controllers/DownloadDialog.java +++ b/desktop/src/cn/harryh/arkpets/controllers/DownloadDialog.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.controllers; @@ -11,8 +11,7 @@ import cn.harryh.arkpets.utils.GuiPrefabs; import cn.harryh.arkpets.utils.Logger; import cn.harryh.arkpets.utils.StringUtils; -import com.alibaba.fastjson.JSONObject; -import com.jfoenix.controls.*; +import com.alibaba.fastjson2.JSONObject; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.event.ActionEvent; @@ -29,20 +28,20 @@ public final class DownloadDialog implements DialogController { @FXML private AnchorPane dialog; @FXML - private JFXButton dialogReturn; + private Button dialogReturn; @FXML private Label mcIndicator; @FXML private Label mcPurchase; @FXML - private JFXTextField mcCdkInput; + private TextField mcCdkInput; @FXML - private JFXButton mcConfirm; + private Button mcConfirm; @FXML private Label psIndicator; @FXML - private JFXButton psConfirm; + private Button psConfirm; private ArkHomeFX app; @@ -68,7 +67,7 @@ public AnchorPane getDialogPane() { } @Override - public JFXButton getReturnButton() { + public Button getReturnButton() { return dialogReturn; } diff --git a/desktop/src/cn/harryh/arkpets/controllers/LogDialog.java b/desktop/src/cn/harryh/arkpets/controllers/LogDialog.java index 7258cb66..99c65675 100644 --- a/desktop/src/cn/harryh/arkpets/controllers/LogDialog.java +++ b/desktop/src/cn/harryh/arkpets/controllers/LogDialog.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.controllers; @@ -6,15 +6,14 @@ import cn.harryh.arkpets.ArkHomeFX; import cn.harryh.arkpets.guitasks.GuiTask; import cn.harryh.arkpets.guitasks.ZipTask; +import cn.harryh.arkpets.utils.GuiComponents; import cn.harryh.arkpets.utils.GuiPrefabs; import cn.harryh.arkpets.utils.IOUtils.FileUtil; import cn.harryh.arkpets.utils.Logger; import cn.harryh.arkpets.utils.StringUtils; -import com.jfoenix.controls.*; -import com.jfoenix.controls.datamodels.treetable.*; +import com.jfoenix.controls.RecursiveTreeItem; +import com.jfoenix.controls.datamodels.treetable.RecursiveTreeObject; import javafx.application.Platform; -import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; @@ -22,7 +21,6 @@ import javafx.scene.control.*; import javafx.scene.layout.AnchorPane; import javafx.stage.FileChooser; -import javafx.util.Callback; import java.io.File; import java.io.FilenameFilter; @@ -33,7 +31,6 @@ import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.*; -import java.util.function.Function; import static cn.harryh.arkpets.Const.LogConfig.logDir; @@ -42,14 +39,14 @@ public final class LogDialog implements DialogController { @FXML private AnchorPane dialog; @FXML - private JFXButton dialogReturn; + private Button dialogReturn; @FXML private TreeTableView logView; @FXML - private JFXButton logRefetch; + private Button logRefetch; @FXML - private JFXButton logExplore; + private Button logExplore; @FXML private Label logName; @@ -64,11 +61,11 @@ public final class LogDialog implements DialogController { @FXML private Label logSelectedCount; @FXML - public JFXButton quickSelectAll; + public Button quickSelectAll; @FXML - public JFXButton quickSelectRecent; + public Button quickSelectRecent; @FXML - private JFXButton exportSelected; + private Button exportSelected; private ObservableList coreLogList; private ObservableList desktopLogList; @@ -96,7 +93,7 @@ public AnchorPane getDialogPane() { } @Override - public JFXButton getReturnButton() { + public Button getReturnButton() { return dialogReturn; } @@ -140,23 +137,21 @@ public void clearTable() { } private void prepareTable() { - TreeTableColumn nameCol = new JFXTreeTableColumn<>("文件名"); - nameCol.setCellValueFactory( - new SimpleCellValueFactory<>(logItem -> logItem.name) - ); - nameCol.setReorderable(false); - - TreeTableColumn sizeCol = new JFXTreeTableColumn<>("大小"); - sizeCol.setCellValueFactory( - new SimpleCellValueFactory<>(logItem -> new HumanSize(logItem.size, !logItem.isAvailable())) - ); - sizeCol.setReorderable(false); - - TreeTableColumn modifiedTimeCol = new JFXTreeTableColumn<>("更新时间"); - modifiedTimeCol.setCellValueFactory( - new SimpleCellValueFactory<>(logItem -> new HumanInstant(logItem.modifiedTime, !logItem.isAvailable())) - ); - modifiedTimeCol.setReorderable(false); + new GuiComponents.TreeTableColumnSetup() + .setText("文件名") + .setReorderable(false) + .setValueExtractor(logItem -> logItem.name) + .attachTo(logView); + new GuiComponents.TreeTableColumnSetup() + .setText("大小") + .setReorderable(false) + .setValueExtractor(logItem -> new HumanSize(logItem.size, !logItem.isAvailable())) + .attachTo(logView); + new GuiComponents.TreeTableColumnSetup() + .setText("更新时间") + .setReorderable(false) + .setValueExtractor(logItem -> new HumanInstant(logItem.modifiedTime, !logItem.isAvailable())) + .attachTo(logView); LogItem coreLogRoot = new LogItem("桌宠日志"); LogItem desktopLogRoot = new LogItem("启动器日志"); @@ -174,9 +169,6 @@ private void prepareTable() { logView.setRoot(root); logView.setShowRoot(false); - logView.getColumns().add(nameCol); - logView.getColumns().add(sizeCol); - logView.getColumns().add(modifiedTimeCol); logView.getSelectionModel().setCellSelectionEnabled(false); logView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); } @@ -364,18 +356,6 @@ public boolean isAvailable() { } - private record SimpleCellValueFactory(Function extractor) - implements Callback, ObservableValue> { - @Override - public ObservableValue call(TreeTableColumn.CellDataFeatures cellDataFeatures) { - S value = cellDataFeatures.getValue().getValue(); - if (value == null || extractor == null) - return null; - return new SimpleObjectProperty<>(extractor.apply(value)); - } - } - - private record HumanSize(long size, boolean hide) implements Comparable { @Override public String toString() { diff --git a/desktop/src/cn/harryh/arkpets/controllers/ModelsModule.java b/desktop/src/cn/harryh/arkpets/controllers/ModelsModule.java index 37886f17..d454077a 100644 --- a/desktop/src/cn/harryh/arkpets/controllers/ModelsModule.java +++ b/desktop/src/cn/harryh/arkpets/controllers/ModelsModule.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.controllers; @@ -14,23 +14,19 @@ import cn.harryh.arkpets.network.SourceStrategy; import cn.harryh.arkpets.network.api.McQueryVersion; import cn.harryh.arkpets.utils.GuiComponents.NoticeBar; -import cn.harryh.arkpets.utils.GuiPrefabs; -import cn.harryh.arkpets.utils.IOUtils; -import cn.harryh.arkpets.utils.Logger; -import cn.harryh.arkpets.utils.Version; -import com.alibaba.fastjson.JSONObject; -import com.jfoenix.controls.*; +import cn.harryh.arkpets.utils.*; +import com.alibaba.fastjson2.JSONObject; import javafx.application.Platform; -import javafx.collections.FXCollections; -import javafx.collections.ListChangeListener; -import javafx.collections.ObservableSet; -import javafx.collections.SetChangeListener; +import javafx.beans.binding.BooleanBinding; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.collections.*; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.fxml.FXML; -import javafx.scene.Group; import javafx.scene.Node; import javafx.scene.control.*; +import javafx.scene.effect.DropShadow; import javafx.scene.input.KeyCode; import javafx.scene.input.MouseEvent; import javafx.scene.layout.AnchorPane; @@ -44,6 +40,8 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; @@ -60,19 +58,19 @@ public final class ModelsModule implements Controller { @FXML private Pane loadFailureTip; @FXML - private JFXButton searchModelConfirm; + private Button searchModelConfirm; @FXML - private JFXButton searchModelReset; + private Button searchModelReset; @FXML - private JFXButton searchModelRandom; + private Button searchModelRandom; @FXML - private JFXButton searchModelReload; + private Button searchModelReload; @FXML - private JFXTextField searchModelInput; + private TextField searchModelInput; @FXML private Label searchModelStatus; @FXML - private JFXListView> modelListView; + private ListView modelListView; @FXML private Label selectedModelName; @FXML @@ -82,9 +80,13 @@ public final class ModelsModule implements Controller { @FXML private Label selectedModelType; @FXML - private JFXButton modelFavorite; + private Button modelWiki; @FXML - private JFXButton topFavorite; + private Button modelFavorite; + @FXML + public SVGPath modelFavoriteIconFill; + @FXML + private Button topFavorite; @FXML private AnchorPane infoPane; @@ -93,9 +95,9 @@ public final class ModelsModule implements Controller { @FXML private AnchorPane managePane; @FXML - private JFXButton toggleFilterPane; + private Button toggleFilterPane; @FXML - private JFXButton toggleManagePane; + private Button toggleManagePane; @FXML private ScrollPane infoPaneTagScroll; @FXML @@ -110,39 +112,40 @@ public final class ModelsModule implements Controller { @FXML private VBox noticeBox; @FXML - private JFXButton modelUpdate; + private Button modelUpdate; @FXML - private JFXButton modelFetch; + private Button modelFetch; @FXML - private JFXButton modelVerify; + private Button modelVerify; @FXML - private JFXButton modelReFetch; + private Button modelReFetch; @FXML - private JFXButton modelImport; + private Button modelImport; @FXML - private JFXButton modelExport; + private Button modelExport; @FXML private Label modelHelp; private ModelItemGroup assetItemList; - private JFXListCell selectedModelCell; - private ArrayList> modelCellList = new ArrayList<>(); + private ModelItemWrapper selectedModel; + private final ObservableList targetList = FXCollections.observableArrayList(); private ObservableSet filterTagSet = FXCollections.observableSet(); + private boolean filterFavorite; private GuiPrefabs.PeerNodeComposer infoPaneComposer; private GuiPrefabs.PeerNodeComposer mngBtnComposer; private NoticeBar datasetTooLowVerNotice; private NoticeBar datasetTooHighVerNotice; - private final SVGPath favIcon = GuiPrefabs.Icons.getIcon(GuiPrefabs.Icons.SVG_STAR, GuiPrefabs.COLOR_LIGHT_GRAY); - private final SVGPath favFillIcon = GuiPrefabs.Icons.getIcon(GuiPrefabs.Icons.SVG_STAR_FILLED, GuiPrefabs.COLOR_WARNING); - private boolean filterFavorite; - private ArkHomeFX app; @Override public void initializeWith(ArkHomeFX app) { this.app = app; + this.selectedModel = new ModelItemWrapper(); + this.modelListView.setItems(targetList); + ScrollUtils.addSmoothScrolling(modelListView); + infoPaneComposer = new GuiPrefabs.PeerNodeComposer(); infoPaneComposer.add(0, infoPane); infoPaneComposer.add(1, @@ -161,9 +164,7 @@ public void initializeWith(ArkHomeFX app) { initModelSearch(); initModelFilter(); initModelManage(); - initModelFavorite(); modelReload(false); - Platform.runLater(() -> { GuiPrefabs.disableScrollPaneCache(infoPaneTagScroll); GuiPrefabs.disableScrollPaneCache(filterPaneTagScroll); @@ -212,13 +213,12 @@ public boolean initModelsDataset(boolean doPopNotice) { } catch (FileNotFoundException e) { Logger.warn("ModelManager", "Failed to initialize model dataset due to file not found. (" + e.getMessage() + ")"); if (doPopNotice) { - JFXDialog dialog = GuiPrefabs.Dialogs.createCommonDialog(app.body, + GuiPrefabs.Dialogs.createCommonDialog(app.body, GuiPrefabs.Icons.getIcon(GuiPrefabs.Icons.SVG_WARNING_ALT, GuiPrefabs.COLOR_WARNING), "模型载入失败", "模型未成功载入:未找到数据集。", "模型数据集文件 " + PathConfig.fileModelsDataPath + " 可能不在工作目录下。\n请先前往 [选项] 进行模型下载。", - null); - dialog.show(); + null).show(); } } catch (ModelsDataset.DatasetKeyException e) { Logger.warn("ModelManager", "Failed to initialize model dataset due to dataset parsing error. (" + e.getMessage() + ")"); @@ -245,8 +245,35 @@ public boolean initModelsDataset(boolean doPopNotice) { } private void initInfoPane() { + // Model info labels toggleFilterPane.setOnAction(e -> infoPaneComposer.toggle(1, 0)); toggleManagePane.setOnAction(e -> infoPaneComposer.toggle(2, 0)); + selectedModelName.textProperty().bind(selectedModel.nameProperty); + selectedModelAppellation.textProperty().bind(selectedModel.appellationProperty); + selectedModelType.textProperty().bind(selectedModel.typeProperty); + selectedModelSkinGroupName.textProperty().bind(selectedModel.skinGroupNameProperty); + GuiPrefabs.addTooltip(selectedModelName, selectedModel.nameProperty); + GuiPrefabs.addTooltip(selectedModelAppellation, selectedModel.appellationProperty); + GuiPrefabs.addTooltip(selectedModelType, selectedModel.typeProperty); + GuiPrefabs.addTooltip(selectedModelSkinGroupName, selectedModel.skinGroupNameProperty); + + // Model quick operations + modelWiki.setOnAction(e -> { + String name = selectedModel.nameProperty.get(); + if (name != null && !name.isEmpty()) { + app.popBrowser(urlWikiPrefix + URLEncoder.encode(name, StandardCharsets.UTF_8)); + } + }); + modelWiki.visibleProperty().bind(selectedModel.getHasWikiProperty()); + GuiPrefabs.addTooltip(modelWiki, "Wiki"); + + modelFavorite.setOnAction(e -> { + selectedModel.setFavorite(!selectedModel.getFavoriteProperty().get()); + modelListView.refresh(); + app.config.save(); + }); + modelFavoriteIconFill.visibleProperty().bind(selectedModel.getFavoriteProperty()); + GuiPrefabs.addTooltip(modelFavorite, "收藏"); } private void initModelSearch() { @@ -280,11 +307,36 @@ private void initModelSearch() { } private void initModelFilter() { + ScrollUtils.addSmoothScrolling(filterPaneTagScroll); filterPaneTagClear.setOnMouseClicked(e -> app.popLoading(ev -> { filterTagSet.clear(); modelSearch(searchModelInput.getText()); infoPaneComposer.activate(0); })); + + if (app.config.character_favorites == null) { + app.config.character_favorites = new JSONObject(); + app.config.save(); + } + + topFavorite.setOnAction(e -> { + Logger.debug("ModelManager", "Toggle favorite display"); + modelListView.scrollTo(0); + if (filterFavorite) { + GuiPrefabs.replaceStyleClass(topFavorite, "btn-primary", "btn-secondary"); + } else { + GuiPrefabs.replaceStyleClass(topFavorite, "btn-secondary", "btn-primary"); + } + filterFavorite = !filterFavorite; + modelSearch(searchModelInput.getText()); + ModelItem recentSelected = assetItemList.searchByRelPath(app.config.character_asset); + if (recentSelected != null) + for (ModelItem cell : modelListView.getItems()) + if (recentSelected.equals(cell)) { + modelListView.scrollTo(cell); + modelListView.getSelectionModel().select(cell); + } + }); } private void initModelManage() { @@ -434,52 +486,9 @@ protected void onSucceeded(boolean result) { modelHelp.setOnMouseClicked(e -> app.popBrowser(urlHelp)); } - private void initModelFavorite() { - if (app.config.character_favorites == null) { - app.config.character_favorites = new JSONObject(); - app.config.save(); - } - - modelFavorite.setGraphic(favIcon); - modelFavorite.setRipplerFill(Color.GRAY); - modelFavorite.setOnAction(e -> { - String key = selectedModelCell.getItem().key; - if (app.config.character_favorites.containsKey(key)) { - app.config.character_favorites.remove(key); - selectedModelCell.getStyleClass().remove("Search-models-item-favorite"); - modelFavorite.setGraphic(favIcon); - Logger.debug("ModelManager", "Remove favorite model " + key); - } else { - app.config.character_favorites.put(key, new ModelItem.AssetPrefab()); - selectedModelCell.getStyleClass().add("Search-models-item-favorite"); - modelFavorite.setGraphic(favFillIcon); - Logger.debug("ModelManager", "Add favorite model " + key); - } - app.config.save(); - }); - - topFavorite.setOnAction(e -> { - Logger.debug("ModelManager", "Toggle favorite display"); - modelListView.scrollTo(0); - if (filterFavorite) { - GuiPrefabs.replaceStyleClass(topFavorite, "btn-primary", "btn-secondary"); - } else { - GuiPrefabs.replaceStyleClass(topFavorite, "btn-secondary", "btn-primary"); - } - filterFavorite = !filterFavorite; - modelSearch(searchModelInput.getText()); - ModelItem recentSelected = assetItemList.searchByRelPath(app.config.character_asset); - if (recentSelected != null) - for (JFXListCell cell : modelListView.getItems()) - if (recentSelected.equals(cell.getItem())) { - modelListView.scrollTo(cell); - modelListView.getSelectionModel().select(cell); - } - }); - } - public void modelSearch(String keyWords) { modelListView.getItems().clear(); + searchModelStatus.setText(""); if (assertModelLoaded(false)) { // Filter assets @@ -494,10 +503,8 @@ public void modelSearch(String keyWords) { long tEnd = System.nanoTime(); int curSize = searched.size(); searchModelStatus.setText((rawSize == curSize ? rawSize : curSize + " / " + rawSize) + " 个模型"); - // Add cells - for (JFXListCell cell : modelCellList) - if (searched.contains(cell.getItem())) - modelListView.getItems().add(cell); + targetList.clear(); + targetList.setAll(searched); Logger.info("ModelManager", "Search \"%s\" (%d results, %.1f ms)" .formatted(keyWords, curSize, (tEnd - tStart) / 1000000f)); } @@ -516,8 +523,7 @@ public void modelRandom() { public void modelReload(boolean doPopNotice) { app.popLoading(e -> { Logger.info("ModelManager", "Reloading"); - boolean willGc = modelCellList != null; - modelCellList = new ArrayList<>(); + boolean willGc = !targetList.isEmpty(); assetItemList = new ModelItemGroup(); if (initModelsDataset(doPopNotice)) { @@ -529,18 +535,18 @@ public void modelReload(boolean doPopNotice) { throw new IOException("Found no assets in the target directories."); // Initialize list view. modelListView.getSelectionModel().getSelectedItems().addListener( - (ListChangeListener>) (observable -> observable.getList().forEach( - (Consumer>) this::selectCell) + (ListChangeListener) (observable -> observable.getList().forEach( + (Consumer) this::selectCell) ) ); + modelListView.setCellFactory(listView -> new ModelListCell(listView.getPrefWidth() - 30, 30)); modelListView.setFixedCellSize(30); // Write models to menu items. - assetItemList.forEach(assetItem -> modelCellList.add(createCell(assetItem))); + selectedModel.setSortTags(app.modelsDataset.sortTags); Logger.debug("ModelManager", "Initialized model assets successfully."); } catch (IOException ex) { // Explicitly set all lists to empty. Logger.error("ModelManager", "Failed to initialize model assets due to unknown reasons, details see below.", ex); - modelCellList = new ArrayList<>(); assetItemList = new ModelItemGroup(); if (doPopNotice) GuiPrefabs.Dialogs.createCommonDialog(app.body, @@ -560,7 +566,7 @@ public void modelReload(boolean doPopNotice) { String s = change.getElementAdded() == null ? change.getElementRemoved() : change.getElementAdded(); String t = app.modelsDataset.sortTags == null ? s : app.modelsDataset.sortTags.getOrDefault(s, s); for (Node node : filterPaneTagFlow.getChildren()) - if (node instanceof JFXButton tag && t.equals(tag.getText())) { + if (node instanceof Button tag && t.equals(tag.getText())) { String styleFrom = change.getElementAdded() == null ? "info-tag-badge-active" : "info-tag-badge"; String styleTo = change.getElementAdded() == null ? "info-tag-badge" : "info-tag-badge-active"; GuiPrefabs.replaceStyleClass(tag, styleFrom, styleTo); @@ -572,15 +578,17 @@ public void modelReload(boolean doPopNotice) { sortTags.sort(Comparator.naturalOrder()); sortTags.forEach(s -> { String t = app.modelsDataset.sortTags == null ? s : app.modelsDataset.sortTags.getOrDefault(s, s); - JFXButton tag = new JFXButton(t); - tag.getStyleClass().add("info-tag-badge"); - tag.setOnAction(ev -> { - if (filterTagSet.contains(s)) - filterTagSet.remove(s); - else - filterTagSet.add(s); - modelSearch(searchModelInput.getText()); - }); + Button tag = new GuiPrefabs.ButtonBuilder() + .setText(t) + .setAdditionalStyleClass("info-tag-badge") + .setOnAction(ev -> { + if (filterTagSet.contains(s)) + filterTagSet.remove(s); + else + filterTagSet.add(s); + modelSearch(searchModelInput.getText()); + }) + .build(); filterPaneTagFlow.getChildren().add(tag); }); } @@ -589,107 +597,53 @@ public void modelReload(boolean doPopNotice) { // 3. Update model list: modelSearch(""); searchModelInput.clear(); - if (assetItemList != null && !modelCellList.isEmpty() && + if (assetItemList != null && !targetList.isEmpty() && app.config.character_asset != null && !app.config.character_asset.isEmpty()) { // Scroll to recent selected model and then select it. ModelItem recentSelected = assetItemList.searchByRelPath(app.config.character_asset); if (recentSelected != null) { - for (JFXListCell cell : modelListView.getItems()) - if (recentSelected.equals(cell.getItem())) { + for (ModelItem cell : modelListView.getItems()) + if (recentSelected.equals(cell)) { modelListView.scrollTo(cell); modelListView.getSelectionModel().select(cell); + selectedModel.setModelItem(cell); } - // Check model favorite: - if (app.config.character_favorites.containsKey(recentSelected.key)) { - modelFavorite.setGraphic(favFillIcon); - } } } } // Post process: - loadFailureTip.setVisible(modelCellList.isEmpty()); - app.rootModule.launchBtn.setDisable(modelCellList.isEmpty()); + loadFailureTip.setVisible(targetList.isEmpty()); + app.rootModule.launchBtn.setDisable(targetList.isEmpty()); if (willGc) System.gc(); Logger.info("ModelManager", "Reloaded"); }); } - private JFXListCell createCell(ModelItem model) { - double width = modelListView.getPrefWidth() - 50; - double height = 30; - double divide = 0.618; - JFXListCell item = new JFXListCell<>(); - item.getStyleClass().addAll("list-item"); - Label name = new Label(model.toString()); - name.getStyleClass().addAll("list-item-label"); - name.setPrefSize(model.skinGroupName == null ? width : width * divide, height); - name.setLayoutX(15); - Label alias1 = new Label(model.skinGroupName); - alias1.getStyleClass().addAll("list-item-label-sub"); - alias1.setPrefSize(width * (1 - divide), height); - alias1.setLayoutX(model.skinGroupName == null ? 0 : width * divide); - SVGPath fav = GuiPrefabs.Icons.getIcon(GuiPrefabs.Icons.SVG_STAR_FILLED, GuiPrefabs.COLOR_WARNING); - fav.getStyleClass().add("Search-models-star"); - fav.setLayoutX(0); - fav.setLayoutY(3); - fav.setScaleX(0.75); - fav.setScaleY(0.75); - item.setPrefSize(width, height); - item.setGraphic(new Group(fav, name, alias1)); - item.setItem(model); - item.setId(model.getLocation()); - if (app.config.character_favorites.containsKey(model.key)) - item.getStyleClass().add("Search-models-item-favorite"); - return item; - } - - private void selectCell(JFXListCell cell) { - // Reset - if (selectedModelCell != null) { - selectedModelCell.getStyleClass().setAll("list-item"); - if (app.config.character_favorites.containsKey(selectedModelCell.getItem().key)) - selectedModelCell.getStyleClass().add("Search-models-item-favorite"); - } - selectedModelCell = cell; - selectedModelCell.getStyleClass().add("list-item-active"); - // Display details - ModelItem model = cell.getItem(); - selectedModelName.setText(model.name); - selectedModelAppellation.setText(model.appellation); - selectedModelSkinGroupName.setText(model.skinGroupName); - selectedModelType.setText(app.modelsDataset.sortTags == null ? - model.type : app.modelsDataset.sortTags.getOrDefault(model.type, model.type)); - GuiPrefabs.addTooltip(selectedModelName, model.name); - GuiPrefabs.addTooltip(selectedModelAppellation, model.appellation); - GuiPrefabs.addTooltip(selectedModelSkinGroupName, model.skinGroupName); - GuiPrefabs.addTooltip(selectedModelType, model.type); + private void selectCell(ModelItem model) { + selectedModel.setModelItem(model); // Setup tag flow pane infoPaneTagFlow.getChildren().clear(); model.sortTags.forEach(o -> { String s = o.toString(); String t = app.modelsDataset.sortTags == null ? s : app.modelsDataset.sortTags.getOrDefault(s, s); - JFXButton tag = new JFXButton(t); - tag.getStyleClass().add("info-tag-badge-active"); - tag.setOnAction(e -> { - filterTagSet.clear(); - filterTagSet.add(s); - infoPaneComposer.activate(1); - modelSearch(searchModelInput.getText()); - }); + Button tag = new GuiPrefabs.ButtonBuilder() + .setText(t) + .setAdditionalStyleClass("info-tag-badge-active") + .setOnAction(e -> { + filterTagSet.clear(); + filterTagSet.add(s); + infoPaneComposer.activate(1); + modelSearch(searchModelInput.getText()); + }) + .build(); infoPaneTagFlow.getChildren().add(tag); }); // Switch info pane if (infoPaneComposer.getActivatedId() != 0) infoPaneComposer.activate(0); - // Check favorite - if (app.config.character_favorites.containsKey(model.key)) { - modelFavorite.setGraphic(favFillIcon); - } else { - modelFavorite.setGraphic(favIcon); - } // Apply to app.config, but not to save app.config.character_asset = model.getLocation(); app.config.character_files = model.assetList; @@ -816,4 +770,115 @@ private void checkModelUpdateByMD5(String remoteMD5) throws IOException { Logger.info("ModelManager", "Model repo version check finished (not up-to-dated)"); } } + + + private class ModelListCell extends GuiPrefabs.RipperListCell { + private final double width; + private final double height; + private final SVGPath icon; + private final Label name; + private final Label alias; + private static final double divide = 0.618; + private final static DropShadow iconShadow = new DropShadow(null, GuiPrefabs.COLOR_WHITE, 4.0, 0.5, 0.0, 0.0); + + public ModelListCell(double width, double height) { + super(); + this.width = width; + this.height = height; + + icon = GuiPrefabs.Icons.getIcon(GuiPrefabs.Icons.SVG_STAR_FILLED, GuiPrefabs.COLOR_WARNING); + icon.setLayoutX(3); + icon.setLayoutY(3); + icon.setScaleX(0.75); + icon.setScaleY(0.75); + icon.setOpacity(0); + icon.setEffect(iconShadow); + name = new Label(); + name.getStyleClass().add("list-item-label"); + name.setTranslateX(25); + alias = new Label(); + alias.setPrefSize(width * (1 - divide), height); + alias.getStyleClass().add("list-item-label-sub"); + alias.setTranslateX(20); + + getContent().setAll(icon, name, alias); + setPrefSize(width, height); + } + + @Override + protected void updateItem(ModelItem model, boolean empty) { + super.updateItem(model, empty); + if (empty || model == null) { + setContentVisible(false); + } else { + name.setText(model.toString()); + name.setPrefSize(model.skinGroupName == null ? width : width * divide, height); + alias.setText(model.skinGroupName); + alias.setLayoutX(model.skinGroupName == null ? 0 : width * divide); + icon.setOpacity(app.config.character_favorites.containsKey(model.key) ? 1 : 0); + setId(model.getLocation()); + setContentVisible(true); + } + } + } + + + private class ModelItemWrapper { + private final StringProperty nameProperty = new SimpleStringProperty(); + private final StringProperty typeProperty = new SimpleStringProperty(); + private final StringProperty skinGroupNameProperty = new SimpleStringProperty(); + private final StringProperty appellationProperty = new SimpleStringProperty(); + private HashMap sortTags; + private ModelItem modelItem; + + private final BooleanBinding hasWikiProperty = new BooleanBinding() { + @Override + protected boolean computeValue() { + return modelItem != null && nameProperty.get() != null && !nameProperty.get().isEmpty(); + } + }; + private final BooleanBinding favoriteProperty = new BooleanBinding() { + @Override + protected boolean computeValue() { + return modelItem != null && app.config.character_favorites.containsKey(modelItem.key); + } + }; + + public void setSortTags(HashMap sortTags) { + this.sortTags = sortTags; + } + + public void setModelItem(ModelItem modelItem) { + if (modelItem == null) return; + this.modelItem = modelItem; + nameProperty.set(modelItem.name); + typeProperty.set(sortTags == null ? + modelItem.type : sortTags.getOrDefault(modelItem.type, modelItem.type)); + skinGroupNameProperty.set(modelItem.skinGroupName); + appellationProperty.set(modelItem.appellation); + favoriteProperty.invalidate(); + hasWikiProperty.invalidate(); + } + + public void setFavorite(boolean favorite) { + if (modelItem == null) return; + app.config.character_favorites.remove(modelItem.key); + if (favorite) { + app.config.character_favorites.put(modelItem.key, new JSONObject()); + Logger.debug("ModelManager", "Add favorite model " + modelItem.key); + } else { + Logger.debug("ModelManager", "Remove favorite model " + modelItem.key); + } + favoriteProperty.invalidate(); + hasWikiProperty.invalidate(); + } + + public BooleanBinding getFavoriteProperty() { + return favoriteProperty; + } + + public BooleanBinding getHasWikiProperty() { + return hasWikiProperty; + } + } } diff --git a/desktop/src/cn/harryh/arkpets/controllers/RootModule.java b/desktop/src/cn/harryh/arkpets/controllers/RootModule.java index 8c0e7dce..f228b1df 100644 --- a/desktop/src/cn/harryh/arkpets/controllers/RootModule.java +++ b/desktop/src/cn/harryh/arkpets/controllers/RootModule.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.controllers; @@ -19,7 +19,8 @@ import cn.harryh.arkpets.utils.GuiComponents.Toast; import cn.harryh.arkpets.utils.GuiPrefabs; import cn.harryh.arkpets.utils.Logger; -import com.jfoenix.controls.*; +import com.jfoenix.controls.JFXDialog; +import com.jfoenix.controls.JFXDialogLayout; import javafx.application.Platform; import javafx.concurrent.ScheduledService; import javafx.concurrent.Task; @@ -76,21 +77,21 @@ public final class RootModule implements Controller { @FXML private Pane sidebar; @FXML - private JFXButton annoEntrance; + private Button annoEntrance; @FXML - private JFXButton menuBtn1; + private Button menuBtn1; @FXML - private JFXButton menuBtn2; + private Button menuBtn2; @FXML - private JFXButton menuBtn3; + private Button menuBtn3; @FXML - public JFXButton launchBtn; + public Button launchBtn; @FXML private HBox toast; private ArkHomeFX app; - private boolean checkEnd; + private boolean envChecked; @Override public void initializeWith(ArkHomeFX app) { @@ -237,24 +238,23 @@ protected void onSucceeded(boolean result) { private void initLaunchButton() { // Build environment check confirm dialog. + List tasks = EnvCheckTask.getAvailableTasks(); JFXDialog dialog = GuiPrefabs.Dialogs.createConfirmDialog(body, GuiPrefabs.Icons.getIcon(GuiPrefabs.Icons.SVG_HELP_ALT, GuiPrefabs.COLOR_INFO), "环境检查", "首次运行环境检查", "这似乎是你第一次运行 ArkPets,我们需要对您的系统进行一些基本检查以确保桌宠能够正常运行。\n你也可以跳过检查,但可能会导致使用体验下降。", - () -> { - new CheckEnvironmentTask(app.body,EnvCheckTask.getAvailableTasks(),this::launchArkPets).start(); - }); + () -> new CheckEnvironmentTask(app.body,tasks,this::launchArkPets).start()); Node cancel = ((JFXDialogLayout)dialog.getContent()).getActions().get(0); - ((JFXButton) cancel).setOnAction(e -> { + ((Button) cancel).setOnAction(e -> { GuiPrefabs.Dialogs.disposeDialog(dialog); launchArkPets(); }); // Set handler for internal start button. launchBtn.setOnAction(e -> { // When request to launch ArkPets: - if (isNewcomer && !checkEnd) { - checkEnd = true; + if (!tasks.isEmpty() && isNewcomer && !envChecked) { + envChecked = true; dialog.show(); return; } @@ -334,6 +334,7 @@ private void launchArkPets() { }); } } + private static class TrayExitHandBook extends Handbook { @Override public String getTitle() { diff --git a/desktop/src/cn/harryh/arkpets/controllers/SettingsModule.java b/desktop/src/cn/harryh/arkpets/controllers/SettingsModule.java index a6d35dc7..bd05d2bd 100644 --- a/desktop/src/cn/harryh/arkpets/controllers/SettingsModule.java +++ b/desktop/src/cn/harryh/arkpets/controllers/SettingsModule.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.controllers; @@ -21,9 +21,9 @@ import cn.harryh.arkpets.tray.HostTray; import cn.harryh.arkpets.utils.*; import cn.harryh.arkpets.utils.GuiComponents.*; -import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson2.JSONObject; import com.badlogic.gdx.graphics.Color; -import com.jfoenix.controls.*; +import com.jfoenix.controls.JFXDialog; import javafx.application.Platform; import javafx.beans.property.BooleanProperty; import javafx.concurrent.ScheduledService; @@ -52,81 +52,81 @@ public final class SettingsModule implements Controller { private Pane noticeBox; @FXML - private JFXComboBox> configDisplayScale; + private ComboBox> configDisplayScale; @FXML - private JFXButton configDisplayScaleHelp; + private Button configDisplayScaleHelp; @FXML - private JFXComboBox> configDisplayFps; + private ComboBox> configDisplayFps; @FXML - private JFXButton configDisplayFpsHelp; + private Button configDisplayFpsHelp; @FXML - private JFXTabPane configRenderTabPane; + private TabPane configRenderTabPane; @FXML - private JFXComboBox> configCanvasColor; + private ComboBox> configCanvasColor; @FXML - private JFXComboBox> configCanvasCoverage; + private ComboBox> configCanvasCoverage; @FXML - private JFXButton configCanvasCoverageHelp; + private Button configCanvasCoverageHelp; @FXML - private JFXComboBox> configCanvasSamplingInterval; + private ComboBox> configCanvasSamplingInterval; @FXML - private JFXComboBox> configRenderOutline; + private ComboBox> configRenderOutline; @FXML - private JFXComboBox> configRenderOutlineEmphasis; + private ComboBox> configRenderOutlineEmphasis; @FXML - private JFXComboBox> configRenderOutlineColor; + private ComboBox> configRenderOutlineColor; @FXML - private JFXComboBox> configRenderOutlineColorEmphasis; + private ComboBox> configRenderOutlineColorEmphasis; @FXML - private JFXComboBox> configRenderOutlineWidth; + private ComboBox> configRenderOutlineWidth; @FXML - private JFXSlider configRenderOpacityNormal; + private Slider configRenderOpacityNormal; @FXML private Label configRenderOpacityNormalValue; @FXML - private JFXSlider configRenderOpacityDim; + private Slider configRenderOpacityDim; @FXML private Label configRenderOpacityDimValue; @FXML - private JFXComboBox> configRenderShadowColor; + private ComboBox> configRenderShadowColor; @FXML private HBox configAngleBox; @FXML - private JFXCheckBox configEnableAngle; + private CheckBox configEnableAngle; @FXML - private JFXButton configEnableAngleHelp; + private Button configEnableAngleHelp; @FXML - private JFXCheckBox configEnableMipMap; + private CheckBox configEnableMipMap; @FXML - private JFXButton configEnableMipMapHelp; + private Button configEnableMipMapHelp; @FXML - private JFXCheckBox configWindowTopmost; + private CheckBox configWindowTopmost; @FXML - private JFXComboBox configLoggingLevel; + private ComboBox configLoggingLevel; @FXML - private JFXButton exportLog; + private Button exportLog; @FXML - private JFXCheckBox configAutoStartup; + private CheckBox configAutoStartup; @FXML - private JFXCheckBox configSolidExit; + private CheckBox configSolidExit; @FXML - private JFXCheckBox configWindowToolwindow; + private CheckBox configWindowToolwindow; @FXML - private JFXButton configWindowToolwindowHelp; + private Button configWindowToolwindowHelp; @FXML - private JFXCheckBox configEcoMode; + private CheckBox configEcoMode; @FXML - private JFXComboBox> configWindowSystem; + private ComboBox> configWindowSystem; @FXML - private JFXButton configWindowSystemHelp; + private Button configWindowSystemHelp; @FXML private Label runEnvCheck; @FXML - private JFXTextField configNetworkSource; + private TextField configNetworkSource; @FXML - private JFXTextField configNetworkProxy; + private TextField configNetworkProxy; @FXML private Label configNetworkProxyStatus; @@ -157,8 +157,8 @@ public void initializeWith(ArkHomeFX app) { initNetwork(); initAbout(); initScheduledListener(); - - javafx.application.Platform.runLater(() -> GuiPrefabs.disableScrollPaneCache(moduleScroll)); + ScrollUtils.addSmoothScrolling(moduleScroll); + Platform.runLater(() -> GuiPrefabs.disableScrollPaneCache(moduleScroll)); } private void initConfigDisplay() { @@ -336,12 +336,6 @@ public String getContent() { } }; - if(isMac || isLinux) { - // Because some ANGLE Metal bug (background), temporary hide on macOS. - // Hide on Linux because the primary graphics API is OpenGL. - configAngleBox.setVisible(false); - configAngleBox.setManaged(false); - } configEnableAngle.setSelected(app.config.render_enable_angle); configEnableAngle.setOnAction(e -> { app.config.render_enable_angle = configEnableAngle.isSelected(); @@ -569,8 +563,10 @@ protected void onHasNewStableVersion(Version stableVersion) { "检测到软件有新的版本!", "当前版本 " + appVersion + " 可更新到 " + stableVersion + "\n是否要现在进行更新?", () -> executeAppUpdate()); - JFXButton gotoButton = new JFXButton("访问官网"); - gotoButton.setOnAction(e -> app.popBrowser(PathConfig.urlDownload)); + Button gotoButton = new GuiPrefabs.ButtonBuilder() + .setText("访问官网") + .setOnAction(e -> app.popBrowser(PathConfig.urlDownload)) + .build(); GuiPrefabs.Dialogs.attachAction(dialog, gotoButton, 0); dialog.show(); } @@ -585,12 +581,14 @@ protected void onUpToDated(Version stableVersion) { "尚未发现新的正式版本。", "当前版本 " + appVersion + " 已是最新", null); - JFXButton forceBtn = new JFXButton("强制重装"); - forceBtn.setOnAction(e -> { - executeAppUpdate(); - GuiPrefabs.Dialogs.disposeDialog(dialog); - }); - GuiPrefabs.Dialogs.attachAction(dialog, forceBtn, 0); + Button forceButton = new GuiPrefabs.ButtonBuilder() + .setText("强制重装") + .setOnAction(e -> { + executeAppUpdate(); + GuiPrefabs.Dialogs.disposeDialog(dialog); + }) + .build(); + GuiPrefabs.Dialogs.attachAction(dialog, forceButton, 0); dialog.show(); } diff --git a/desktop/src/cn/harryh/arkpets/controllers/Titlebar.java b/desktop/src/cn/harryh/arkpets/controllers/Titlebar.java index 7c3b8208..801e7159 100644 --- a/desktop/src/cn/harryh/arkpets/controllers/Titlebar.java +++ b/desktop/src/cn/harryh/arkpets/controllers/Titlebar.java @@ -11,8 +11,7 @@ import javafx.scene.layout.HBox; import javafx.scene.text.Text; -import static cn.harryh.arkpets.Const.appName; -import static cn.harryh.arkpets.Const.durationFast; +import static cn.harryh.arkpets.Const.*; public class Titlebar implements Controller { @@ -44,7 +43,7 @@ public class Titlebar implements Controller { @Override public void initializeWith(ArkHomeFX app) { this.app = app; - if (forceUiStyle.equals("mac") || Const.isMac) { + if (forceUiStyle.equals("mac") || isMac) { initMacTitlebar(); } else if (forceUiStyle.equals("win") || Const.isWindows){ @@ -65,10 +64,12 @@ public void titleBarDragged(MouseEvent event) { @FXML public void windowMinimize(MouseEvent event) { - GuiPrefabs.fadeOutWindow(app.stage, durationFast, e -> { - app.stage.hide(); - app.stage.setIconified(true); - }); + if (!isMac) { + GuiPrefabs.fadeOutWindow(app.stage, durationFast, e -> { + app.stage.hide(); + }); + } + app.stage.setIconified(true); } @FXML @@ -144,5 +145,6 @@ private void initMacTitlebar() { titleMacMinimizeImage.setViewport(area); } }); + } } diff --git a/desktop/src/cn/harryh/arkpets/guitasks/AppInstallTask.java b/desktop/src/cn/harryh/arkpets/guitasks/AppInstallTask.java index 40bcece6..98525aa8 100644 --- a/desktop/src/cn/harryh/arkpets/guitasks/AppInstallTask.java +++ b/desktop/src/cn/harryh/arkpets/guitasks/AppInstallTask.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.guitasks; diff --git a/desktop/src/cn/harryh/arkpets/guitasks/DeleteTempFilesTask.java b/desktop/src/cn/harryh/arkpets/guitasks/DeleteTempFilesTask.java index dd687061..536791d1 100644 --- a/desktop/src/cn/harryh/arkpets/guitasks/DeleteTempFilesTask.java +++ b/desktop/src/cn/harryh/arkpets/guitasks/DeleteTempFilesTask.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.guitasks; diff --git a/desktop/src/cn/harryh/arkpets/guitasks/GuiTask.java b/desktop/src/cn/harryh/arkpets/guitasks/GuiTask.java index 865cfea1..b04c5d58 100644 --- a/desktop/src/cn/harryh/arkpets/guitasks/GuiTask.java +++ b/desktop/src/cn/harryh/arkpets/guitasks/GuiTask.java @@ -1,11 +1,12 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.guitasks; import cn.harryh.arkpets.utils.GuiPrefabs; import cn.harryh.arkpets.utils.Logger; -import com.jfoenix.controls.*; +import com.jfoenix.controls.JFXDialog; +import com.jfoenix.controls.JFXDialogLayout; import javafx.concurrent.Task; import javafx.scene.control.*; import javafx.scene.layout.StackPane; @@ -136,12 +137,11 @@ private JFXDialog getDialog(StackPane parent, Task boundTask, boolean c JFXDialogLayout layout = new JFXDialogLayout(); layout.setHeading(bar); layout.setBody(content); - layout.setActions(GuiPrefabs.Dialogs.getOkayButton(dialog)); dialog.setContent(layout); + + // Set the actions of the dialog if (cancelable) { - JFXButton cancel = GuiPrefabs.Dialogs.getCancelButton(dialog); - cancel.setOnAction(e -> boundTask.cancel()); - layout.setActions(cancel); + layout.setActions(GuiPrefabs.Dialogs.getCancelButton(dialog, e -> boundTask.cancel())); } else { layout.setActions(List.of()); } diff --git a/desktop/src/cn/harryh/arkpets/guitasks/PostUnzipModelTask.java b/desktop/src/cn/harryh/arkpets/guitasks/PostUnzipModelTask.java index a4f88e59..780de25c 100644 --- a/desktop/src/cn/harryh/arkpets/guitasks/PostUnzipModelTask.java +++ b/desktop/src/cn/harryh/arkpets/guitasks/PostUnzipModelTask.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.guitasks; diff --git a/desktop/src/cn/harryh/arkpets/guitasks/UnzipModelsTask.java b/desktop/src/cn/harryh/arkpets/guitasks/UnzipModelsTask.java index 7e6d2d5c..01316f5c 100644 --- a/desktop/src/cn/harryh/arkpets/guitasks/UnzipModelsTask.java +++ b/desktop/src/cn/harryh/arkpets/guitasks/UnzipModelsTask.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.guitasks; diff --git a/desktop/src/cn/harryh/arkpets/guitasks/UnzipTask.java b/desktop/src/cn/harryh/arkpets/guitasks/UnzipTask.java index 4d3d9a75..f865c194 100644 --- a/desktop/src/cn/harryh/arkpets/guitasks/UnzipTask.java +++ b/desktop/src/cn/harryh/arkpets/guitasks/UnzipTask.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.guitasks; diff --git a/desktop/src/cn/harryh/arkpets/guitasks/VerifyModelsTask.java b/desktop/src/cn/harryh/arkpets/guitasks/VerifyModelsTask.java index a841a0b9..a7665dbb 100644 --- a/desktop/src/cn/harryh/arkpets/guitasks/VerifyModelsTask.java +++ b/desktop/src/cn/harryh/arkpets/guitasks/VerifyModelsTask.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.guitasks; diff --git a/desktop/src/cn/harryh/arkpets/guitasks/ZipTask.java b/desktop/src/cn/harryh/arkpets/guitasks/ZipTask.java index 1ede45de..cb8eba64 100644 --- a/desktop/src/cn/harryh/arkpets/guitasks/ZipTask.java +++ b/desktop/src/cn/harryh/arkpets/guitasks/ZipTask.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.guitasks; diff --git a/desktop/src/cn/harryh/arkpets/guitasks/envchecker/AccessibilityCheckTask.java b/desktop/src/cn/harryh/arkpets/guitasks/envchecker/AccessibilityCheckTask.java new file mode 100644 index 00000000..164e6c73 --- /dev/null +++ b/desktop/src/cn/harryh/arkpets/guitasks/envchecker/AccessibilityCheckTask.java @@ -0,0 +1,44 @@ +package cn.harryh.arkpets.guitasks.envchecker; + +import cn.harryh.arkpets.natives.HIServices; +import cn.harryh.arkpets.utils.Logger; + +import java.awt.*; +import java.net.URI; + + +public class AccessibilityCheckTask extends EnvCheckTask{ + @Override + public String getFailureReason() { + return "启用辅助功能"; + } + + @Override + public String getFailureDetail() { + return "ArkPets 的部分功能需要启用辅助功能权限才能使用。\n请在打开的“辅助功能”窗口中找到 ArkPets,并启用辅助功能权限。"; + } + + @Override + public boolean tryFix() { + return false; + } + + @Override + public boolean canFix() { + return false; + } + + @Override + public boolean run() { + if (!HIServices.INSTANCE.AXIsProcessTrusted()) { + try { + Desktop.getDesktop().browse(new URI("x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility")); + } catch (Exception e) { + Logger.error("Launcher", "Failed to open System Preferences Page"); + } + return false; + } else { + return true; + } + } +} diff --git a/desktop/src/cn/harryh/arkpets/guitasks/envchecker/EnvCheckTask.java b/desktop/src/cn/harryh/arkpets/guitasks/envchecker/EnvCheckTask.java index a9634475..7ecefff7 100644 --- a/desktop/src/cn/harryh/arkpets/guitasks/envchecker/EnvCheckTask.java +++ b/desktop/src/cn/harryh/arkpets/guitasks/envchecker/EnvCheckTask.java @@ -39,7 +39,7 @@ public static List getAvailableTasks() { ArrayList list = new ArrayList<>(); list.add(new SleepEnvCheckTask(1000)); if (Const.isWindows) { - list.add(new WinGraphicsEnvCheckTask()); + // TODO } if (Const.isLinux) { String desktop = System.getenv("XDG_CURRENT_DESKTOP"); @@ -54,6 +54,9 @@ public static List getAvailableTasks() { } } } + if (Const.isMac) { + list.add(new AccessibilityCheckTask()); + } return list; } } diff --git a/desktop/src/cn/harryh/arkpets/guitasks/envchecker/GNOMEPluginCheckTask.java b/desktop/src/cn/harryh/arkpets/guitasks/envchecker/GNOMEPluginCheckTask.java index b775d6ff..4eb2df86 100644 --- a/desktop/src/cn/harryh/arkpets/guitasks/envchecker/GNOMEPluginCheckTask.java +++ b/desktop/src/cn/harryh/arkpets/guitasks/envchecker/GNOMEPluginCheckTask.java @@ -1,7 +1,7 @@ package cn.harryh.arkpets.guitasks.envchecker; import cn.harryh.arkpets.Const; -import cn.harryh.arkpets.natives.MutterPluginInterface; +import cn.harryh.arkpets.rpc.MutterPluginInterface; import cn.harryh.arkpets.utils.IOUtils; import cn.harryh.arkpets.utils.Logger; import org.freedesktop.dbus.connections.impl.DBusConnection; diff --git a/desktop/src/cn/harryh/arkpets/guitasks/envchecker/KWinPluginCheckTask.java b/desktop/src/cn/harryh/arkpets/guitasks/envchecker/KWinPluginCheckTask.java index 5030da48..5d4433bc 100644 --- a/desktop/src/cn/harryh/arkpets/guitasks/envchecker/KWinPluginCheckTask.java +++ b/desktop/src/cn/harryh/arkpets/guitasks/envchecker/KWinPluginCheckTask.java @@ -1,7 +1,7 @@ package cn.harryh.arkpets.guitasks.envchecker; import cn.harryh.arkpets.Const; -import cn.harryh.arkpets.natives.KWinPluginInterface; +import cn.harryh.arkpets.rpc.KWinPluginInterface; import cn.harryh.arkpets.utils.IOUtils; import cn.harryh.arkpets.utils.Logger; import org.freedesktop.dbus.connections.impl.DBusConnection; @@ -61,7 +61,7 @@ public boolean run() { return false; } try { - String result = IOUtils.CommandUtil.runCommand("plasmashell --version", null, null); + String result = IOUtils.CommandUtil.runCommand(new String[] {"plasmashell", "--version"}, null, null); Pattern pattern = Pattern.compile("plasmashell (\\d+)"); if (result == null) return false; Matcher matcher = pattern.matcher(result); diff --git a/desktop/src/cn/harryh/arkpets/guitasks/envchecker/WinGraphicsEnvCheckTask.java b/desktop/src/cn/harryh/arkpets/guitasks/envchecker/WinGraphicsEnvCheckTask.java deleted file mode 100644 index efd51a15..00000000 --- a/desktop/src/cn/harryh/arkpets/guitasks/envchecker/WinGraphicsEnvCheckTask.java +++ /dev/null @@ -1,283 +0,0 @@ -package cn.harryh.arkpets.guitasks.envchecker; - -import cn.harryh.arkpets.ArkConfig; -import cn.harryh.arkpets.naitves.NVAPIWrapper; -import cn.harryh.arkpets.utils.IOUtils; -import cn.harryh.arkpets.utils.Logger; -import com.sun.jna.Native; -import com.sun.jna.NativeLong; -import com.sun.jna.Pointer; -import com.sun.jna.WString; -import com.sun.jna.platform.win32.Advapi32; -import com.sun.jna.platform.win32.Advapi32Util; -import com.sun.jna.platform.win32.Win32Exception; -import com.sun.jna.platform.win32.WinReg; -import com.sun.jna.ptr.IntByReference; -import com.sun.jna.ptr.PointerByReference; - -import java.io.File; -import java.util.Objects; - -import static com.sun.jna.platform.win32.WinNT.*; -import static com.sun.jna.platform.win32.WinReg.HKEY_CURRENT_USER; - - -public class WinGraphicsEnvCheckTask extends EnvCheckTask { - public static final String NVAPI_PROFILE_NAME = "ArkPets"; - private final String launcherPath; - private final String javaBin; - private String failureReason; - private String failureDetail; - private FixMode fix; - - public WinGraphicsEnvCheckTask() { - super(); - javaBin = System.getProperty("java.home") + File.separator + "bin" + File.separator + "java.exe"; - File launcher = new File("ArkPets.exe"); - if (launcher.exists()) launcherPath = launcher.getAbsolutePath().replaceAll("\"", "\"\""); - else launcherPath = javaBin; - } - - private static String wmicCheck() { - try { - String result = IOUtils.CommandUtil.runCommand("wmic path win32_VideoController get Name", null, null); - if (result != null) { - return result; - } else { - Logger.warn("EnvCheck", "Failed to get graphics card info"); - return null; - } - } catch (Exception e) { - Logger.warn("EnvCheck", "Failed to get graphics card info"); - return null; - } - } - - @Override - public String getFailureReason() { - return failureReason; - } - - @Override - public String getFailureDetail() { - return failureDetail; - } - - @Override - public boolean tryFix() { - try { - switch (fix) { - case NV -> setNvidiaGLSettings(true, launcherPath, javaBin); - case WIN_SAV -> { - setWinGraphicsCard(launcherPath, false); - setWinGraphicsCard(javaBin, false); - } - case WIN_SAV_NV -> { - setWinGraphicsCard(launcherPath, false); - setWinGraphicsCard(javaBin, false); - setNvidiaGLSettings(false, launcherPath, javaBin); - } - case ANGLE -> { - ArkConfig config = Objects.requireNonNull(ArkConfig.getConfig()); - config.render_enable_angle = true; - config.save(); - } - } - } catch (Exception e) { - Logger.error("System", "Failed to modify graphics settings", e); - failureDetail = "自动设置显卡失败"; - failureReason = "尝试设置显卡时失败,请查看“常见问题解答”中的方法进行设置。"; - return false; - } - return true; - } - - @Override - public boolean canFix() { - return fix != FixMode.FAIL; - } - - @Override - public boolean run() { - String cards = wmicCheck(); - if (cards != null) { - try { - if (cards.contains("Intel") && cards.contains("NVIDIA")) { - // I+N Hybrid - boolean card = checkWinGraphicsCard(launcherPath, false) && checkWinGraphicsCard(javaBin, false); - boolean nv = checkNvidiaGLSettings(); - if (!card || !nv) { - fix = FixMode.WIN_SAV_NV; - return false; - } - return true; - } else if (cards.contains("Intel") && cards.contains("AMD")) { - // I+A Hybrid - boolean card = checkWinGraphicsCard(launcherPath, false) && checkWinGraphicsCard(javaBin, false); - if (!card) { - fix = FixMode.WIN_SAV; - return false; - } - return true; - } else if (cards.contains("AMD") && cards.contains("NVIDIA")) { - fix = FixMode.ANGLE; - return false; - } else if (cards.contains("AMD")) { - fix = FixMode.ANGLE; - return false; - } else if (cards.contains("NVIDIA")) { - // NVIDIA only - boolean status = checkNvidiaGLSettings(); - if (!status) { - fix = FixMode.NV; - return false; - } - return true; - } else if (cards.contains("Intel")) { - // Intel only - return true; - } else { - // Other card (Virtual,Software,Non-mainstream...) - failureReason = "未知显卡警告"; - failureDetail = "当前可能正在使用特殊显卡(虚拟显卡、软件渲染等),ArkPets 尚未对这类显卡进行测试。\n你仍可以强制运行,但可能会产生未知的问题。"; - fix = FixMode.FAIL; - return false; - } - } catch (Exception e) { - failureReason = "获取显卡信息失败"; - failureDetail = "当前无法获取显卡信息,请参考“常见问题解答”对显卡进行设置。"; - fix = FixMode.FAIL; - return false; - } - } else { - failureReason = "获取显卡信息失败"; - failureDetail = "当前无法获取显卡信息,请参考“常见问题解答”对显卡进行设置。"; - fix = FixMode.FAIL; - return false; - } - } - - public boolean checkNvidiaGLSettings() { - boolean status = false; - NVAPIWrapper.NvAPI_Initialize(); - PointerByReference sess = new PointerByReference(); - NVAPIWrapper.NvAPI_DRS_CreateSession(sess); - NVAPIWrapper.NvAPI_DRS_LoadSettings(sess.getValue()); - PointerByReference pro = new PointerByReference(); - try { - NVAPIWrapper.NvAPI_DRS_FindProfileByName(sess.getValue(), new WString(NVAPI_PROFILE_NAME), pro); - status = true; - } catch (Exception e) { - Logger.error("EnvCheck", "Failed to get NVIDIA Settings", e); - } - NVAPIWrapper.NvAPI_DRS_DestroySession(sess.getValue()); - NVAPIWrapper.NvAPI_Unload(); - return status; - } - - public void setNvidiaGLSettings(boolean performance, String... path) { - NVAPIWrapper.NvAPI_Initialize(); - PointerByReference sess = new PointerByReference(); - NVAPIWrapper.NvAPI_DRS_CreateSession(sess); - NVAPIWrapper.NvAPI_DRS_LoadSettings(sess.getValue()); - removeNvidiaProfile(sess.getValue()); // clean before write - PointerByReference prof = new PointerByReference(); - NVAPIWrapper.NVDRS_PROFILE.ByReference profile = new NVAPIWrapper.NVDRS_PROFILE.ByReference(); - NVAPIWrapper.writeStringToShortArray(NVAPI_PROFILE_NAME, profile.profileName); - NVAPIWrapper.NvAPI_DRS_CreateProfile(sess.getValue(), profile, prof); - for (String p : path) { - NVAPIWrapper.NVDRS_APPLICATION.ByReference app = new NVAPIWrapper.NVDRS_APPLICATION.ByReference(); - NVAPIWrapper.writeStringToShortArray(p, app.appName); - NVAPIWrapper.writeStringToShortArray(p, app.userFriendlyName); - NVAPIWrapper.NvAPI_DRS_CreateApplication(sess.getValue(), prof.getValue(), app); - } - if (performance) { - NVAPIWrapper.NVDRS_SETTING.ByReference glSetting = new NVAPIWrapper.NVDRS_SETTING.ByReference(); - glSetting.settingId = new NativeLong(0x2072C5A3); - glSetting.settingType = 0; - glSetting.currentValue.u32 = new NativeLong(1); - NVAPIWrapper.NVDRS_SETTING.ByReference dxgiSetting = new NVAPIWrapper.NVDRS_SETTING.ByReference(); - dxgiSetting.settingId = new NativeLong(0x20D690F8); - dxgiSetting.currentValue.u32 = new NativeLong(0); - dxgiSetting.settingType = 0; - NVAPIWrapper.NvAPI_DRS_SetSetting(sess.getValue(), prof.getValue(), glSetting); - NVAPIWrapper.NvAPI_DRS_SetSetting(sess.getValue(), prof.getValue(), dxgiSetting); - } - NVAPIWrapper.NVDRS_SETTING.ByReference optimusSetting = new NVAPIWrapper.NVDRS_SETTING.ByReference(); - optimusSetting.settingId = new NativeLong(0x10F9DC81); - optimusSetting.currentValue.u32 = performance ? new NativeLong(1) : new NativeLong(0); - optimusSetting.settingType = 0; - NVAPIWrapper.NvAPI_DRS_SetSetting(sess.getValue(), prof.getValue(), optimusSetting); - NVAPIWrapper.NvAPI_DRS_SaveSettings(sess.getValue()); - NVAPIWrapper.NvAPI_DRS_DestroySession(sess.getValue()); - NVAPIWrapper.NvAPI_Unload(); - Logger.info("EnvCheck", "Success write NVIDIA GPU settings"); - } - - public boolean checkWinGraphicsCard(String path, boolean performance) { - WinReg.HKEYByReference outKey = new WinReg.HKEYByReference(); - int winstatus = Advapi32.INSTANCE.RegOpenKeyEx(HKEY_CURRENT_USER, - "Software\\Microsoft\\DirectX\\UserGpuPreferences", 0, KEY_READ, outKey); - if (winstatus != 0) throw new Win32Exception(winstatus); - char[] data = new char[1024]; - winstatus = Advapi32.INSTANCE.RegQueryValueEx(outKey.getValue(), path, 0, - new IntByReference(REG_SZ), data, new IntByReference(1024)); - if (winstatus != 0) { - if (winstatus == 2) return false; // not found, uncertain card. - throw new Win32Exception(winstatus); - } - String value = Native.toString(data); - Advapi32.INSTANCE.RegCloseKey(outKey.getValue()); - if (value.contains("GpuPreference=0;")) return false; // uncertain card. - if (value.contains("GpuPreference=1;") && !performance) return true; - return value.contains("GpuPreference=2;") && performance; - } - - public void setWinGraphicsCard(String path, boolean performance) { - WinReg.HKEYByReference outKey = new WinReg.HKEYByReference(); - int winstatus = Advapi32.INSTANCE.RegOpenKeyEx(HKEY_CURRENT_USER, - "Software\\Microsoft\\DirectX\\UserGpuPreferences", 0, KEY_WRITE, outKey); - if (winstatus != 0) throw new Win32Exception(winstatus); - String value = performance ? "GpuPreference=2;" : "GpuPreference=1;"; - Advapi32Util.registrySetStringValue(outKey.getValue(), path, value); - Advapi32.INSTANCE.RegCloseKey(outKey.getValue()); - Logger.info("EnvCheck", "Success set GPU to " + (performance ? "performance" : "power-saving") + "mode"); - } - - public void removeNvidiaSettings() { - try { - String cards = wmicCheck(); - if (cards != null && cards.contains("NVIDIA")) { - NVAPIWrapper.NvAPI_Initialize(); - PointerByReference sess = new PointerByReference(); - NVAPIWrapper.NvAPI_DRS_CreateSession(sess); - NVAPIWrapper.NvAPI_DRS_LoadSettings(sess.getValue()); - removeNvidiaProfile(sess.getValue()); - NVAPIWrapper.NvAPI_DRS_DestroySession(sess.getValue()); - NVAPIWrapper.NvAPI_Unload(); - Logger.info("EnvCheck", "Success remove NVIDIA GPU settings"); - } - } catch (Exception e) { - Logger.error("EnvCheck", "Failed to remove NVIDIA settings,possible already remove", e); - } - } - - private void removeNvidiaProfile(Pointer sess) { - PointerByReference pro = new PointerByReference(); - try { - NVAPIWrapper.NvAPI_DRS_FindProfileByName(sess, new WString(NVAPI_PROFILE_NAME), pro); - NVAPIWrapper.NvAPI_DRS_DeleteProfile(sess, pro.getValue()); - NVAPIWrapper.NvAPI_DRS_SaveSettings(sess); - } catch (Exception e) { - Logger.error("EnvCheck", "Failed to remove NVIDIA settings,possible already remove", e); - } - } - - private enum FixMode { - WIN_SAV, // Windows Power-saving - WIN_SAV_NV, // Windows and NVIDIA Power-saving - ANGLE, // Enable ANGLE - NV, // NVIDIA OpenGL GDI and Present method - FAIL // Can't Fix - } -} diff --git a/desktop/src/cn/harryh/arkpets/guitasks/requests/CheckAppUpdateTask.java b/desktop/src/cn/harryh/arkpets/guitasks/requests/CheckAppUpdateTask.java index b63faabc..9bf8d47b 100644 --- a/desktop/src/cn/harryh/arkpets/guitasks/requests/CheckAppUpdateTask.java +++ b/desktop/src/cn/harryh/arkpets/guitasks/requests/CheckAppUpdateTask.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.guitasks.requests; @@ -10,7 +10,7 @@ import cn.harryh.arkpets.utils.Logger; import cn.harryh.arkpets.utils.StringUtils; import cn.harryh.arkpets.utils.Version; -import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson2.JSONObject; import javafx.scene.layout.StackPane; import java.net.URL; diff --git a/desktop/src/cn/harryh/arkpets/guitasks/requests/DownloadAppTask.java b/desktop/src/cn/harryh/arkpets/guitasks/requests/DownloadAppTask.java index e29b95d9..6d84a19a 100644 --- a/desktop/src/cn/harryh/arkpets/guitasks/requests/DownloadAppTask.java +++ b/desktop/src/cn/harryh/arkpets/guitasks/requests/DownloadAppTask.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.guitasks.requests; diff --git a/desktop/src/cn/harryh/arkpets/guitasks/requests/DownloadModelDatasetTask.java b/desktop/src/cn/harryh/arkpets/guitasks/requests/DownloadModelDatasetTask.java index 61f51d9f..308cc4bf 100644 --- a/desktop/src/cn/harryh/arkpets/guitasks/requests/DownloadModelDatasetTask.java +++ b/desktop/src/cn/harryh/arkpets/guitasks/requests/DownloadModelDatasetTask.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.guitasks.requests; diff --git a/desktop/src/cn/harryh/arkpets/guitasks/requests/DownloadModelsTask.java b/desktop/src/cn/harryh/arkpets/guitasks/requests/DownloadModelsTask.java index 00588276..cbbb3474 100644 --- a/desktop/src/cn/harryh/arkpets/guitasks/requests/DownloadModelsTask.java +++ b/desktop/src/cn/harryh/arkpets/guitasks/requests/DownloadModelsTask.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.guitasks.requests; diff --git a/desktop/src/cn/harryh/arkpets/guitasks/requests/FetchAnnounceTask.java b/desktop/src/cn/harryh/arkpets/guitasks/requests/FetchAnnounceTask.java index 2e381496..671e6848 100644 --- a/desktop/src/cn/harryh/arkpets/guitasks/requests/FetchAnnounceTask.java +++ b/desktop/src/cn/harryh/arkpets/guitasks/requests/FetchAnnounceTask.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.guitasks.requests; @@ -7,7 +7,7 @@ import cn.harryh.arkpets.utils.GuiPrefabs; import cn.harryh.arkpets.utils.Logger; import cn.harryh.arkpets.utils.StringUtils; -import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson2.JSONObject; import javafx.collections.ObservableList; import javafx.scene.layout.StackPane; diff --git a/desktop/src/cn/harryh/arkpets/guitasks/requests/FetchAsDataTask.java b/desktop/src/cn/harryh/arkpets/guitasks/requests/FetchAsDataTask.java index cb6ab6c3..8052838b 100644 --- a/desktop/src/cn/harryh/arkpets/guitasks/requests/FetchAsDataTask.java +++ b/desktop/src/cn/harryh/arkpets/guitasks/requests/FetchAsDataTask.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.guitasks.requests; @@ -7,8 +7,8 @@ import cn.harryh.arkpets.network.Connections; import cn.harryh.arkpets.utils.Logger; import cn.harryh.arkpets.utils.StringUtils; -import com.alibaba.fastjson.JSONException; -import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson2.JSONException; +import com.alibaba.fastjson2.JSONObject; import javafx.concurrent.Task; import javafx.scene.layout.StackPane; diff --git a/desktop/src/cn/harryh/arkpets/guitasks/requests/FetchAsFileTask.java b/desktop/src/cn/harryh/arkpets/guitasks/requests/FetchAsFileTask.java index a0bcbd61..096203d3 100644 --- a/desktop/src/cn/harryh/arkpets/guitasks/requests/FetchAsFileTask.java +++ b/desktop/src/cn/harryh/arkpets/guitasks/requests/FetchAsFileTask.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.guitasks.requests; diff --git a/desktop/src/cn/harryh/arkpets/guitasks/requests/FetchTask.java b/desktop/src/cn/harryh/arkpets/guitasks/requests/FetchTask.java index 0dacb423..fac98588 100644 --- a/desktop/src/cn/harryh/arkpets/guitasks/requests/FetchTask.java +++ b/desktop/src/cn/harryh/arkpets/guitasks/requests/FetchTask.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.guitasks.requests; diff --git a/desktop/src/cn/harryh/arkpets/guitasks/requests/McCheckAppUpdateTask.java b/desktop/src/cn/harryh/arkpets/guitasks/requests/McCheckAppUpdateTask.java index 845119c6..5de57d0e 100644 --- a/desktop/src/cn/harryh/arkpets/guitasks/requests/McCheckAppUpdateTask.java +++ b/desktop/src/cn/harryh/arkpets/guitasks/requests/McCheckAppUpdateTask.java @@ -1,10 +1,10 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.guitasks.requests; import cn.harryh.arkpets.utils.StringUtils; -import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson2.JSONObject; import javafx.scene.layout.StackPane; import java.net.URL; diff --git a/desktop/src/cn/harryh/arkpets/guitasks/requests/McCheckModelsUpdateTask.java b/desktop/src/cn/harryh/arkpets/guitasks/requests/McCheckModelsUpdateTask.java index f302961c..21344ab4 100644 --- a/desktop/src/cn/harryh/arkpets/guitasks/requests/McCheckModelsUpdateTask.java +++ b/desktop/src/cn/harryh/arkpets/guitasks/requests/McCheckModelsUpdateTask.java @@ -1,10 +1,10 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.guitasks.requests; import cn.harryh.arkpets.utils.StringUtils; -import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson2.JSONObject; import javafx.scene.layout.StackPane; import java.net.URL; @@ -16,7 +16,7 @@ public class McCheckModelsUpdateTask extends FetchAsDataTask { private final String cdk; public McCheckModelsUpdateTask(StackPane parent, GuiTaskStyle style, String cdk) { - super(parent, style); + super(parent, style, new int[]{400, 403}); this.cdk = cdk; } diff --git a/desktop/src/cn/harryh/arkpets/naitves/NVAPIWrapper.java b/desktop/src/cn/harryh/arkpets/naitves/NVAPIWrapper.java deleted file mode 100644 index 29cb4941..00000000 --- a/desktop/src/cn/harryh/arkpets/naitves/NVAPIWrapper.java +++ /dev/null @@ -1,194 +0,0 @@ -package cn.harryh.arkpets.naitves; - -import com.sun.jna.*; -import com.sun.jna.ptr.PointerByReference; - - -public class NVAPIWrapper { - public static void NvAPI_Initialize() { - checkStatus(getFunction(0x150E828).invokeInt(new Object[]{})); - } - - public static void NvAPI_DRS_CreateSession(PointerByReference phSession) { - checkStatus(getFunction(0x694D52E).invokeInt(new Object[]{phSession})); - } - - public static void NvAPI_DRS_DestroySession(Pointer hSession) { - checkStatus(getFunction(0xDAD9CFF8).invokeInt(new Object[]{hSession})); - } - - public static void NvAPI_DRS_LoadSettings(Pointer hSession) { - checkStatus(getFunction(0x375DBD6B).invokeInt(new Object[]{hSession})); - } - - public static void NvAPI_DRS_SaveSettings(Pointer hSession) { - checkStatus(getFunction(0xFCBC7E14).invokeInt(new Object[]{hSession})); - } - - public static void NvAPI_DRS_FindProfileByName(Pointer hSession, WString profileName, PointerByReference phProfile) { - checkStatus(getFunction(0x7E4A9A0B).invokeInt(new Object[]{hSession, profileName, phProfile})); - } - - public static void NvAPI_Unload() { - checkStatus(getFunction(0xD22BDD7E).invokeInt(new Object[]{})); - } - - public static void NvAPI_DRS_CreateApplication(Pointer hSession, Pointer hProfile, NVDRS_APPLICATION.ByReference pApplication) { - checkStatus(getFunction(0x4347A9DE).invokeInt(new Object[]{hSession, hProfile, pApplication})); - } - - public static void NvAPI_DRS_CreateProfile(Pointer hSession, NVDRS_PROFILE.ByReference pProfileInfo, PointerByReference phProfile) { - checkStatus(getFunction(0xCC176068).invokeInt(new Object[]{hSession, pProfileInfo, phProfile})); - } - - public static void NvAPI_DRS_SetSetting(Pointer hSession, Pointer hProfile, NVDRS_SETTING.ByReference setting) { - checkStatus(getFunction(0x577DD202).invokeInt(new Object[]{hSession, hProfile, setting})); - } - - public static void NvAPI_DRS_DeleteProfile(Pointer hSession, Pointer hProfile) { - checkStatus(getFunction(0x17093206).invokeInt(new Object[]{hSession, hProfile})); - } - - - @Structure.FieldOrder({"version", "settingName", "settingId", "settingType", "settingLocation", "isCurrentPredefined", "isPredefinedValid", "predefinedValue", "currentValue"}) - public static class NVDRS_SETTING extends Structure { - public NativeLong version; - public short[] settingName = new short[2048]; - public NativeLong settingId; - public int settingType; - public int settingLocation; - public NativeLong isCurrentPredefined; - public NativeLong isPredefinedValid; - public DrsSettingValue predefinedValue; - public DrsSettingValue currentValue; - - public NVDRS_SETTING() { - super(); - version = new NativeLong((size() | ((1) << 16))); - } - - @Override - public void read() { - super.read(); - if (settingType == 0) { - predefinedValue.setType(NativeLong.class); - currentValue.setType(NativeLong.class); - } else if (settingType == 1) { - predefinedValue.setType(NVDRS_BINARY_SETTING.class); - currentValue.setType(NVDRS_BINARY_SETTING.class); - } else if (settingType == 3 || settingType == 4) { - predefinedValue.setType(Short[].class); - currentValue.setType(Short[].class); - } - predefinedValue.read(); - currentValue.read(); - } - - @Override - public void write() { - super.write(); - if (settingType == 0) { - predefinedValue.setType(NativeLong.class); - currentValue.setType(NativeLong.class); - } else if (settingType == 1) { - predefinedValue.setType(NVDRS_BINARY_SETTING.class); - currentValue.setType(NVDRS_BINARY_SETTING.class); - } else if (settingType == 3 || settingType == 4) { - predefinedValue.setType(Short.class); - currentValue.setType(Short.class); - } - predefinedValue.write(); - currentValue.write(); - } - - public static class ByReference extends NVDRS_SETTING implements Structure.ByReference { - } - } - - public static class DrsSettingValue extends Union { - public NativeLong u32; - public NVDRS_BINARY_SETTING binary; - public short[] wsz = new short[2048]; - } - - @Structure.FieldOrder({"valueLength", "valueData"}) - public static class NVDRS_BINARY_SETTING extends Structure { - public NativeLong valueLength; - public byte[] valueData = new byte[4096]; - - public static class ByReference extends NVDRS_BINARY_SETTING implements Structure.ByReference { - } - } - - @Structure.FieldOrder({"version", "isPredefined", "appName", "userFriendlyName", "launcher"}) - public static class NVDRS_APPLICATION extends Structure { - public NativeLong version; - public NativeLong isPredefined; - public short[] appName = new short[2048]; - public short[] userFriendlyName = new short[2048]; - public short[] launcher = new short[2048]; - - public NVDRS_APPLICATION() { - super(); - version = new NativeLong((size() | ((1) << 16))); - } - - public static class ByReference extends NVDRS_APPLICATION implements Structure.ByReference { - } - } - - @Structure.FieldOrder({"version", "profileName", "gpuSupport", "isPredefined", "numOfApps", "numOfSettings"}) - public static class NVDRS_PROFILE extends Structure { - public NativeLong version; - public short[] profileName = new short[2048]; - public NativeLong gpuSupport; - - public NativeLong isPredefined; - public NativeLong numOfApps; - public NativeLong numOfSettings; - - public NVDRS_PROFILE() { - super(); - version = new NativeLong((size() | ((1) << 16))); - } - - public static class ByReference extends NVDRS_PROFILE implements Structure.ByReference { - } - } - - private static Function getFunction(int id) { - return Function.getFunction(NVAPI.INSTANCE.nvapi_QueryInterface(id)); - } - - private static void checkStatus(int status) { - if (status != 0) { - byte[] errmsg = new byte[64]; - getFunction(0x6C2D048C).invokeInt(new Object[]{status, errmsg}); - throw new RuntimeException("Failed to execute NVAPI, message: " + Native.toString(errmsg)); - } - } - - private interface NVAPI extends Library { - NVAPI INSTANCE = Native.load("nvapi64", NVAPI.class); - - Pointer nvapi_QueryInterface(int id); - } - - public static String shortArrayToString(short[] array) { - StringBuilder sb = new StringBuilder(); - for (short value : array) { - if (value == 0) { - break; - } - sb.append((char) value); - } - return sb.toString(); - } - - public static void writeStringToShortArray(String str, short[] target) { - char[] strarr = str.toCharArray(); - for (int i = 0; i < strarr.length; i++) { - target[i] = (short) strarr[i]; - } - } -} diff --git a/desktop/src/cn/harryh/arkpets/network/Connections.java b/desktop/src/cn/harryh/arkpets/network/Connections.java index 31d6a117..84bd0077 100644 --- a/desktop/src/cn/harryh/arkpets/network/Connections.java +++ b/desktop/src/cn/harryh/arkpets/network/Connections.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.network; diff --git a/desktop/src/cn/harryh/arkpets/network/SourceStrategy.java b/desktop/src/cn/harryh/arkpets/network/SourceStrategy.java index 3c6dc2dd..3d602376 100644 --- a/desktop/src/cn/harryh/arkpets/network/SourceStrategy.java +++ b/desktop/src/cn/harryh/arkpets/network/SourceStrategy.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.network; diff --git a/desktop/src/cn/harryh/arkpets/network/TrustUtils.java b/desktop/src/cn/harryh/arkpets/network/TrustUtils.java index 34cac7bc..f688f69f 100644 --- a/desktop/src/cn/harryh/arkpets/network/TrustUtils.java +++ b/desktop/src/cn/harryh/arkpets/network/TrustUtils.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.network; diff --git a/desktop/src/cn/harryh/arkpets/network/api/AppQueryAnnouncement.java b/desktop/src/cn/harryh/arkpets/network/api/AppQueryAnnouncement.java index 027f0526..1bdc1803 100644 --- a/desktop/src/cn/harryh/arkpets/network/api/AppQueryAnnouncement.java +++ b/desktop/src/cn/harryh/arkpets/network/api/AppQueryAnnouncement.java @@ -1,11 +1,11 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.network.api; import cn.harryh.arkpets.controllers.AnnounceDialog; import cn.harryh.arkpets.utils.Logger; -import com.alibaba.fastjson.annotation.JSONField; +import com.alibaba.fastjson2.annotation.JSONField; import java.io.Serializable; import java.time.Instant; diff --git a/desktop/src/cn/harryh/arkpets/network/api/AppQueryVersion.java b/desktop/src/cn/harryh/arkpets/network/api/AppQueryVersion.java index 9c565919..97ca0e3d 100644 --- a/desktop/src/cn/harryh/arkpets/network/api/AppQueryVersion.java +++ b/desktop/src/cn/harryh/arkpets/network/api/AppQueryVersion.java @@ -1,10 +1,10 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.network.api; import cn.harryh.arkpets.utils.Version; -import com.alibaba.fastjson.annotation.JSONField; +import com.alibaba.fastjson2.annotation.JSONField; import java.io.Serializable; diff --git a/desktop/src/cn/harryh/arkpets/network/api/BaseModel.java b/desktop/src/cn/harryh/arkpets/network/api/BaseModel.java index 32182fc7..33da3fea 100644 --- a/desktop/src/cn/harryh/arkpets/network/api/BaseModel.java +++ b/desktop/src/cn/harryh/arkpets/network/api/BaseModel.java @@ -1,9 +1,9 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.network.api; -import com.alibaba.fastjson.annotation.JSONField; +import com.alibaba.fastjson2.annotation.JSONField; import java.io.Serializable; diff --git a/desktop/src/cn/harryh/arkpets/network/api/McQueryVersion.java b/desktop/src/cn/harryh/arkpets/network/api/McQueryVersion.java index 9a15c25e..6fc94680 100644 --- a/desktop/src/cn/harryh/arkpets/network/api/McQueryVersion.java +++ b/desktop/src/cn/harryh/arkpets/network/api/McQueryVersion.java @@ -1,10 +1,10 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.network.api; import cn.harryh.arkpets.utils.Version; -import com.alibaba.fastjson.annotation.JSONField; +import com.alibaba.fastjson2.annotation.JSONField; import java.io.Serializable; import java.time.Instant; diff --git a/desktop/src/cn/harryh/arkpets/startup/NullStartupConfig.java b/desktop/src/cn/harryh/arkpets/startup/NullStartupConfig.java index 3c6108c0..c1a7475d 100644 --- a/desktop/src/cn/harryh/arkpets/startup/NullStartupConfig.java +++ b/desktop/src/cn/harryh/arkpets/startup/NullStartupConfig.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2024, Harry Huang, Litwak913 +/** Copyright (c) 2022-2026, Harry Huang, Litwak913 * At GPL-3.0 License */ package cn.harryh.arkpets.startup; diff --git a/desktop/src/cn/harryh/arkpets/startup/StartupConfig.java b/desktop/src/cn/harryh/arkpets/startup/StartupConfig.java index 86223a9e..92db84e3 100644 --- a/desktop/src/cn/harryh/arkpets/startup/StartupConfig.java +++ b/desktop/src/cn/harryh/arkpets/startup/StartupConfig.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2024, Harry Huang, Litwak913 +/** Copyright (c) 2022-2026, Harry Huang, Litwak913 * At GPL-3.0 License */ package cn.harryh.arkpets.startup; diff --git a/desktop/src/cn/harryh/arkpets/startup/WindowsStartupConfig.java b/desktop/src/cn/harryh/arkpets/startup/WindowsStartupConfig.java index 4aeed2f9..4479fb1c 100644 --- a/desktop/src/cn/harryh/arkpets/startup/WindowsStartupConfig.java +++ b/desktop/src/cn/harryh/arkpets/startup/WindowsStartupConfig.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2024, Harry Huang, Litwak913 +/** Copyright (c) 2022-2026, Harry Huang, Litwak913 * At GPL-3.0 License */ package cn.harryh.arkpets.startup; diff --git a/desktop/src/cn/harryh/arkpets/utils/ArgPending.java b/desktop/src/cn/harryh/arkpets/utils/ArgPending.java index f4d969b5..79f3ab8c 100644 --- a/desktop/src/cn/harryh/arkpets/utils/ArgPending.java +++ b/desktop/src/cn/harryh/arkpets/utils/ArgPending.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.utils; diff --git a/desktop/src/cn/harryh/arkpets/utils/DialogComposer.java b/desktop/src/cn/harryh/arkpets/utils/DialogComposer.java index 12c50851..a3eff39e 100644 --- a/desktop/src/cn/harryh/arkpets/utils/DialogComposer.java +++ b/desktop/src/cn/harryh/arkpets/utils/DialogComposer.java @@ -1,10 +1,10 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.utils; import cn.harryh.arkpets.controllers.DialogController; -import com.jfoenix.controls.*; +import com.jfoenix.controls.JFXDialog; import javafx.application.Application; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; diff --git a/desktop/src/cn/harryh/arkpets/utils/FXMLHelper.java b/desktop/src/cn/harryh/arkpets/utils/FXMLHelper.java index 7fa84efe..858b6962 100644 --- a/desktop/src/cn/harryh/arkpets/utils/FXMLHelper.java +++ b/desktop/src/cn/harryh/arkpets/utils/FXMLHelper.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.utils; diff --git a/desktop/src/cn/harryh/arkpets/utils/GuiComponents.java b/desktop/src/cn/harryh/arkpets/utils/GuiComponents.java index 055d93ee..34f76469 100644 --- a/desktop/src/cn/harryh/arkpets/utils/GuiComponents.java +++ b/desktop/src/cn/harryh/arkpets/utils/GuiComponents.java @@ -1,16 +1,18 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.utils; import cn.harryh.arkpets.Const; -import com.jfoenix.controls.*; +import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXTreeTableColumn; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.ScaleTransition; import javafx.animation.Timeline; import javafx.beans.property.DoubleProperty; import javafx.beans.property.DoublePropertyBase; +import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ChangeListener; import javafx.event.ActionEvent; import javafx.event.EventHandler; @@ -32,6 +34,7 @@ import java.io.Serializable; import java.lang.reflect.Method; import java.util.List; +import java.util.function.Function; public class GuiComponents { @@ -317,6 +320,49 @@ private void responseTabHeight(Tab tab) { } + /** A useful tool to create a {@link TreeTableColumn}. + * @param The type of the cell items. + * @param The type of the cell values. + */ + @SuppressWarnings("UnusedReturnValue") + public static class TreeTableColumnSetup { + private final TreeTableColumn column; + + public TreeTableColumnSetup(TreeTableColumn column) { + this.column = column; + } + + public TreeTableColumnSetup() { + this(new JFXTreeTableColumn<>()); + } + + public TreeTableColumnSetup setText(String text) { + column.setText(text); + return this; + } + + public TreeTableColumnSetup setReorderable(boolean reorderable) { + column.setReorderable(reorderable); + return this; + } + + public TreeTableColumnSetup setValueExtractor(Function extractor) { + column.setCellValueFactory(cellDataFeatures -> { + S rawValue = cellDataFeatures.getValue().getValue(); + if (rawValue == null || extractor == null) + return null; + return new SimpleObjectProperty<>(extractor.apply(rawValue)); + }); + return this; + } + + public TreeTableColumnSetup attachTo(TreeTableView treeTableView) { + treeTableView.getColumns().add(column); + return this; + } + } + + public static final class SimpleIntegerSliderSetup extends SliderSetup { public SimpleIntegerSliderSetup(Slider slider) { super(slider); @@ -567,10 +613,10 @@ protected SVGPath getIcon() { abstract public static class HandbookEntrance { private static final double scale = 2.0 / 3; protected final StackPane root; - protected final JFXButton target; + protected final ButtonBase target; protected final Handbook handbook; - public HandbookEntrance(StackPane root, JFXButton target) { + public HandbookEntrance(StackPane root, ButtonBase target) { this.root = root; this.target = target; handbook = getHandbook(); @@ -580,7 +626,8 @@ public HandbookEntrance(StackPane root, JFXButton target) { graphic.setScaleY(scale); target.setText(""); target.setGraphic(graphic); - target.setRipplerFill(Color.GRAY); + if (target instanceof JFXButton jfxButton) + jfxButton.setRipplerFill(Color.GRAY); target.setOnAction(e -> getHandbook().show(root)); } @@ -591,7 +638,7 @@ public HandbookEntrance(StackPane root, JFXButton target) { abstract public static class DangerHandbookEntrance extends HandbookEntrance { - public DangerHandbookEntrance(StackPane root, JFXButton target) { + public DangerHandbookEntrance(StackPane root, ButtonBase target) { super(root, target); refresh(); } @@ -616,7 +663,7 @@ protected SVGPath getIcon() { abstract public static class WarningHandbookEntrance extends HandbookEntrance { - public WarningHandbookEntrance(StackPane root, JFXButton target) { + public WarningHandbookEntrance(StackPane root, ButtonBase target) { super(root, target); refresh(); } @@ -641,7 +688,7 @@ protected SVGPath getIcon() { abstract public static class HelpHandbookEntrance extends HandbookEntrance { - public HelpHandbookEntrance(StackPane root, JFXButton target) { + public HelpHandbookEntrance(StackPane root, ButtonBase target) { super(root, target); } diff --git a/desktop/src/cn/harryh/arkpets/utils/GuiPrefabs.java b/desktop/src/cn/harryh/arkpets/utils/GuiPrefabs.java index 9ec28f31..e114e51e 100644 --- a/desktop/src/cn/harryh/arkpets/utils/GuiPrefabs.java +++ b/desktop/src/cn/harryh/arkpets/utils/GuiPrefabs.java @@ -1,4 +1,4 @@ -/** Copyright (c) 2022-2025, Harry Huang +/** Copyright (c) 2022-2026, Harry Huang * At GPL-3.0 License */ package cn.harryh.arkpets.utils; @@ -7,17 +7,21 @@ import cn.harryh.arkpets.concurrent.ProcessPool; import cn.harryh.arkpets.guitasks.GuiTask; import cn.harryh.arkpets.guitasks.ZipTask; +import cn.harryh.arkpets.natives.ObjCHelper; import cn.harryh.arkpets.network.Connections; import cn.harryh.arkpets.network.api.McQueryVersion; import com.jfoenix.controls.*; +import com.sun.javafx.tk.TKStage; import javafx.animation.FadeTransition; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.Timeline; +import javafx.beans.binding.StringExpression; import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.event.Event; import javafx.event.EventHandler; +import javafx.scene.Group; import javafx.scene.Node; import javafx.scene.control.*; import javafx.scene.control.skin.ScrollPaneSkin; @@ -26,9 +30,12 @@ import javafx.scene.layout.VBox; import javafx.scene.paint.Color; import javafx.scene.shape.SVGPath; +import javafx.scene.shape.Shape; import javafx.scene.text.Text; import javafx.stage.FileChooser; +import javafx.stage.Stage; import javafx.stage.Window; +import javafx.util.Builder; import javafx.util.Duration; import javax.net.ssl.SSLException; @@ -49,16 +56,16 @@ @SuppressWarnings("unused") public class GuiPrefabs { - public static final Color COLOR_INFO = Color.web("#37B"); - public static final Color COLOR_SUCCESS = Color.web("#5B5"); - public static final Color COLOR_WARNING = Color.web("#E93"); - public static final Color COLOR_DANGER = Color.web("#F54"); - public static final Color COLOR_WHITE = Color.web("#FFF"); - public static final Color COLOR_BLACK = Color.web("#000"); - public static final Color COLOR_DARK_GRAY = Color.web("#222"); - public static final Color COLOR_GRAY = Color.web("#444"); + public static final Color COLOR_INFO = Color.web("#37B"); + public static final Color COLOR_SUCCESS = Color.web("#5B5"); + public static final Color COLOR_WARNING = Color.web("#E93"); + public static final Color COLOR_DANGER = Color.web("#F54"); + public static final Color COLOR_WHITE = Color.web("#FFF"); + public static final Color COLOR_BLACK = Color.web("#000"); + public static final Color COLOR_DARK_GRAY = Color.web("#222"); + public static final Color COLOR_GRAY = Color.web("#444"); public static final Color COLOR_LIGHT_GRAY = Color.web("#666"); - public static final Color COLOR_THEME = Color.web("#2A528C"); + public static final Color COLOR_THEME = Color.web("#2A528C"); private static final double BLUR_VALUE = 9.0; @@ -139,7 +146,7 @@ public static void deblurNode(Node node, Duration duration, EventHandler { if (wrapper.isVisible()) GuiPrefabs.fadeOutNode(wrapper, duration, null); @@ -173,6 +180,13 @@ public static void addTooltip(Control control, String content) { control.setTooltip(tooltip); } + public static void addTooltip(Control control, StringExpression content) { + Tooltip tooltip = new Tooltip(); + tooltip.textProperty().bind(content); + tooltip.setStyle(tooltipStyle); + control.setTooltip(tooltip); + } + public static void disableScrollPaneCache(ScrollPane scrollPane) { // https://bugs.openjdk.org/browse/JDK-8211294 // https://github.com/javafxports/openjdk-jfx/issues/225 @@ -186,24 +200,155 @@ public static void disableScrollPaneCache(ScrollPane scrollPane) { } } + public static long getStageNativeHandle(Stage stage) { + try { + Field peerField = Window.class.getDeclaredField("peer"); + peerField.setAccessible(true); + return ((TKStage)peerField.get(stage)).getRawHandle(); + } catch (Exception e) { + return 0L; + } + } + + public static void disableNSWindowRestore(long ptr) { + ObjCHelper.init(); + ObjCHelper.msgSend.invokeVoid(new Object[] { + ptr, + ObjCHelper.sel("setRestorable:"), + 0 + }); + } + + + public static class ButtonBuilder implements Builder