diff --git a/.appveyor.yml b/.appveyor.yml index f8cc9d3..7983117 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -1,33 +1,52 @@ version: '{branch}.{build}' pull_requests: do_not_increment_build_number: true -image: Visual Studio 2017 +skip_non_tags: false platform: x64 -before_build: -- gradlew.bat spotlessApply +image: + - Visual Studio 2017 + - Ubuntu +init: + - cmd: git config --global core.autocrlf true +install: + - sh: cd .. + - sh: mkdir java + - sh: cd java + - sh: wget http://www.nerps.net/jdk10/jdk-10.0.2_linux-x64_bin.tar.gz + - sh: tar zxf jdk-10.0.2_linux-x64_bin.tar.gz + - sh: export JAVA_HOME=$(pwd)/jdk-10.0.2 + - sh: echo java home is $JAVA_HOME + - sh: export PATH=$JAVA_HOME/bin:$PATH + - sh: echo $PATH + - sh: cd ../tokentool + - sh: sudo apt install -y fakeroot + - ./gradlew -version + - java -version build_script: -- gradlew.bat deploy + - ./gradlew build +after_build: + - sh: if [[ $APPVEYOR_REPO_TAG == "true" ]]; then ./gradlew deploy; else echo "Not creating deploy artifacts because this is not a tag build."; fi + - cmd: IF "%APPVEYOR_REPO_TAG%"=="true" (gradlew deploy) ELSE (echo Not creating deploy artifacts because this is not a tag build.) artifacts: -- path: releases\release-*\bundles\tokentool-*.exe - name: TokenTool-Install -- path: releases\release-*\tokentool-*.jar +- path: build\libs\TokenTool-*.jar name: TokenTool-Jar +- path: releases\release-*\TokenTool-*.exe + name: TokenTool-Windows +- path: releases\release-*\tokentool-*.deb + name: TokenTool-Linux deploy: - provider: GitHub - description: Windows release from AppVeyor - tag: $(APPVEYOR_REPO_TAG_NAME) - auth_token: - secure: J2mFPaAOI3z2AOUG28I1mFzTB5eIJ7cyAJpEBHRtRA3s2Zz54IUzKt+MuT3ufw6D - artifact: TokenTool-Install, TokenTool-Jar + description: Release build from AppVeyor + auth_token: $(GITHUB_RELEASE_KEY) + artifact: TokenTool-Jar, TokenTool-Windows, TokenTool-Linux draft: false prerelease: true force_update: false on: - appveyor_repo_tag: true -notifications: -- provider: Webhook - url: https://discordapp.com/api/webhooks/399650806032367616/7biqTSlqVIGCAsqZR1vzq8neErZ2kzmDZ5Y4llL0WFwST073vC24_Btqm6uePNU1DWOC - method: GET - on_build_success: true - on_build_failure: true - on_build_status_changed: true + APPVEYOR_REPO_TAG: true +on_success: + - ps: Invoke-RestMethod $env:APPVEYOR_DISCORD_WEBHOOK_SCRIPT_URL -o send.ps1 + - ps: ./send.ps1 success $env:DISCORD_URL +on_failure: + - ps: Invoke-RestMethod $env:APPVEYOR_DISCORD_WEBHOOK_SCRIPT_URL -o send.ps1 + - ps: ./send.ps1 failure $env:DISCORD_URL diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..28fd4f6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,33 @@ +--- +name: Bug report +about: Create a report to help us improve + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. If macro related, sample macro code '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**TokenTool Info** +- Version: 1.5.? +- Install: New, Upgrade [previous version], or JAR [Java Version] + +**Desktop (please complete the following information):** + - OS: [e.g. Windows, Linux [Ubuntu, Debian, CentOS, etc], MacOS] + - Version [10, 18.04, etc.] + +**Additional context** +Add any other context about the problem here. +You can also attach files (drag and drop here or paste from clipboard) such as log files or screenshots. For large files, paste a file sharing link. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..066b2d9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,17 @@ +--- +name: Feature request +about: Suggest an idea for this project + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/submit-a-question.md b/.github/ISSUE_TEMPLATE/submit-a-question.md new file mode 100644 index 0000000..46bdac2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/submit-a-question.md @@ -0,0 +1,12 @@ +--- +name: Submit a Question +about: Technical Questions + +--- + +**Describe your question** +A clear and concise question regarding this Project, how to contribute, or similar topics. + +*This should NOT be used for general questions on use of TokenTool* +For user support, please post your question on the Forums: http://forums.rptools.net +Or on our Discord channel: [Invite Link](https://discord.gg/2FCwhZ9) diff --git a/.github/move.yml b/.github/move.yml new file mode 100644 index 0000000..1214bac --- /dev/null +++ b/.github/move.yml @@ -0,0 +1,27 @@ +# Configuration for move-issues - https://github.com/dessant/move-issues + +# Delete the command comment when it contains no other content +deleteCommand: true + +# Close the source issue after moving +closeSourceIssue: true + +# Lock the source issue after moving +lockSourceIssue: false + +# Mention issue and comment authors +mentionAuthors: true + +# Preserve mentions in the issue content +keepContentMentions: false + +# Move labels that also exist on the target repository +moveLabels: true + +# Set custom aliases for targets +# aliases: +# r: repo +# or: owner/repo + +# Repository to extend settings from +# _extends: repo diff --git a/.github/no-response.yml b/.github/no-response.yml new file mode 100644 index 0000000..1e480b4 --- /dev/null +++ b/.github/no-response.yml @@ -0,0 +1,13 @@ +# Configuration for probot-no-response - https://github.com/probot/no-response + +# Number of days of inactivity before an Issue is closed for lack of response +daysUntilClose: 90 +# Label requiring a response +responseRequiredLabel: info needed +# Comment to post when closing an Issue for lack of response. Set to `false` to disable +closeComment: > + This issue has been automatically closed because there has been no response + to our request for more information from the original author. With only the + information that is currently in the issue, we don't have enough information + to take action. Please reach out if you have or find the answers we need so + that we can investigate further. diff --git a/.gitignore b/.gitignore index 08605d9..73bbf03 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,3 @@ -# Gradle -################################################################################ -.gradle -/gradle.properties -/gradlew.bat -/gradlew - # OS generated files ################################################################################ .DS_Store @@ -16,29 +9,37 @@ Icon? ehthumbs.db Thumbs.db + +# Others +################################################################################ +.gradle +build/ +/bin/ +target/ +out/ +/releases/ +*.log +*~ +\${sys:appHome}/ + + +# Automatically Generated +################################################################################ +package/windows/TokenTool.iss +src/main/resources/sentry.properties + + # IDEs ################################################################################ *.iml -.idea/* +.idea/ .*.sw[p0-9] .sw[p0-9] .project +.settings/ .classpath .history workbench.xmi # Keystore -build-resources/rptools-keystore - -# Others -################################################################################ -*.log -*~ -target/* -build/* -out/* -releases/* -bin/* -sentry-events/* -/bin/ -/build/ +build-resources/rptools-keystore \ No newline at end of file diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs deleted file mode 100644 index d5a336f..0000000 --- a/.settings/org.eclipse.jdt.core.prefs +++ /dev/null @@ -1,13 +0,0 @@ -# -#Thu Nov 16 11:29:57 CST 2017 -org.eclipse.jdt.core.compiler.debug.localVariable=generate -org.eclipse.jdt.core.compiler.compliance=1.8 -org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve -org.eclipse.jdt.core.compiler.debug.sourceFile=generate -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.debug.lineNumber=generate -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.source=1.8 -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error diff --git a/.travis.yml b/.travis.yml index dcfb398..f8875bb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,33 +2,24 @@ language: java sudo: false matrix: include: - - os: linux - jdk: oraclejdk8 - os: osx - osx_image: xcode8.3 -install: true -addons: - apt: - packages: - - oracle-java8-installer - - fakeroot + osx_image: xcode10 script: - ./gradlew build -before_cache: - - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock - - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ -cache: - directories: - - "$HOME/.gradle/caches/" - - "$HOME/.gradle/wrapper/" before_deploy: + # Install Sentry.io CLI + - curl -sL https://sentry.io/get-cli/ | bash + - sentry-cli info + - echo Version is $TRAVIS_TAG + # Create a Sentry release + - sentry-cli releases new "$TRAVIS_TAG" + # Create Deploy Artifacts - ./gradlew deploy deploy: provider: releases - api_key: - secure: hTBFlftPw61rqEx5+4z+VUHxDWi6l9wcIKQFipLnVHlLeQ1neW1pZ8y7WBvXUWCtZ3/D0iTVJpMlWtpWnu6OoMDbFRAOfy2/2kU7bGUDGoH0QGGwrXjAGhs7GY3s+6eu1/uRWUsZ3Ys/vrBrJxyADGThdhD0QxwQ7SuuwM0ERvD94JuyjfvCFZ6xvOjccYdzilmxC4cRvOrBqv1PHTOLkPoQcXsP/FqUdzUAXrud5iUFw8+4el6kLZT/vmbIE8EdpocAKca8QOjf1AMWCPnr5JwKU+n2PUpEqI1WtFA9PatjR2M1e0yvQFWT0mb/rcfZ6LJEPb/s6cFlHCa0aosgV+aj6tN6K81zLj65L+Vv6F4dJNqW40wkBXRfcen0SR4duLlKx5jSHm3XA6Q5b9aI8+XL12cmFZoKYG+yuVgCYAx/qxHBs6es/wMaGkNrxataKsfzwm4Dw8QGKu+115bsxEywFhEbQfFYvClP9/WFEgrdBdP2bplLpFSGDgv2n6DmJq083CV7bJmxgoqhAhjCry/UL9DG0WFtE1FqWsoxzAUoklDxAE0zgW9D66Xucp5dthuVJbzMpxUiR5zRf8npz/NaRAH7EK1ROl9ue67mtuUobMN7+lnG3pszyxTT/y7kqy0OWFvaGnXJ4UMPbBnXXYdFKc+rSO86/4aWWVE3lwQ= + api_key: $GITHUB_RELEASE_KEY file_glob: true - file: releases/release-*/bundles/* + file: releases/release-*/* overwrite: true skip_cleanup: true target_commitish: $TRAVIS_COMMIT @@ -36,6 +27,21 @@ deploy: draft: false prerelease: true on: - repo: RPTools/tokentool + repo: $REPO tags: true all_branches: true +after_deploy: + # Finalize Sentry release + - sentry-cli releases finalize "$TRAVIS_TAG" + # Associate commits with the Sentry release + - sentry-cli releases set-commits "$TRAVIS_TAG" --auto + # Tell Sentry.io we have deployed a release + - sentry-cli releases deploys "$TRAVIS_TAG" new -e Production +after_success: + - wget $TRAVIS_DISCORD_WEBHOOK_SCRIPT_URL + - chmod +x send.sh + - ./send.sh success $DISCORD_URL +after_failure: + - wget $TRAVIS_DISCORD_WEBHOOK_SCRIPT_URL + - chmod +x send.sh + - ./send.sh failure $DISCORD_URL \ No newline at end of file diff --git a/CHANGE_LOG.md b/CHANGE_LOG.md new file mode 100644 index 0000000..635c218 --- /dev/null +++ b/CHANGE_LOG.md @@ -0,0 +1,40 @@ +TokenTool 2.1 +===== +You can now add background images, extract images from PDF files, and save the portrait alongside the token using the same background image and/or colors! Several bug fixes and requested enhancements have also been added. + + +Enhancements +----- +* [#7][i7] - *Add PDF Extraction*. Open and extract images straight from the module! Using the File -> Open PDF menu or dragging a PDF to the main window, you can open and view and PDF. The individual images are shown to the right and you can either drag them to the main window or left-click with the mouse to use the image as the Portrait or right-click with the mouse to use the image as the background. +* [#10][i10] - *Add arrow key nudges*. You can use the arrow keys as well as the number pad to nudge the images 1 pixel at a time! Be sure you have the layer selected that you want to move. +* [#12][i12] - *Update CI config and install to Java 10*. The packaged JRE has been bumped up to Oracle JDK 10 and is now the required JRE if launching the JAR manually. +* [#13][i13] - *Allow user supplied image for background*. In addition to background color, you can now supply a background image! When dragging images to the main window, drop zones will highlight and the "Layer" button in the upper right will tell you if you are setting the Portrait or Background image. Several options have been added to support the use of backgrounds as well. +* [#14][i14] - *Add a keyboard shortcut to take a screen capture*. Several keyboard shortcuts as well as Accelerators have been added to the various menu items. +* [#15][i15] - *The screen capture box could retain its size*. The screen capture window now remembers its size! In fact, all the windows now remember their size and position! +* [#19][i19] - *Add ESC key to cancel Screen Capture*. Didn't mean to capture that screen shot? Pressing ESC key will now cancel the action and close the window. +* Misc - A new menu option under Help -> Reset Settings will restore all settings to their defaults. +* Misc - A few new overlays were added, enjoy! +* Misc - If you unlock the aspect ratio (pressing the padlock on the Overlay Options), you can now set the token size to any integer. +* Misc - The overlay's original width x height is now displayed under the Overlays name on the Overlay Options panel +* Misc - Save options were expanded to allow you to save the Portrait used (with or without Background color/image) alongside the token. This is useful if you grabbed the image from a URL or PDF and forgot or couldn't save it. Also, if you save with Background Options, it will save the portrait as a .jpg instead of a .png (which are typically 10x smaller in disk space) so you can use that high quality PNG from the PDF to create a token but use a smaller jpg for the Portrait (say, in MapTool) where transparency is probably not needed or wanted. + + +Bug Fixes +----- +* [#8][i8] - *Bug in pdf-extract-feature branch*. This has been squashed! +* [#9][i9] - *Pog filenames substitute %20 for spaces*. Token names are now properly unescaped from URL's. No more %20's! +* [#11][i11] - *Background color layer not on the bottom*. Another bug squashed! +* [#17][i17] - *Imported PNG overlays coming in at 100x100*. Squashed this one too! + + +[i7]: https://github.com/JamzTheMan/TokenTool/issues/7 +[i8]: https://github.com/JamzTheMan/TokenTool/issues/8 +[i9]: https://github.com/JamzTheMan/TokenTool/issues/9 +[i10]: https://github.com/JamzTheMan/TokenTool/issues/10 +[i11]: https://github.com/JamzTheMan/TokenTool/issues/11 +[i12]: https://github.com/JamzTheMan/TokenTool/issues/12 +[i13]: https://github.com/JamzTheMan/TokenTool/issues/13 +[i14]: https://github.com/JamzTheMan/TokenTool/issues/14 +[i15]: https://github.com/JamzTheMan/TokenTool/issues/15 +[i17]: https://github.com/JamzTheMan/TokenTool/issues/17 +[i19]: https://github.com/JamzTheMan/TokenTool/issues/19 diff --git a/CNAME b/CNAME index fa642f1..3c5732b 100644 --- a/CNAME +++ b/CNAME @@ -1 +1 @@ -tokentool.nerps.net \ No newline at end of file +tokentool.rptools.net diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..794dd9f --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at admin@rptools.net. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/README.md b/README.md index 0ee021a..ffd45e7 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ -[![Build Status](https://travis-ci.org/RPTools/tokentool.svg?branch=ci-fixes)](https://travis-ci.org/RPTools/tokentool) -[![Build status](https://ci.appveyor.com/api/projects/status/v0fve3b6uj3r8gwq?svg=true)](https://ci.appveyor.com/project/cwisniew/tokentool) +[![Build Status](https://travis-ci.org/RPTools/tokentool.svg?branch=master)](https://travis-ci.org/RPTools/TokenTool) +[![Build status](https://ci.appveyor.com/api/projects/status/f2dpp5xfnotbhgcy/branch/master?svg=true)](https://ci.appveyor.com/project/rptools-automation/tokentool/branch/master) -# TokenTool 2.0 +# TokenTool 2.1 A Token creation tool to create png images suitable for Virtual Table Tops. -Rewritten in JavaFX 8 to bring a modern and updated UI as well as a few new features! TokenTool 2.0 now supports PSD format for 'overlays' to allow for better masking by supporting 'layers'. To create your own 'overlay' you can use any version of PhotoShop or GIMP and add your masking layer as the first (bottom) layer and your overlay image as the second (top) layer. +Rewritten in JavaFX 10 to bring a modern and updated UI as well as a few new features! TokenTool 2.1 now supports PSD format for 'overlays' to allow for better masking by supporting 'layers'. To create your own 'overlay' you can use any version of PhotoShop or GIMP and add your masking layer as the first (bottom) layer and your overlay image as the second (top) layer. + +You can add background images, extract images from PDF files, and save the portrait alongside the token using the same background image and/or colors! Several bug fixes and requested enhancements have also been added since 2.0. diff --git a/build-resources/TokenTool.iss.template b/build-resources/TokenTool.iss.template new file mode 100644 index 0000000..af84202 --- /dev/null +++ b/build-resources/TokenTool.iss.template @@ -0,0 +1,77 @@ +;This file will be executed next to the application bundle image +;I.e. current directory will contain folder TokenTool with application files +[Setup] +AppId={{net.rptools.tokentool}} +AppName=${AppName} +AppVersion=${AppVersion} +AppVerName=${AppName} ${AppVersion} +AppPublisher=${Vendor} +AppComments=${AppName} by ${Vendor} +AppCopyright=Copyright (C) 2018 +AppPublisherURL=http://www.rptools.net/ +AppSupportURL=http://forums.rptools.net/viewtopic.php?f=60&t=23681 +;AppUpdatesURL=http://java.com/ +DefaultDirName={localappdata}/${AppName} +DisableStartupPrompt=Yes +DisableDirPage=No +DisableProgramGroupPage=Yes +DisableReadyPage=Yes +DisableFinishedPage=Yes +DisableWelcomePage=no +DefaultGroupName=${Vendor} +;Optional License +LicenseFile=COPYING.AFFERO +;WinXP or above +MinVersion=0,5.1 +OutputBaseFilename=${AppName}-${AppVersion} +Compression=lzma +SolidCompression=yes +PrivilegesRequired=lowest +SetupIconFile=${SetupIcon} +UninstallDisplayIcon={app}/${AppName}.ico +UninstallDisplayName=${AppName} +WizardImageStretch=Yes +WizardSmallImageFile=${AppName}-setup-icon.bmp +WizardImageFile=${WizardImage} +ArchitecturesInstallIn64BitMode=x64 + + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl" + +[Files] +Source: "TokenTool/TokenTool.exe"; DestDir: "{app}"; Flags: ignoreversion +Source: "TokenTool/*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs + +[Icons] +Name: "{group}${Slash}TokenTool"; Filename: "{app}/TokenTool.exe"; IconFilename: "{app}/TokenTool.ico"; Check: returnTrue() +Name: "{commondesktop}${Slash}TokenTool"; Filename: "{app}/TokenTool.exe"; IconFilename: "{app}/TokenTool.ico"; Check: returnTrue() + + +[Run] +Filename: "{app}/TokenTool.exe"; Parameters: "-Xappcds:generatecache"; Check: returnFalse() +Filename: "{app}/TokenTool.exe"; Description: "{cm:LaunchProgram,TokenTool}"; Flags: nowait postinstall skipifsilent; Check: returnTrue() +Filename: "{app}/TokenTool.exe"; Parameters: "-install -svcName ""TokenTool"" -svcDesc ""TokenTool"" -mainExe ""TokenTool.exe"" "; Check: returnFalse() + +[UninstallRun] +Filename: "{app}/TokenTool.exe "; Parameters: "-uninstall -svcName TokenTool -stopOnUninstall"; Check: returnFalse() + +[Code] +function returnTrue(): Boolean; +begin + Result := True; +end; + +function returnFalse(): Boolean; +begin + Result := False; +end; + +function InitializeSetup(): Boolean; +begin +// Possible future improvements: +// if version less or same => just launch app +// if upgrade => check if same app is running and wait for it to exit +// Add pack200/unpack200 support? + Result := True; +end; diff --git a/build-resources/eclipse.prefs.formatter.xml b/build-resources/eclipse.prefs.formatter.xml deleted file mode 100644 index 382983a..0000000 --- a/build-resources/eclipse.prefs.formatter.xml +++ /dev/null @@ -1,314 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/build-resources/sentry.properties.template b/build-resources/sentry.properties.template new file mode 100644 index 0000000..8487d03 --- /dev/null +++ b/build-resources/sentry.properties.template @@ -0,0 +1,4 @@ +dsn=${SentryDSN} +environment=${Environment} +stacktrace.app.packages=net.rptools.tokentool +release=${AppVersion} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 9450e16..9dfe9be 100644 --- a/build.gradle +++ b/build.gradle @@ -6,14 +6,19 @@ import org.gradle.plugins.ide.eclipse.model.AccessRule buildscript { - dependencies { - classpath "com.diffplug.spotless:spotless-plugin-gradle:3.7.0" - } - - repositories { - jcenter() - mavenCentral() - } + dependencies { + classpath "com.diffplug.spotless:spotless-plugin-gradle:3.13.0" + } + + repositories { + jcenter() + mavenCentral() + } +} + +// Access Git info from build script +plugins { + id "org.ajoberstar.grgit" version "3.0.0" } // Apply the java plugin to add support for Java @@ -25,8 +30,8 @@ apply plugin: 'com.diffplug.gradle.spotless' // Definitions defaultTasks 'clean', 'build' -sourceCompatibility = 1.8 -targetCompatibility = 1.8 +sourceCompatibility = 10 +targetCompatibility = 10 // Don't fail build if tests fail until tests are cleaned up test.ignoreFailures = true @@ -34,170 +39,232 @@ test.ignoreFailures = true // Used by gradle assemble & run tasks mainClassName = 'net.rptools.tokentool.client.TokenTool' -// Current Build version -version = '2.0' - // Custom properties ext { - vendor = "RPTools" // Change to RPTools or "" for official builds - preloaderClassName = 'net.rptools.tokentool.client.SplashScreenLoader' - msiVersion = '2.0.0' + def repo = org.ajoberstar.grgit.Grgit.open(currentDir: file('.')) + def head = repo.head() + def tags = repo.tag.list().find { + it.commit == head + } + + revision = head.abbreviatedId + revisionFull = head.id + + if (tags) { + tagVersion = tags.getName() + msiVersion = tagVersion + enviroment = "Production" + sentryDSN = sentry_production_dsn + } else { + tagVersion = 'SNAPSHOT-' + revision + enviroment = "Development" + sentryDSN = sentry_development_dsn + } + + // vendor, tagVersion, msiVersion, and DSN's defaults are set in gradle.properties + println 'Configuring for ' + project.name + " " + tagVersion + " by " + vendor } // Default parameters for gradle run command run { - args = [ '-v='+version, '-vendor='+vendor ] - applicationDefaultJvmArgs = [ "-Djavafx.preloader=" + preloaderClassName, "-Dsentry.environment=Development" ] + args = [ '-v='+tagVersion, '-vendor='+vendor ] + applicationDefaultJvmArgs = [ "-Dsentry.environment=Development", "-Dfile.encoding=UTF-8" ] - if(System.getProperty("exec.args") != null) { - args System.getProperty("exec.args").split() - } + if(System.getProperty("exec.args") != null) { + args System.getProperty("exec.args").split() + } } spotless { - java { - licenseHeaderFile 'spotless.license.java' - eclipse().configFile('build-resources/eclipse.prefs.formatter.xml') - removeUnusedImports() - } - - format 'misc', { - target '**/*.gradle', '**/.gitignore' - - // spotless has built-in rules for most basic formatting tasks - trimTrailingWhitespace() - // or spaces. Takes an integer argument if you don't like 4 - indentWithTabs() - } + java { + licenseHeaderFile 'spotless.license.java' + googleJavaFormat() + } + + format 'misc', { + target '**/*.gradle', '**/.gitignore' + + // spotless has built-in rules for most basic formatting tasks + trimTrailingWhitespace() + // or spaces. Takes an integer argument if you don't like 4 + indentWithSpaces(4) + } } // Set eclipse natures, access rules, and other settings // https://docs.gradle.org/current/dsl/org.gradle.plugins.ide.eclipse.model.EclipseProject.html eclipse { - project { - natures 'org.eclipse.buildship.core.gradleprojectnature' - buildCommand 'org.eclipse.buildship.core.gradleprojectbuilder' - } - - classpath { - file { - withXml { - def node = it.asNode() - node.appendNode('classpathentry', [kind: 'con', path: 'org.eclipse.fx.ide.jdt.core.JAVAFX_CONTAINER']) - } - - whenMerged { - def jre = entries.find { it.path.contains 'org.eclipse.jdt.launching.JRE_CONTAINER' } - jre.accessRules.add(new AccessRule('0', 'javafx/**')) - } - } - } + project { + natures 'org.eclipse.buildship.core.gradleprojectnature' + buildCommand 'org.eclipse.buildship.core.gradleprojectbuilder' + } + + classpath { + file { + withXml { + def node = it.asNode() + node.appendNode('classpathentry', [kind: 'con', path: 'org.eclipse.fx.ide.jdt.core.JAVAFX_CONTAINER']) + } + + whenMerged { + def jre = entries.find { it.path.contains 'org.eclipse.jdt.launching.JRE_CONTAINER' } + jre.accessRules.add(new AccessRule('0', 'javafx/**')) + } + } + } } // In this section you declare where to find the dependencies of your project repositories { - jcenter() - mavenCentral() - maven { url = 'http://maptool.craigs-stuff.net/repo/' } - maven { url = 'http://www.nerps.net/repo/' } - mavenLocal() + mavenLocal() + mavenCentral() + jcenter() } // In this section you declare the dependencies for your production and test code dependencies { - // For Sentry bug reporting - compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.8.2' - compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.8.2' - compile group: 'org.apache.logging.log4j', name: 'log4j-1.2-api', version: '2.8.2' // Bridges v1 to v2 for other code in other libs - - compile group: 'org.slf4j', name: 'slf4j-simple', version: '1.7.25' - compile group: 'commons-logging', name: 'commons-logging', version: '1.2' + // For Sentry bug reporting + annotationProcessor group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.11.0' + implementation group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.11.0' + implementation group: 'org.apache.logging.log4j', name: 'log4j-1.2-api', version: '2.11.0' // Bridges v1 to v2 for other code in other libs + + implementation group: 'org.slf4j', name: 'slf4j-simple', version: '1.7.25' + implementation group: 'commons-logging', name: 'commons-logging', version: '1.2' + + implementation group: 'io.sentry', name: 'sentry', version: '1.7.5' + implementation group: 'io.sentry', name: 'sentry-log4j2', version: '1.7.5' + + // For PDF image extraction + implementation 'org.apache.pdfbox:pdfbox:2.0.10' + implementation 'org.bouncycastle:bcmail-jdk15on:1.59' // To decrypt passworded/secured pdf's + implementation 'com.github.jai-imageio:jai-imageio-core:1.4.0' // For pdf image extraction, specifically for jpeg2000 (jpx) support. + implementation 'com.github.jai-imageio:jai-imageio-jpeg2000:1.3.0' // For pdf image extraction, specifically for jpeg2000 (jpx) support. + + // Image processing lib + implementation group: 'com.twelvemonkeys.imageio', name: 'imageio-core', version: '3.3.2' // https://mvnrepository.com/artifact/com.twelvemonkeys.imageio/imageio-core + implementation group: 'com.twelvemonkeys.imageio', name: 'imageio-jpeg', version: '3.3.2' // https://mvnrepository.com/artifact/com.twelvemonkeys.imageio/imageio-core + implementation group: 'com.twelvemonkeys.imageio', name: 'imageio-psd', version: '3.3.2' // https://mvnrepository.com/artifact/com.twelvemonkeys.imageio/imageio-psd + + // Other public libs + implementation group: 'commons-io', name: 'commons-io', version: '2.6' // https://mvnrepository.com/artifact/commons-io/commons-io + implementation group: 'commons-cli', name: 'commons-cli', version: '1.4' // https://mvnrepository.com/artifact/commons-cli/commons-cli + implementation group: 'org.reflections', name: 'reflections', version: '0.9.10' // https://mvnrepository.com/artifact/commons-cli/commons-cli + implementation group: 'com.google.code.gson', name: 'gson', version: '2.8.5' // https://mvnrepository.com/artifact/com.google.code.gson/gson +} - compile group: 'io.sentry', name: 'sentry', version: '1.6.1' - compile group: 'io.sentry', name: 'sentry-log4j2', version: '1.6.1' +task configSentryRelease(type: Copy) { + from("build-resources/sentry.properties.template") + into("src/main/resources/") + rename("sentry.properties.template", "sentry.properties") + def tokens = [ + AppVersion: "${tagVersion}", + Environment: "${enviroment}", + SentryDSN: "${sentryDSN}" + ] + expand(tokens) + inputs.properties(tokens) +} - // Image processing lib - compile group: 'com.twelvemonkeys.imageio', name: 'imageio-core', version: '3.3.2' // https://mvnrepository.com/artifact/com.twelvemonkeys.imageio/imageio-core - compile group: 'com.twelvemonkeys.imageio', name: 'imageio-psd', version: '3.3.2' // https://mvnrepository.com/artifact/com.twelvemonkeys.imageio/imageio-psd +task uberJar(type: Jar) { + group = 'distribution' + description = 'Create uber jar for native installers' + + baseName project.name + '-' + tagVersion + + manifest { + attributes 'Implementation-Title': project.name, + 'Implementation-Version': tagVersion, + 'Implementation-Vendor': vendor, + 'Git-Commit': revision, + 'Git-Commit-SHA': revisionFull, + 'Built-By': System.getProperty('user.name'), + 'Built-Date': new Date(), + 'Built-JDK': System.getProperty('java.version'), + 'Source-Compatibility': project.sourceCompatibility, + 'Target-Compatibility': project.targetCompatibility, + 'Main-Class': project.mainClassName + } + + from { + configurations.runtimeClasspath.collect { + it.isDirectory() ? it : zipTree(it) + } + } + with jar + exclude 'META-INF/*.RSA', 'META-INF/*.SF','META-INF/*.DSA' +} - // Other public libs - compile group: 'commons-io', name: 'commons-io', version: '2.6' // https://mvnrepository.com/artifact/commons-io/commons-io - compile group: 'commons-cli', name: 'commons-cli', version: '1.4' // https://mvnrepository.com/artifact/commons-cli/commons-cli - compile group: 'org.reflections', name: 'reflections', version: '0.9.10' // https://mvnrepository.com/artifact/commons-cli/commons-cli +// Currently includes license files +task copyPackageExtras(type: Copy) { + from('package/license/') + into('build/libs/') + include('*') } -task uberJar(type: Jar) { - group = 'distribution' - description = 'Create uber jar for native installers' - - manifest { - attributes 'Implementation-Title': project.name, - 'Implementation-Version': version, - 'Implementation-Vendor': vendor, - 'Built-By': System.getProperty('user.name'), - 'Built-Date': new Date(), - 'Built-JDK': System.getProperty('java.version'), - 'Source-Compatibility': project.sourceCompatibility, - 'Target-Compatibility': project.targetCompatibility, - 'Main-Class': project.mainClassName, - 'JavaFX-Preloader-Class' : preloaderClassName - } - - from { - configurations.compile.collect { - it.isDirectory() ? it : zipTree(it) - } - } - with jar - exclude 'META-INF/*.RSA', 'META-INF/*.SF','META-INF/*.DSA' +task prepareInnoSetup(type: Copy) { + from("build-resources/TokenTool.iss.template") + into("package/windows/") + rename("TokenTool.iss.template", "TokenTool.iss") + def tokens = [ + AppName: "${project.name}", + AppVersion: "${tagVersion}", + Vendor: "${vendor}", + SetupIcon: "${project.projectDir.absolutePath}/package/windows/${project.name}.ico", + WizardImage: "${project.projectDir.absolutePath}/package/windows/${project.name}-setup.bmp", + Slash: "\\", + ] + expand(tokens) + inputs.properties(tokens) } -task copyLicense(type: Copy) { - from('license') - into('build/libs/') - include('*') +task deploy(dependsOn: [uberJar, copyPackageExtras, prepareInnoSetup]) { + group = 'distribution' + description = 'Create native installers' + + tasks.findByName('copyPackageExtras').mustRunAfter 'uberJar' + + doLast { + println 'Deploying ' + project.name + " " + tagVersion + " by " + vendor + + // Using the -deploy Command with Bundler Arguments + // javapackager -deploy -native exe -BsystemWide=true -BjvmOptions=-Xmx128m -BjvmOptions=-Xms128m -outdir packages -outfile BrickBreaker -srcdir dist + // -srcfiles BrickBreaker.jar -appclass brickbreaker.Main -name BrickBreaker -title "BrickBreaker demo" + // *Note: You can specify a JRE using "-Bruntime=../../../deploy-ready-jre" + // It will bundle system/workspace JDK by default + def javapackager_deploy = exec { + workingDir "${project.projectDir.absolutePath}" + + commandLine "javapackager", + "-deploy", "-v", + "-native", "installer", + "-appclass", mainClassName, + "-srcdir", "build/libs", + "-outdir", "releases/release-"+tagVersion, + "-outfile", project.name, + "-name", project.name, + "-description", project.name + " " + tagVersion + " by " + vendor, + "-title", project.name, + "-vendor", vendor, + "-BdropinResourcesRoot=.", + "-BinstalldirChooser=true", + "-BsystemWide=false", + "-BmenuHint=true", + "-Bwin.menuGroup=" + vendor, + "-BshortcutHint=true", + "-BappVersion=" + tagVersion, + "-Bwin.msi.productVersion=" + msiVersion, + "-BlicenseFile=COPYING.AFFERO", + "-BlicenseType='GNU AFFERO GENERAL PUBLIC LICENSE'", + "-Bcategory=Graphics", + "-Bemail=tokentool@rptools.net" + } + } } -task deploy(dependsOn: [uberJar, copyLicense]) { - group = 'distribution' - description = 'Create native installers' - - tasks.findByName('copyLicense').mustRunAfter 'uberJar' - - doLast { - // Using the -deploy Command with Bundler Arguments - // javapackager -deploy -native exe -BsystemWide=true -BjvmOptions=-Xmx128m -BjvmOptions=-Xms128m -outdir packages -outfile BrickBreaker -srcdir dist - // -srcfiles BrickBreaker.jar -appclass brickbreaker.Main -name BrickBreaker -title "BrickBreaker demo" - // *Note: You can specify a JRE using "-Bruntime=../../../deploy-ready-jre" - // It will bundle system/workspace JDK by default - def javapackager_deploy = exec { - workingDir "${project.projectDir.absolutePath}" - - commandLine "javapackager", - "-deploy", "-v", - "-native", "installer", - "-appclass", mainClassName, - "-preloader", preloaderClassName, - "-srcdir", "build/libs", - "-srcfiles", jar.archiveName, - "-srcfiles", "COPYING.AFFERO", - "-outdir", "releases/release-"+version, - "-outfile", project.name, - "-name", project.name, - "-title", project.name, - "-vendor", vendor, - "-BinstalldirChooser=true", - "-BsystemWide=false", - "-BmenuHint=true", - "-Bwin.menuGroup=" + vendor, - "-BshortcutHint=true", - "-BappVersion=" + version, - "-Bwin.msi.productVersion=" + msiVersion, - "-BlicenseFile=COPYING.AFFERO", - "-BlicenseType=GNU AFFERO GENERAL PUBLIC LICENSE", - "-Bcategory=Graphics", - "-Bemail=tokentool@" + vendor + ".net" - } - } +task createWrapper(type: Wrapper) { + gradleVersion = '5.2.1' } + +// Configure current release tag in Sentry.io properties +processResources.dependsOn configSentryRelease diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..bd8d6c0 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,11 @@ +# Change vendor RPTools for official builds, or used for things like BETA builds +# Should change this just once for a given Repo/Fork +vendor=rptools + +# Versioning is controlled via git tag for that commit. For developing you can pass a -version=2.1 -vendor=Nerps if needed +tagVersion=0.0.0 +msiVersion=0.0.0 + +sentry_development_dsn=http://dontlogme:indevelopment@localhost/1234 +sentry_staging_dsn=https://312e480e6390426095b0a66a11f343a9@sentry.io/1406000 +sentry_production_dsn=https://312e480e6390426095b0a66a11f343a9@sentry.io/1406000 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 913f13b..87b738c 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index cb586cb..44e7c4d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Thu Nov 09 14:16:59 CST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.0.2-bin.zip diff --git a/gradlew b/gradlew index cccdd3d..af6708f 100755 --- a/gradlew +++ b/gradlew @@ -28,7 +28,7 @@ APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +DEFAULT_JVM_OPTS='"-Xmx64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" diff --git a/gradlew.bat b/gradlew.bat index f955316..6d57edc 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= +set DEFAULT_JVM_OPTS="-Xmx64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome diff --git a/other-resources/DevTestBuild.png b/other-resources/DevTestBuild.png new file mode 100644 index 0000000..62101dd Binary files /dev/null and b/other-resources/DevTestBuild.png differ diff --git a/other-resources/RPTools_Logo_403-_no_shadow.png b/other-resources/RPTools_Logo_403-_no_shadow.png new file mode 100644 index 0000000..1520454 Binary files /dev/null and b/other-resources/RPTools_Logo_403-_no_shadow.png differ diff --git a/other-resources/TokenTool Splash Icon.psd b/other-resources/TokenTool Splash Icon.psd new file mode 100644 index 0000000..1d4af36 Binary files /dev/null and b/other-resources/TokenTool Splash Icon.psd differ diff --git a/other-resources/TokenTool_setup-mac-image.psd b/other-resources/TokenTool_setup-mac-image.psd new file mode 100644 index 0000000..48917e2 Binary files /dev/null and b/other-resources/TokenTool_setup-mac-image.psd differ diff --git a/other-resources/TokenTool_splash.psd b/other-resources/TokenTool_splash.psd index bf0d1a5..2bfc6d1 100644 Binary files a/other-resources/TokenTool_splash.psd and b/other-resources/TokenTool_splash.psd differ diff --git a/other-resources/maptool_setup-mac--image.psd b/other-resources/maptool_setup-mac--image.psd new file mode 100644 index 0000000..64b09ce Binary files /dev/null and b/other-resources/maptool_setup-mac--image.psd differ diff --git a/other-resources/toikentool_setup-image.psd b/other-resources/toikentool_setup-image.psd new file mode 100644 index 0000000..232af18 Binary files /dev/null and b/other-resources/toikentool_setup-image.psd differ diff --git a/license/COPYING.AFFERO b/package/license/COPYING.AFFERO similarity index 100% rename from license/COPYING.AFFERO rename to package/license/COPYING.AFFERO diff --git a/license/COPYING.LESSER b/package/license/COPYING.LESSER similarity index 100% rename from license/COPYING.LESSER rename to package/license/COPYING.LESSER diff --git a/package/macosx/TokenTool-background.png b/package/macosx/TokenTool-background.png new file mode 100644 index 0000000..f02fa55 Binary files /dev/null and b/package/macosx/TokenTool-background.png differ diff --git a/package/windows/TokenTool-setup.bmp b/package/windows/TokenTool-setup.bmp new file mode 100644 index 0000000..7793481 Binary files /dev/null and b/package/windows/TokenTool-setup.bmp differ diff --git a/package/windows/tokentool-setup-icon.bmp.bmp b/package/windows/tokentool-setup-icon.bmp.bmp deleted file mode 100644 index b67b8f9..0000000 Binary files a/package/windows/tokentool-setup-icon.bmp.bmp and /dev/null differ diff --git a/settings.gradle b/settings.gradle index ed25a63..0d3a458 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,18 +1 @@ -/* -* This settings file was generated by the Gradle 'init' task. -* -* The settings file is used to specify which projects to include in your build. -* In a single project build this file can be empty or even removed. -* -* Detailed information about configuring a multi-project build in Gradle can be found -* in the user guide at https://docs.gradle.org/4.1/userguide/multi_project_builds.html -*/ - -/* -// To declare projects as part of a multi-project build use the 'include' method -include 'shared' -include 'api' -include 'services:webservice' -*/ - -rootProject.name = 'tokentool' +rootProject.name = 'TokenTool' \ No newline at end of file diff --git a/src/main/java/net/rptools/tokentool/AppConstants.java b/src/main/java/net/rptools/tokentool/AppConstants.java index 3c88245..c826a2b 100644 --- a/src/main/java/net/rptools/tokentool/AppConstants.java +++ b/src/main/java/net/rptools/tokentool/AppConstants.java @@ -1,59 +1,99 @@ /* - * This software Copyright by the RPTools.net development team, and licensed under the Affero GPL Version 3 or, at your option, any later version. + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. * - * TokenTool Source Code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * TokenTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * - * You should have received a copy of the GNU Affero General Public License * along with this source Code. If not, please visit and specifically the Affero license text - * at . + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . */ package net.rptools.tokentool; import java.io.File; - +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; import javafx.stage.FileChooser.ExtensionFilter; public class AppConstants { - public static final String APP_NAME = "TokenTool"; - - public static final String TOKEN_TOOL_ICON = "/net/rptools/tokentool/image/token_tool_icon.png"; - public static final String REGION_SELECTOR_ICON = "/net/rptools/tokentool/image/region_selector_icon.png"; - - public static final String TOKEN_TOOL_BUNDLE = "net.rptools.tokentool.i18n.TokenTool"; - - public static final String TOKEN_TOOL_FXML = "/net/rptools/tokentool/view/TokenTool.fxml"; - public static final String MANAGE_OVERLAYS_FXML = "/net/rptools/tokentool/view/ManageOverlays.fxml"; - public static final String REGION_SELECTOR_FXML = "/net/rptools/tokentool/view/RegionSelector.fxml"; - public static final String CREDITS_FXML = "/net/rptools/tokentool/view/Credits.fxml"; - - public static final String RPTOOLS_URL = "http://www.rptools.net"; - - public static final String DEFAULT_IMAGE_EXTENSION = ".png"; - public static final String DEFAULT_IMAGE_EXTENSION_DESCRIPTION = "PNG Image"; - public static final String DEFAULT_TOKEN_NAME = "token"; - public static final String DEFAULT_FILE_NAME_SUFFIX = "0001"; - - public static final String VALID_FILE_NAME_PATTERN = "[a-zA-Z0-9._ `~!@#$%^&()\\-=+\\[\\]{}',]*"; - public static final String VALID_FILE_NAME_REPLACEMENT_PATTERN = "[^a-zA-Z0-9._ `~!@#$%^&()\\-=+\\[\\]{}',]"; - public static final String VALID_FILE_NAME_REPLACEMENT_CHARACTER = "_"; - - public static final int THUMB_SIZE = 100; - public static final int MAX_RECENT_SIZE = 10; - - public static final boolean DEFAULT_OVERLAY_ASPECT = true; - public static final boolean DEFAULT_OVERLAY_USE_BASE = false; - public static final boolean DEFAULT_OVERLAY_CLIP_PORTRAIT = true; - public static final boolean DEFAULT_USE_FILE_NUMBERING = false; - public static final double DEFAULT_OVERLAY_SIZE = 256; - - public static final File OVERLAY_DIR = AppSetup.getAppHome("overlays"); - public static final File CACHE_DIR = AppSetup.getAppHome("cache"); - - public static final ExtensionFilter IMAGE_EXTENSION_FILTER = new ExtensionFilter(DEFAULT_IMAGE_EXTENSION_DESCRIPTION, "*" + DEFAULT_IMAGE_EXTENSION); - - // public static final String DEFAULT_TOKEN_EXTENSION = ".rptok"; - // public static final String DEFAULT_TOKEN_EXTENSION_DESCRIPTION = "MapTool Token"; - - // public static final ExtensionFilter TOKEN_EXTENSION_FILTER = new ExtensionFilter( - // DEFAULT_TOKEN_EXTENSION_DESCRIPTION, "*" + DEFAULT_TOKEN_EXTENSION); -} \ No newline at end of file + public static final String APP_NAME = "TokenTool"; + + public static final String TOKEN_TOOL_ICON = "/net/rptools/tokentool/image/token_tool_icon.png"; + public static final String REGION_SELECTOR_ICON = + "/net/rptools/tokentool/image/region_selector_icon.png"; + + public static final String TOKEN_TOOL_BUNDLE = "net.rptools.tokentool.i18n.TokenTool"; + + public static final String SPLASH_SCREEN_FXML = + "/net/rptools/tokentool/view/SplashScreenLoader.fxml"; + public static final String TOKEN_TOOL_FXML = "/net/rptools/tokentool/view/TokenTool.fxml"; + public static final String MANAGE_OVERLAYS_FXML = + "/net/rptools/tokentool/view/ManageOverlays.fxml"; + public static final String REGION_SELECTOR_FXML = + "/net/rptools/tokentool/view/RegionSelector.fxml"; + public static final String CREDITS_FXML = "/net/rptools/tokentool/view/Credits.fxml"; + public static final String PDF_VIEW_FXML = "/net/rptools/tokentool/view/PdfView.fxml"; + + public static final String RPTOOLS_URL = "http://www.rptools.net"; + + public static final String DEFAULT_IMAGE_EXTENSION = ".png"; + public static final String DEFAULT_IMAGE_EXTENSION_DESCRIPTION = "PNG Image"; + public static final String DEFAULT_TOKEN_NAME = "token"; + public static final String DEFAULT_FILE_NAME_SUFFIX = "0001"; + + public static final String VALID_FILE_NAME_PATTERN = "[a-zA-Z0-9._ `~!@#$%^&()\\-=+\\[\\]{}',]*"; + public static final String VALID_FILE_NAME_REPLACEMENT_PATTERN = + "[^a-zA-Z0-9._ `~!@#$%^&()\\-=+\\[\\]{}',]"; + public static final String VALID_FILE_NAME_REPLACEMENT_CHARACTER = "_"; + + public static final int THUMB_SIZE = 100; + public static final int MAX_RECENT_SIZE = 10; + + // UI Defaults + public static final boolean DEFAULT_OVERLAY_ASPECT = true; + public static final boolean DEFAULT_OVERLAY_USE_BASE = false; + public static final boolean DEFAULT_OVERLAY_CLIP_PORTRAIT = true; + public static final boolean DEFAULT_USE_FILE_NUMBERING = false; + public static final int DEFAULT_OVERLAY_SIZE = 256; + + // Set by controller using defaults set in FXML + public static Image DEFAULT_PORTRAIT_IMAGE; + public static ImageView DEFAULT_BACKGROUND_IMAGE_VIEW; + public static Image DEFAULT_MASK_IMAGE; + public static Image DEFAULT_OVERLAY_IMAGE; + public static double DEFAULT_PORTRAIT_IMAGE_X; + public static double DEFAULT_PORTRAIT_IMAGE_Y; + public static double DEFAULT_PORTRAIT_IMAGE_SCALE; + public static double DEFAULT_PORTRAIT_IMAGE_ROTATE; + public static final double WINDOW_WIDTH = 825; + public static final double WINDOW_HEIGHT = 825; + + public static final File OVERLAY_DIR = AppSetup.getAppHome("overlays"); + public static final File CACHE_DIR = AppSetup.getAppHome("cache"); + + public static final ExtensionFilter IMAGE_EXTENSION_FILTER = + new ExtensionFilter(DEFAULT_IMAGE_EXTENSION_DESCRIPTION, "*" + DEFAULT_IMAGE_EXTENSION); + + public static final double DEFAULT_PORTRAIT_TRANSPARENCY = 1; + public static final double DEFAULT_PORTRAIT_BLUR = 0; + public static final double DEFAULT_PORTRAIT_GLOW = 0; + + public static final double DEFAULT_OVERLAY_TRANSPARENCY = 1; + + public static boolean DEFAULT_SAVE_PORTRAIT_ON_DRAG; + public static boolean DEFAULT_USE_BACKGROUND_ON_DRAG; + public static String DEFAULT_PORTRAIT_NAME_TEXT_FIELD; + public static boolean DEFAULT_USE_TOKEN_NAME; + public static String DEFAULT_PORTRAIT_NAME_SUFFIX_TEXT_FIELD; + + // public static final String DEFAULT_TOKEN_EXTENSION = ".rptok"; + // public static final String DEFAULT_TOKEN_EXTENSION_DESCRIPTION = "MapTool Token"; + + // public static final ExtensionFilter TOKEN_EXTENSION_FILTER = new ExtensionFilter( + // DEFAULT_TOKEN_EXTENSION_DESCRIPTION, "*" + DEFAULT_TOKEN_EXTENSION); +} diff --git a/src/main/java/net/rptools/tokentool/AppPreferences.java b/src/main/java/net/rptools/tokentool/AppPreferences.java index 2eee803..0d9df42 100644 --- a/src/main/java/net/rptools/tokentool/AppPreferences.java +++ b/src/main/java/net/rptools/tokentool/AppPreferences.java @@ -1,201 +1,279 @@ /* - * This software Copyright by the RPTools.net development team, and licensed under the Affero GPL Version 3 or, at your option, any later version. + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. * - * TokenTool Source Code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * TokenTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * - * You should have received a copy of the GNU Affero General Public License * along with this source Code. If not, please visit and specifically the Affero license text - * at . + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . */ package net.rptools.tokentool; import java.io.File; import java.io.IOException; -import java.net.MalformedURLException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Map; +import java.util.prefs.BackingStoreException; import java.util.prefs.Preferences; - -import javax.imageio.ImageIO; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - import javafx.embed.swing.SwingFXUtils; import javafx.scene.control.TreeItem; import javafx.scene.image.Image; -import javafx.scene.image.ImageView; -import javafx.scene.paint.Color; +import javax.imageio.ImageIO; import net.rptools.tokentool.client.TokenTool; import net.rptools.tokentool.controller.TokenTool_Controller; +import net.rptools.tokentool.model.Window_Preferences; import net.rptools.tokentool.util.FileSaveUtil; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; public class AppPreferences { - private static final Logger log = LogManager.getLogger(AppPreferences.class); - private static final Preferences prefs = Preferences.userNodeForPackage(net.rptools.tokentool.client.TokenTool.class); - - private static final String OVERLAY_ASPECT = "overlayAspectToggleButton"; - private static final String OVERLAY_WIDTH = "overlayWidthSpinner"; - private static final String OVERLAY_HEIGHT = "overlayHeightSpinner"; - private static final String OVERLAY_USE_BASE = "overlayUseAsBaseCheckbox"; - private static final String OVERLAY_CLIP_PORTRAIT = "clipPortraitCheckbox"; - private static final String BACKGROUND_COLOR_RED = "backgroundColor_RED"; - private static final String BACKGROUND_COLOR_BLUE = "backgroundColor_BLUE"; - private static final String BACKGROUND_COLOR_GREEN = "backgroundColor_GREEN"; - private static final String BACKGROUND_COLOR_ALPHA = "backgroundColor_ALPHA"; - private static final String LAST_FILE = "lastFileSaved"; - private static final String LAST_PORTRAIT_FILE = "lastPortrait"; - private static final String FILE_NAME_TEXT_FIELD = "fileNameTextField"; - private static final String FILE_NAME_SUFFIX_TEXT_FIELD = "fileNameSuffixTextField"; - private static final String USE_FILE_NUMBERING = "useFileNumberingCheckbox"; - private static final String RECENT_OVERLAY_COUNT = "recent_overlay_count"; - private static final String RECENT_OVERLAY = "recent_overlay_"; - private static final String PORTRAIT_X = "portrait_x"; - private static final String PORTRAIT_Y = "portrait_y"; - private static final String PORTRAIT_ROTATION = "portrait_rotation"; - private static final String PORTRAIT_ZOOM = "portrait_zoom"; - private static final String WINDOW_X = "window_x"; - private static final String WINDOW_Y = "window_y"; - private static final String WINDOW_WIDTH = "window_width"; - private static final String WINDOW_HEIGHT = "window_height"; - - public static void savePreferences(TokenTool_Controller tokentool_Controller) { - log.info("Saving preferences..."); - - // Save Overlay details - prefs.putBoolean(OVERLAY_ASPECT, tokentool_Controller.getOverlayAspect()); - prefs.putDouble(OVERLAY_WIDTH, tokentool_Controller.getOverlayWidth()); - prefs.putDouble(OVERLAY_HEIGHT, tokentool_Controller.getOverlayHeight()); - prefs.putBoolean(OVERLAY_USE_BASE, tokentool_Controller.getOverlayUseAsBase()); - prefs.putBoolean(OVERLAY_CLIP_PORTRAIT, tokentool_Controller.getClipPortraitCheckbox()); - - // Save Portrait background color - prefs.putDouble(BACKGROUND_COLOR_RED, tokentool_Controller.getBackgroundColor().getRed()); - prefs.putDouble(BACKGROUND_COLOR_GREEN, tokentool_Controller.getBackgroundColor().getGreen()); - prefs.putDouble(BACKGROUND_COLOR_BLUE, tokentool_Controller.getBackgroundColor().getBlue()); - prefs.putDouble(BACKGROUND_COLOR_ALPHA, tokentool_Controller.getBackgroundColor().getOpacity()); - - // Save naming details - prefs.putBoolean(USE_FILE_NUMBERING, tokentool_Controller.getUseFileNumberingCheckbox()); - prefs.put(FILE_NAME_TEXT_FIELD, tokentool_Controller.getFileNameTextField()); - prefs.put(FILE_NAME_SUFFIX_TEXT_FIELD, tokentool_Controller.getFileNameSuffixTextField()); - - // Save recent overlays used - log.debug("...saving recent overlay count"); - Map> recentOverlays = tokentool_Controller.getRecentOverlayTreeItems(); - prefs.putInt(RECENT_OVERLAY_COUNT, recentOverlays.size()); - - log.debug("...saving recent overlays"); - int i = 1; - for (Path path : recentOverlays.keySet()) - prefs.put(RECENT_OVERLAY + i++, path.toString()); - - // Save location of save - log.debug("...saving last file save location"); - try { - if (FileSaveUtil.getLastFile() != null) - if (FileSaveUtil.getLastFile().getParentFile().exists()) - prefs.put(LAST_FILE, FileSaveUtil.getLastFile().getCanonicalPath()); - } catch (IOException e) { - log.error("Error saving last file preference.", e); - } - - // Save last portrait used - try { - File lastPortrait = new File(AppConstants.CACHE_DIR, "last_portrait.png"); - ImageIO.write(SwingFXUtils.fromFXImage(tokentool_Controller.getPortraitImage(), null), "png", lastPortrait); - prefs.put(LAST_PORTRAIT_FILE, lastPortrait.getCanonicalPath()); - } catch (IOException e) { - log.error("Error saving last portrait preference.", e); - } - - // Save last portraits position and zoom - ImageView portraitImageView = tokentool_Controller.getPortraitImageView(); - prefs.putDouble(PORTRAIT_X, portraitImageView.getTranslateX()); - prefs.putDouble(PORTRAIT_Y, portraitImageView.getTranslateY()); - prefs.putDouble(PORTRAIT_ROTATION, portraitImageView.getRotate()); - prefs.putDouble(PORTRAIT_ZOOM, portraitImageView.getScaleY()); - - // Save window size/position - prefs.putDouble(WINDOW_X, TokenTool.getInstance().getStage().getX()); - prefs.putDouble(WINDOW_Y, TokenTool.getInstance().getStage().getY()); - prefs.putDouble(WINDOW_WIDTH, TokenTool.getInstance().getStage().getWidth()); - prefs.putDouble(WINDOW_HEIGHT, TokenTool.getInstance().getStage().getHeight()); - } - - public static void restorePreferences(TokenTool_Controller tokentool_Controller) { - log.info("Restoring preferences..."); - - // Restore Overlay details - tokentool_Controller.setOverlayAspect(prefs.getBoolean(OVERLAY_ASPECT, AppConstants.DEFAULT_OVERLAY_ASPECT)); - tokentool_Controller.setOverlayWidth(prefs.getDouble(OVERLAY_WIDTH, AppConstants.DEFAULT_OVERLAY_SIZE)); - tokentool_Controller.setOverlayHeight(prefs.getDouble(OVERLAY_HEIGHT, AppConstants.DEFAULT_OVERLAY_SIZE)); - tokentool_Controller.setOverlayUseAsBase(prefs.getBoolean(OVERLAY_USE_BASE, AppConstants.DEFAULT_OVERLAY_USE_BASE)); - tokentool_Controller.setClipPortraitCheckbox(prefs.getBoolean(OVERLAY_CLIP_PORTRAIT, AppConstants.DEFAULT_OVERLAY_CLIP_PORTRAIT)); - - // Save Portrait background color - double red = prefs.getDouble(BACKGROUND_COLOR_RED, 0); - double grn = prefs.getDouble(BACKGROUND_COLOR_GREEN, 0); - double blu = prefs.getDouble(BACKGROUND_COLOR_BLUE, 0); - double alpha = prefs.getDouble(BACKGROUND_COLOR_ALPHA, 0); - - if (red + grn + blu + alpha > 0) - tokentool_Controller.setBackgroundColor(new Color(red, grn, blu, alpha)); - - // Restore naming details - tokentool_Controller.setUseFileNumberingCheckbox(prefs.getBoolean(USE_FILE_NUMBERING, AppConstants.DEFAULT_USE_FILE_NUMBERING)); - tokentool_Controller.setFileNameTextField(prefs.get(FILE_NAME_TEXT_FIELD, AppConstants.DEFAULT_TOKEN_NAME)); - tokentool_Controller.setFileNameSuffixTextField(prefs.get(FILE_NAME_SUFFIX_TEXT_FIELD, AppConstants.DEFAULT_FILE_NAME_SUFFIX)); - - // Restore recent overlays used - int overlayCount = prefs.getInt(RECENT_OVERLAY_COUNT, 0); - for (int i = 1; i <= overlayCount; i++) { - String filePath = prefs.get(RECENT_OVERLAY + i, ""); - if (!filePath.isEmpty()) - if (new File(filePath).exists()) { - tokentool_Controller.updateRecentOverlayTreeItems(Paths.get(filePath)); - } - } - - // Restore window size/position - double window_x = prefs.getDouble(WINDOW_X, 0); - TokenTool.getInstance().getStage().setX(window_x); - - double window_y = prefs.getDouble(WINDOW_Y, 0); - TokenTool.getInstance().getStage().setY(window_y); - - log.info("Restoring window position x, y: " + window_x + ", " + window_y); - - double window_width = prefs.getDouble(WINDOW_WIDTH, 0); - if (window_width > 0) - TokenTool.getInstance().getStage().setWidth(window_width); - - double window_height = prefs.getDouble(WINDOW_HEIGHT, 0); - if (window_height > 0) - TokenTool.getInstance().getStage().setHeight(window_height); - - // Restore location of save - FileSaveUtil.setLastFile(prefs.get(LAST_FILE, null)); - - // Restore last portrait used - String lastPortraitPath = prefs.get(LAST_PORTRAIT_FILE, null); - if (lastPortraitPath != null) - if (!lastPortraitPath.isEmpty()) { - File lastPortrait = new File(lastPortraitPath); - if (lastPortrait.exists()) - try { - Image portraitImage = new Image(lastPortrait.toURI().toURL().toExternalForm()); - - // Restore last portraits position and zoom - double x = prefs.getDouble(PORTRAIT_X, 0); - double y = prefs.getDouble(PORTRAIT_Y, 0); - double r = prefs.getDouble(PORTRAIT_ROTATION, 0); - double s = prefs.getDouble(PORTRAIT_ZOOM, 1); - - tokentool_Controller.setPortraitImage(portraitImage, x, y, r, s); - } catch (MalformedURLException e) { - log.error("Error loading last portrait preference.", e); - } - } - } + private static final Logger log = LogManager.getLogger(AppPreferences.class); + private static final Preferences prefs = Preferences.userNodeForPackage(TokenTool.class); + + // _PREFERENCES are stored as JSON objects + private static final String OVERLAY_ASPECT = "overlayAspectToggleButton"; + private static final String OVERLAY_WIDTH = "overlayWidthSpinner"; + private static final String OVERLAY_HEIGHT = "overlayHeightSpinner"; + private static final String OVERLAY_USE_BASE = "overlayUseAsBaseCheckbox"; + private static final String OVERLAY_CLIP_PORTRAIT = "clipPortraitCheckbox"; + private static final String LAST_TOKEN_SAVE_LOCATION = "lastFileSaved"; + private static final String FILE_NAME_TEXT_FIELD = "fileNameTextField"; + private static final String FILE_NAME_SUFFIX_TEXT_FIELD = "fileNameSuffixTextField"; + private static final String USE_FILE_NUMBERING = "useFileNumberingCheckbox"; + private static final String SAVE_PORTRAIT_ON_DRAG = "savePortraitOnDragCheckbox"; + private static final String USE_BACKGROUND_ON_DRAG = "useBackgroundOnDragCheckbox"; + + private static final String RECENT_OVERLAY_COUNT = "recent_overlay_count"; + private static final String RECENT_OVERLAY = "recent_overlay_"; + private static final String PORTRAIT_IMAGEVIEW_PREFERENCES = "portraitImageView_preferences"; + private static final String BACKGROUND_IMAGEVIEW_PREFERENCES = "backgroundImageView_preferences"; + + private static final String WINDOW_MAIN_PREFERENCES = "windowMain_preferences"; + public static final String WINDOW_MANAGE_OVERLAYS_PREFERENCES = + "windowManageOverlays_preferences"; + public static final String WINDOW_PDF_PREFERENCES = "windowPdf_preferences"; + public static final String WINDOW_REGION_SELECTOR_PREFERENCES = + "windowRegionSelector_preferences"; + public static final String WINDOW_CREDITS_PREFERENCES = "windowCredits_preferences"; + + public static final String LAST_PDF_FILE = "lastPdfFileSaved"; + public static final String LAST_BACKGROUND_IMAGE_FILE = "lastBackgroundImageFile"; + public static final String LAST_PORTRAIT_IMAGE_FILE = "lastPortraitImageFile"; + + private static final String PORTRAIT_TRANSPARENCY = "portraitTransparency"; + private static final String PORTRAIT_BLUR = "portraitBlur"; + private static final String PORTRAIT_GLOW = "portraitGlow"; + + private static final String OVERLAY_TRANSPARENCY = "overlayTransparency"; + private static final String PORTRAIT_NAME_TEXT_FIELD = "portraitNameTextField"; + private static final String USE_TOKEN_NAME = "useTokenNameCheckbox"; + private static final String PORTRAIT_NAME_SUFFIX_TEXT_FIELD = "portraitNameSuffixTextField"; + + public static void setPreference(String preference, String value) { + prefs.put(preference, value); + } + + public static void setPreference(String preference, boolean value) { + prefs.putBoolean(preference, value); + } + + public static void setPreference(String preference, int value) { + prefs.putInt(preference, value); + } + + public static void setPreference(String preference, double value) { + prefs.putDouble(preference, value); + } + + public static String getPreference(String preference, String defaultVal) { + return prefs.get(preference, defaultVal); + } + + public static boolean getPreference(String preference, boolean defaultVal) { + return prefs.getBoolean(preference, defaultVal); + } + + public static int getPreference(String preference, int defaultVal) { + return prefs.getInt(preference, defaultVal); + } + + public static double getPreference(String preference, double devaultVal) { + return prefs.getDouble(preference, devaultVal); + } + + public static void savePreferences(TokenTool_Controller tokentool_Controller) { + log.info("Saving preferences to " + prefs.toString()); + + // Save Overlay details + prefs.putBoolean(OVERLAY_ASPECT, tokentool_Controller.getOverlayAspect()); + prefs.putInt(OVERLAY_WIDTH, tokentool_Controller.getOverlayWidth()); + prefs.putInt(OVERLAY_HEIGHT, tokentool_Controller.getOverlayHeight()); + prefs.putBoolean(OVERLAY_USE_BASE, tokentool_Controller.getOverlayUseAsBase()); + prefs.putBoolean(OVERLAY_CLIP_PORTRAIT, tokentool_Controller.getClipPortraitCheckbox()); + + // Save naming details + prefs.putBoolean(USE_FILE_NUMBERING, tokentool_Controller.getUseFileNumberingCheckbox()); + prefs.put(FILE_NAME_TEXT_FIELD, tokentool_Controller.getFileNameTextField()); + prefs.put(FILE_NAME_SUFFIX_TEXT_FIELD, tokentool_Controller.getFileNameSuffixTextField()); + prefs.put(PORTRAIT_NAME_TEXT_FIELD, tokentool_Controller.getPortraitNameTextField()); + prefs.putBoolean(USE_TOKEN_NAME, tokentool_Controller.getUseTokenNameCheckbox()); + prefs.put( + PORTRAIT_NAME_SUFFIX_TEXT_FIELD, tokentool_Controller.getPortraitNameSuffixTextField()); + prefs.putBoolean(SAVE_PORTRAIT_ON_DRAG, tokentool_Controller.getSavePortraitOnDragCheckbox()); + prefs.putBoolean(USE_BACKGROUND_ON_DRAG, tokentool_Controller.getUseBackgroundOnDragCheckbox()); + + // Save recent overlays used + log.debug("...saving recent overlay count"); + Map> recentOverlays = tokentool_Controller.getRecentOverlayTreeItems(); + prefs.putInt(RECENT_OVERLAY_COUNT, recentOverlays.size()); + + log.debug("...saving recent overlays"); + int i = 1; + for (Path path : recentOverlays.keySet()) prefs.put(RECENT_OVERLAY + i++, path.toString()); + + // Save location of save + log.debug("...saving last file save location"); + try { + if (FileSaveUtil.getLastFile() != null) + if (FileSaveUtil.getLastFile().getParentFile().exists()) + prefs.put(LAST_TOKEN_SAVE_LOCATION, FileSaveUtil.getLastFile().getCanonicalPath()); + } catch (NullPointerException e) { + log.warn("No last save location to save..."); + } catch (IOException e) { + log.error("Error saving last file preference.", e); + } + + // Save last portrait used + try { + File lastPortrait = new File(AppConstants.CACHE_DIR, "last_portrait.png"); + ImageIO.write( + SwingFXUtils.fromFXImage(tokentool_Controller.getPortraitImage(), null), + "png", + lastPortrait); + prefs.put( + PORTRAIT_IMAGEVIEW_PREFERENCES, + tokentool_Controller.getPortrait_Preferences(lastPortrait.getCanonicalPath())); + } catch (NullPointerException e) { + log.warn("No portrait to save..."); + } catch (IOException e) { + log.error("Error saving last portrait preference.", e); + } + + // Save last background used + try { + Image backgroundImage = tokentool_Controller.getBackgroundImage(); + if (backgroundImage != null) { + File lastBackground = new File(AppConstants.CACHE_DIR, "last_background.png"); + ImageIO.write( + SwingFXUtils.fromFXImage(tokentool_Controller.getBackgroundImage(), null), + "png", + lastBackground); + prefs.put( + BACKGROUND_IMAGEVIEW_PREFERENCES, + tokentool_Controller.getBackground_Preferences(lastBackground.getCanonicalPath())); + } else { + prefs.put( + BACKGROUND_IMAGEVIEW_PREFERENCES, tokentool_Controller.getBackground_Preferences(null)); + } + } catch (IOException e) { + log.error("Error saving last background preference.", e); + } + + // Save window size/position + prefs.put( + WINDOW_MAIN_PREFERENCES, + new Window_Preferences(TokenTool.getInstance().getStage()).toJson()); + } + + public static void restorePreferences(TokenTool_Controller tokentool_Controller) { + log.info("Restoring preferences from " + prefs.toString()); + + // Restore Overlay details + tokentool_Controller.setOverlayAspect( + prefs.getBoolean(OVERLAY_ASPECT, AppConstants.DEFAULT_OVERLAY_ASPECT)); + tokentool_Controller.setOverlayWidth( + prefs.getInt(OVERLAY_WIDTH, AppConstants.DEFAULT_OVERLAY_SIZE)); + tokentool_Controller.setOverlayHeight( + prefs.getInt(OVERLAY_HEIGHT, AppConstants.DEFAULT_OVERLAY_SIZE)); + tokentool_Controller.setOverlayUseAsBase( + prefs.getBoolean(OVERLAY_USE_BASE, AppConstants.DEFAULT_OVERLAY_USE_BASE)); + tokentool_Controller.setClipPortraitCheckbox( + prefs.getBoolean(OVERLAY_CLIP_PORTRAIT, AppConstants.DEFAULT_OVERLAY_CLIP_PORTRAIT)); + + // Restore naming details + tokentool_Controller.setUseFileNumberingCheckbox( + prefs.getBoolean(USE_FILE_NUMBERING, AppConstants.DEFAULT_USE_FILE_NUMBERING)); + tokentool_Controller.setFileNameTextField( + prefs.get(FILE_NAME_TEXT_FIELD, AppConstants.DEFAULT_TOKEN_NAME)); + tokentool_Controller.setFileNameSuffixTextField( + prefs.get(FILE_NAME_SUFFIX_TEXT_FIELD, AppConstants.DEFAULT_FILE_NAME_SUFFIX)); + tokentool_Controller.setUseTokenNameCheckbox( + prefs.getBoolean(USE_TOKEN_NAME, AppConstants.DEFAULT_USE_TOKEN_NAME)); + tokentool_Controller.setPortraitNameTextField( + prefs.get(PORTRAIT_NAME_TEXT_FIELD, AppConstants.DEFAULT_PORTRAIT_NAME_TEXT_FIELD)); + tokentool_Controller.setPortraitNameSuffixTextField( + prefs.get( + PORTRAIT_NAME_SUFFIX_TEXT_FIELD, AppConstants.DEFAULT_PORTRAIT_NAME_SUFFIX_TEXT_FIELD)); + tokentool_Controller.setUseBackgroundOnDragCheckbox( + prefs.getBoolean(USE_BACKGROUND_ON_DRAG, AppConstants.DEFAULT_USE_BACKGROUND_ON_DRAG)); + tokentool_Controller.setSavePortraitOnDragCheckbox( + prefs.getBoolean(SAVE_PORTRAIT_ON_DRAG, AppConstants.DEFAULT_SAVE_PORTRAIT_ON_DRAG)); + + // Restore Portrait Effects + tokentool_Controller + .getPortraitTransparencySlider() + .setValue( + prefs.getDouble(PORTRAIT_TRANSPARENCY, AppConstants.DEFAULT_PORTRAIT_TRANSPARENCY)); + tokentool_Controller + .getPortraitBlurSlider() + .setValue(prefs.getDouble(PORTRAIT_BLUR, AppConstants.DEFAULT_PORTRAIT_BLUR)); + tokentool_Controller + .getPortraitGlowSlider() + .setValue(prefs.getDouble(PORTRAIT_GLOW, AppConstants.DEFAULT_PORTRAIT_GLOW)); + + tokentool_Controller + .getOverlayTransparencySlider() + .setValue(prefs.getDouble(OVERLAY_TRANSPARENCY, AppConstants.DEFAULT_OVERLAY_TRANSPARENCY)); + + // Restore recent overlays used + int overlayCount = prefs.getInt(RECENT_OVERLAY_COUNT, 0); + for (int i = 1; i <= overlayCount; i++) { + String filePath = prefs.get(RECENT_OVERLAY + i, ""); + if (!filePath.isEmpty()) + if (new File(filePath).exists()) { + tokentool_Controller.updateRecentOverlayTreeItems(Paths.get(filePath)); + } + } + + // Restore window size/position + String windowMain_Preferences = + prefs.get( + WINDOW_MAIN_PREFERENCES, + new Window_Preferences(AppConstants.WINDOW_WIDTH, AppConstants.WINDOW_HEIGHT).toJson()); + tokentool_Controller.setWindoFrom_Preferences(windowMain_Preferences); + + // Restore location of save + FileSaveUtil.setLastFile(prefs.get(LAST_TOKEN_SAVE_LOCATION, null)); + + // Restore last portrait used + tokentool_Controller.setPortraitFrom_Preferences( + prefs.get(PORTRAIT_IMAGEVIEW_PREFERENCES, null)); + tokentool_Controller.setBackgroundFrom_Preferences( + prefs.get(BACKGROUND_IMAGEVIEW_PREFERENCES, null)); + } + + public static void removeAllPreferences() { + try { + prefs.clear(); + prefs.flush(); + } catch (BackingStoreException e) { + log.error("Error removing all preferences from backing store!", e); + } + } } diff --git a/src/main/java/net/rptools/tokentool/AppSetup.java b/src/main/java/net/rptools/tokentool/AppSetup.java index a6e4f6d..3b46b56 100644 --- a/src/main/java/net/rptools/tokentool/AppSetup.java +++ b/src/main/java/net/rptools/tokentool/AppSetup.java @@ -1,10 +1,16 @@ /* - * This software Copyright by the RPTools.net development team, and licensed under the Affero GPL Version 3 or, at your option, any later version. + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. * - * TokenTool Source Code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * TokenTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * - * You should have received a copy of the GNU Affero General Public License * along with this source Code. If not, please visit and specifically the Affero license text - * at . + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . */ package net.rptools.tokentool; @@ -15,155 +21,216 @@ import java.util.Collection; import java.util.Set; import java.util.regex.Pattern; - +import javafx.application.Platform; +import javafx.scene.control.Alert; +import javafx.scene.control.Alert.AlertType; +import net.rptools.tokentool.client.TokenTool; +import net.rptools.tokentool.util.I18N; +import net.rptools.tokentool.util.ImageUtil; import org.apache.commons.io.FileUtils; import org.apache.commons.io.filefilter.TrueFileFilter; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.ThreadContext; import org.reflections.Reflections; import org.reflections.scanners.ResourcesScanner; -import net.rptools.tokentool.client.TokenTool; -import net.rptools.tokentool.util.ImageUtil; - -/** - * Executes only the first time the application is run. - */ +/** Executes only the first time the application is run. */ public class AppSetup { - private static Logger log; // Don't instantiate until install gets and sets user_home/logs directory - - private static final String DEFAULT_OVERLAYS = "net/rptools/tokentool/overlays/v2"; - private static final String USER_HOME; - - static { - USER_HOME = System.getProperty("user.home"); - } - - public static void install(String versionString) { - System.setProperty("appHome", getAppHome("logs").getAbsolutePath()); - log = LogManager.getLogger(AppSetup.class); - - File overlayVer = new File(getAppHome().getAbsolutePath() + "/version.txt"); - Collection existingOverLays = FileUtils.listFiles(AppConstants.OVERLAY_DIR, ImageUtil.SUPPORTED_FILE_FILTER, TrueFileFilter.INSTANCE); - log.info("Overlays installed: " + existingOverLays.size()); - - // Only install overlays once or if version.text is missing or version is newer - // Overlays are stored in a version packaged structure so we can later install only newer overlays if wanted - String installedVersion = "0"; - try { - if (overlayVer.exists()) { - installedVersion = FileUtils.readFileToString(overlayVer, Charset.defaultCharset()); - } else { - FileUtils.writeStringToFile(overlayVer, versionString, Charset.defaultCharset()); - } - } catch (IOException ioe) { - log.error(ioe); - } - - if (existingOverLays.isEmpty() || isNewerVersion(TokenTool.getVersion(), installedVersion)) { - try { - installDefaultOverlays(); - } catch (IOException e) { - log.error(e); - } - - // Update version file to new version - try { - FileUtils.writeStringToFile(overlayVer, versionString, Charset.defaultCharset()); - } catch (IOException e) { - log.error(e); - } - } - } - - public static void installDefaultOverlays() throws IOException { - // Create the overlay directory - File overlayDir = AppConstants.OVERLAY_DIR; - overlayDir.mkdirs(); - - // Copy default overlays from resources - // https://dzone.com/articles/get-all-classes-within-package - Reflections reflections = new Reflections(DEFAULT_OVERLAYS, new ResourcesScanner()); - Set resourcePathSet = reflections.getResources(Pattern.compile(".*")); - - for (String resourcePath : resourcePathSet) { - URL inputUrl = AppSetup.class.getClassLoader().getResource(resourcePath); - String resourceName = resourcePath.substring(DEFAULT_OVERLAYS.length()); - - try { - log.info("Installing overlay: " + resourceName); - FileUtils.copyURLToFile(inputUrl, new File(overlayDir, resourceName)); - } catch (IOException e) { - log.error("ERROR writing " + inputUrl); - log.error(e); - } - } - } - - public static File getAppHome() { - if (USER_HOME == null) { - return null; - } - - String vendor = ""; - - if (!TokenTool.getVendor().isEmpty()) - vendor += "-" + TokenTool.getVendor(); - - File home = new File(USER_HOME + "/." + (AppConstants.APP_NAME + vendor).toLowerCase()); - home.mkdirs(); - - return home; - } - - public static File getAppHome(String subdir) { - if (USER_HOME == null) { - return null; - } - - File home = new File(getAppHome().getPath() + "/" + subdir); - home.mkdirs(); - - return home; - } - - /** - * A convenience method to break up the version number string into it's component version identifiers and test if version is newer than existing version - * - * @author Jamz - * @since 2.0 - * - * @param installed - * version - * @param running - * version - * @return true if valid version format - */ - private static boolean isNewerVersion(String version, String installedVersion) { - if (version.equals("DEVELOPMENT") || installedVersion.equals("DEVELOPMENT")) - return false; - - String[] versions = version.indexOf(".") > 0 ? version.split("\\.") : new String[] { version }; - String[] installedVersions = installedVersion.indexOf(".") > 0 ? installedVersion.split("\\.") : new String[] { installedVersion }; - - int i = 0; - for (String ver : versions) { - int v = Integer.parseInt(ver); - int iv = 0; - - if (installedVersions.length > i) - iv = Integer.parseInt(installedVersions[i]); - - if (v > iv) { - log.info("New version detected."); - return true; - } else if (iv > v) { - return false; - } - - i++; - } - - return false; - } + private static Logger + log; // Don't instantiate until install gets and sets user_home/logs directory + + private static final String DEFAULT_OVERLAYS = "net/rptools/tokentool/overlays/"; + private static final String USER_HOME; + + static { + USER_HOME = System.getProperty("user.home"); + ThreadContext.put( + "OS", System.getProperty("os.name")); // Added to the JavaFX-Launcher thread... + } + + public static void install(String versionString) { + System.setProperty("appHome", getAppHome("logs").getAbsolutePath()); + log = LogManager.getLogger(AppSetup.class); + + File overlayVerFile = new File(getAppHome().getAbsolutePath() + "/version.txt"); + Collection existingOverLays = + FileUtils.listFiles( + AppConstants.OVERLAY_DIR, ImageUtil.SUPPORTED_FILE_FILTER, TrueFileFilter.INSTANCE); + log.info("Overlays installed: " + existingOverLays.size()); + + // Only install overlays once or if version.text is missing or version is newer + // Overlays are stored in a version packaged structure so we can later install only newer + // overlays if wanted + String installedVersion = "0"; + try { + if (overlayVerFile.exists()) { + installedVersion = FileUtils.readFileToString(overlayVerFile, Charset.defaultCharset()); + } else { + FileUtils.writeStringToFile(overlayVerFile, versionString, Charset.defaultCharset()); + } + } catch (IOException ioe) { + log.error(ioe); + } + + if (existingOverLays.isEmpty()) { + try { + installDefaultOverlays(); + } catch (IOException e) { + log.error("Error installing overlays upon detecting no overlays are installed...", e); + } + } else if (isNewerVersion(TokenTool.getVersion(), installedVersion)) { + log.info("New version detected."); + + try { + confirmInstallOverlays(installNewOverlays(installedVersion)); + } catch (IOException e) { + log.error("Error installing overlays upon detecting a new version installed...", e); + } + + // Update version file to new version + try { + FileUtils.writeStringToFile(overlayVerFile, versionString, Charset.defaultCharset()); + } catch (IOException e) { + log.error(e); + } + } + } + + private static void confirmInstallOverlays(int overlaysInstalled) { + if (overlaysInstalled <= 0) return; + + Platform.runLater( + () -> { + Alert alert = new Alert(AlertType.INFORMATION); + alert.setHeaderText(I18N.getString("TokenTool.dialog.confirmation.header")); + alert.setTitle(I18N.getString("AppSetup.dialog.install.overlays.confirmation.title")); + alert.setContentText( + overlaysInstalled + + " " + + I18N.getString("AppSetup.dialog.install.overlays.confirmation")); + + alert.showAndWait(); + }); + + log.info(overlaysInstalled + " New overlays installed."); + } + + public static void installDefaultOverlays() throws IOException { + installNewOverlays("0.0"); // Install all overlays + } + + /* + * OK so here will will only install any overlays that are in a directory with a newer version than what is installed. So, if a user skips versions and goes from 2 to 2.3 any overlays in 2.1, 2.2, + * and 2.3 will get installed. I'm doing it this way in cause a user reorganizes his directory structure or deletes overlays he doesn't want, we don't reinstall them and annoy the user! + */ + public static int installNewOverlays(String currentVersion) throws IOException { + // Create the overlay directory if it doesn't already exist + File overlayDir = AppConstants.OVERLAY_DIR; + overlayDir.mkdirs(); + int overlaysInstalled = 0; + + // Copy default overlays from resources + // https://dzone.com/articles/get-all-classes-within-package + Reflections reflections = new Reflections(DEFAULT_OVERLAYS, new ResourcesScanner()); + Set resourcePathSet = reflections.getResources(Pattern.compile(".*")); + + for (String resourcePath : resourcePathSet) { + URL inputUrl = AppSetup.class.getClassLoader().getResource(resourcePath); + String resourceName = resourcePath.substring(DEFAULT_OVERLAYS.length()); + + int verIndex = resourceName.indexOf("/"); + String resourceVerion = resourceName.substring(1, verIndex).replace("_", "."); + + if (isNewerVersion(resourceVerion, currentVersion)) { + File resourceFile = + new File( + overlayDir, + resourceName.substring(resourceVerion.length() + 1, resourceName.length())); + log.info("Installing overlay: " + resourceFile); + + try { + FileUtils.copyURLToFile(inputUrl, resourceFile); + overlaysInstalled++; + } catch (IOException e) { + log.error("ERROR copying " + inputUrl + " to " + resourceFile, e); + } + } + } + + return overlaysInstalled; + } + + public static File getAppHome() { + if (USER_HOME == null) { + return null; + } + + String vendor = ""; + + if (!TokenTool.getVendor().isEmpty()) vendor += "-" + TokenTool.getVendor(); + + File home = new File(USER_HOME + "/." + (AppConstants.APP_NAME + vendor).toLowerCase()); + home.mkdirs(); + + return home; + } + + public static File getAppHome(String subdir) { + if (USER_HOME == null) { + return null; + } + + File home = new File(getAppHome().getPath() + "/" + subdir); + home.mkdirs(); + + return home; + } + + public static File getTmpDir() { + return getAppHome("tmp"); + } + + /** + * A convenience method to break up the version number string into it's component version + * identifiers and test if version is newer than existing version + * + * @author Jamz + * @since 2.0 + * @param installed version + * @param running version + * @return true if valid version format + */ + private static boolean isNewerVersion(String version, String installedVersion) { + if (version.equals("DEVELOPMENT") || installedVersion.equals("DEVELOPMENT")) return false; + + String[] versions = version.indexOf(".") > 0 ? version.split("\\.") : new String[] {version}; + String[] installedVersions = + installedVersion.indexOf(".") > 0 + ? installedVersion.split("\\.") + : new String[] {installedVersion}; + + int i = 0; + try { + for (String ver : versions) { + int v = Integer.parseInt(ver); + int iv = 0; + + if (installedVersions.length > i) iv = Integer.parseInt(installedVersions[i]); + + if (v > iv) { + return true; + } else if (iv > v) { + return false; + } + + i++; + } + } catch (NumberFormatException e) { + log.warn("Unable to parse version, installedVersion: " + version + ", " + installedVersion); + return false; + } + + return false; + } } diff --git a/src/main/java/net/rptools/tokentool/client/Credits.java b/src/main/java/net/rptools/tokentool/client/Credits.java index e182fd6..4c0e3f0 100644 --- a/src/main/java/net/rptools/tokentool/client/Credits.java +++ b/src/main/java/net/rptools/tokentool/client/Credits.java @@ -1,18 +1,22 @@ /* - * This software Copyright by the RPTools.net development team, and licensed under the Affero GPL Version 3 or, at your option, any later version. + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. * - * TokenTool Source Code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * TokenTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * - * You should have received a copy of the GNU Affero General Public License * along with this source Code. If not, please visit and specifically the Affero license text - * at . + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . */ package net.rptools.tokentool.client; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; import java.util.ResourceBundle; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - import javafx.event.EventHandler; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; @@ -22,38 +26,57 @@ import javafx.stage.Stage; import javafx.stage.WindowEvent; import net.rptools.tokentool.AppConstants; +import net.rptools.tokentool.AppPreferences; import net.rptools.tokentool.controller.TokenTool_Controller; +import net.rptools.tokentool.model.Window_Preferences; import net.rptools.tokentool.util.I18N; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; public class Credits { - private static final Logger log = LogManager.getLogger(Credits.class); - - private Stage stage; - - public Credits(TokenTool_Controller tokenTool_Controller) { - try { - FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource(AppConstants.CREDITS_FXML), ResourceBundle.getBundle(AppConstants.TOKEN_TOOL_BUNDLE)); - Parent root = (Parent) fxmlLoader.load(); - - stage = new Stage(); - Scene scene = new Scene(root); - - stage.getIcons().add(new Image(getClass().getResourceAsStream(AppConstants.TOKEN_TOOL_ICON))); - stage.initModality(Modality.APPLICATION_MODAL); - stage.resizableProperty().setValue(false); - stage.setTitle(I18N.getString("Credits.stage.title")); - stage.setScene(scene); - - stage.setOnCloseRequest(new EventHandler() { - @Override - public void handle(WindowEvent event) { - stage.hide(); - } - }); - - stage.show(); - } catch (Exception e) { - log.error("Error loading Credits stage!", e); - } - } -} \ No newline at end of file + private static final Logger log = LogManager.getLogger(Credits.class); + + private Stage stage; + + public Credits(TokenTool_Controller tokenTool_Controller) { + try { + FXMLLoader fxmlLoader = + new FXMLLoader( + getClass().getResource(AppConstants.CREDITS_FXML), + ResourceBundle.getBundle(AppConstants.TOKEN_TOOL_BUNDLE)); + Parent root = (Parent) fxmlLoader.load(); + + stage = new Stage(); + Scene scene = new Scene(root); + + stage.getIcons().add(new Image(getClass().getResourceAsStream(AppConstants.TOKEN_TOOL_ICON))); + stage.initModality(Modality.APPLICATION_MODAL); + stage.resizableProperty().setValue(false); + stage.setTitle(I18N.getString("Credits.stage.title")); + stage.setScene(scene); + + stage.setOnCloseRequest( + new EventHandler() { + @Override + public void handle(WindowEvent event) { + AppPreferences.setPreference( + AppPreferences.WINDOW_CREDITS_PREFERENCES, + new Window_Preferences(stage).toJson()); + stage.hide(); + } + }); + + String preferencesJson = + AppPreferences.getPreference(AppPreferences.WINDOW_CREDITS_PREFERENCES, null); + if (preferencesJson != null) { + Window_Preferences window_Preferences = + new Gson().fromJson(preferencesJson, new TypeToken() {}.getType()); + window_Preferences.setWindow(stage); + } + + stage.show(); + } catch (Exception e) { + log.error("Error loading Credits stage!", e); + } + } +} diff --git a/src/main/java/net/rptools/tokentool/client/ManageOverlays.java b/src/main/java/net/rptools/tokentool/client/ManageOverlays.java index 413c791..ef1ede5 100644 --- a/src/main/java/net/rptools/tokentool/client/ManageOverlays.java +++ b/src/main/java/net/rptools/tokentool/client/ManageOverlays.java @@ -1,18 +1,22 @@ /* - * This software Copyright by the RPTools.net development team, and licensed under the Affero GPL Version 3 or, at your option, any later version. + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. * - * TokenTool Source Code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * TokenTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * - * You should have received a copy of the GNU Affero General Public License * along with this source Code. If not, please visit and specifically the Affero license text - * at . + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . */ package net.rptools.tokentool.client; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; import java.util.ResourceBundle; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - import javafx.event.EventHandler; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; @@ -22,38 +26,57 @@ import javafx.stage.Stage; import javafx.stage.WindowEvent; import net.rptools.tokentool.AppConstants; +import net.rptools.tokentool.AppPreferences; import net.rptools.tokentool.controller.TokenTool_Controller; +import net.rptools.tokentool.model.Window_Preferences; import net.rptools.tokentool.util.I18N; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; public class ManageOverlays { - private static final Logger log = LogManager.getLogger(ManageOverlays.class); - - private Stage stage; - - public ManageOverlays(TokenTool_Controller tokenTool_Controller) { - try { - FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource(AppConstants.MANAGE_OVERLAYS_FXML), ResourceBundle.getBundle(AppConstants.TOKEN_TOOL_BUNDLE)); - Parent root = (Parent) fxmlLoader.load(); - - stage = new Stage(); - Scene scene = new Scene(root); - - stage.getIcons().add(new Image(getClass().getResourceAsStream(AppConstants.TOKEN_TOOL_ICON))); - stage.initModality(Modality.APPLICATION_MODAL); - stage.setTitle(I18N.getString("ManageOverlays.stage.title")); - stage.setScene(scene); - - stage.setOnCloseRequest(new EventHandler() { - @Override - public void handle(WindowEvent event) { - stage.hide(); - tokenTool_Controller.refreshCache(); - } - }); - - stage.show(); - } catch (Exception e) { - log.error(e); - } - } -} \ No newline at end of file + private static final Logger log = LogManager.getLogger(ManageOverlays.class); + + private Stage stage; + + public ManageOverlays(TokenTool_Controller tokenTool_Controller) { + try { + FXMLLoader fxmlLoader = + new FXMLLoader( + getClass().getResource(AppConstants.MANAGE_OVERLAYS_FXML), + ResourceBundle.getBundle(AppConstants.TOKEN_TOOL_BUNDLE)); + Parent root = (Parent) fxmlLoader.load(); + + stage = new Stage(); + Scene scene = new Scene(root); + + stage.getIcons().add(new Image(getClass().getResourceAsStream(AppConstants.TOKEN_TOOL_ICON))); + stage.initModality(Modality.APPLICATION_MODAL); + stage.setTitle(I18N.getString("ManageOverlays.stage.title")); + stage.setScene(scene); + + stage.setOnCloseRequest( + new EventHandler() { + @Override + public void handle(WindowEvent event) { + AppPreferences.setPreference( + AppPreferences.WINDOW_MANAGE_OVERLAYS_PREFERENCES, + new Window_Preferences(stage).toJson()); + stage.hide(); + tokenTool_Controller.refreshCache(); + } + }); + + String preferencesJson = + AppPreferences.getPreference(AppPreferences.WINDOW_MANAGE_OVERLAYS_PREFERENCES, null); + if (preferencesJson != null) { + Window_Preferences window_Preferences = + new Gson().fromJson(preferencesJson, new TypeToken() {}.getType()); + window_Preferences.setWindow(stage); + } + + stage.show(); + } catch (Exception e) { + log.error(e); + } + } +} diff --git a/src/main/java/net/rptools/tokentool/client/PdfViewer.java b/src/main/java/net/rptools/tokentool/client/PdfViewer.java new file mode 100644 index 0000000..9125a38 --- /dev/null +++ b/src/main/java/net/rptools/tokentool/client/PdfViewer.java @@ -0,0 +1,89 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * TokenTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.tokentool.client; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import java.io.File; +import java.io.IOException; +import java.util.ResourceBundle; +import javafx.event.EventHandler; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.scene.image.Image; +import javafx.stage.Modality; +import javafx.stage.Stage; +import javafx.stage.WindowEvent; +import net.rptools.tokentool.AppConstants; +import net.rptools.tokentool.AppPreferences; +import net.rptools.tokentool.controller.PdfViewer_Controller; +import net.rptools.tokentool.controller.TokenTool_Controller; +import net.rptools.tokentool.model.Window_Preferences; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class PdfViewer { + private static final Logger log = LogManager.getLogger(PdfViewer.class); + private Stage stage; + + public PdfViewer(File selectedPDF, TokenTool_Controller tokenTool_Controller) { + Parent root; + FXMLLoader fxmlLoader = + new FXMLLoader( + getClass().getResource(AppConstants.PDF_VIEW_FXML), + ResourceBundle.getBundle(AppConstants.TOKEN_TOOL_BUNDLE)); + + try { + root = fxmlLoader.load(); + } catch (IOException e) { + log.error("Error loading PdfViewer Stage!", e); + return; + } + + PdfViewer_Controller pdfViewerController = fxmlLoader.getController(); + + stage = new Stage(); + Scene scene = new Scene(root); + + stage.getIcons().add(new Image(getClass().getResourceAsStream(AppConstants.TOKEN_TOOL_ICON))); + stage.initModality(Modality.WINDOW_MODAL); + stage.setTitle(selectedPDF.getName()); + stage.setScene(scene); + + stage.setOnCloseRequest( + new EventHandler() { + @Override + public void handle(WindowEvent event) { + log.debug("Shutting down PDF Viewer..."); + AppPreferences.setPreference( + AppPreferences.WINDOW_PDF_PREFERENCES, new Window_Preferences(stage).toJson()); + stage.hide(); + pdfViewerController.close(); + } + }); + + String preferencesJson = + AppPreferences.getPreference(AppPreferences.WINDOW_PDF_PREFERENCES, null); + if (preferencesJson != null) { + Window_Preferences window_Preferences = + new Gson().fromJson(preferencesJson, new TypeToken() {}.getType()); + window_Preferences.setWindow(stage); + } + + stage.show(); + pdfViewerController.loadPDF(selectedPDF, tokenTool_Controller, stage); + } +} diff --git a/src/main/java/net/rptools/tokentool/client/RegionSelector.java b/src/main/java/net/rptools/tokentool/client/RegionSelector.java index 51697b6..d67d026 100644 --- a/src/main/java/net/rptools/tokentool/client/RegionSelector.java +++ b/src/main/java/net/rptools/tokentool/client/RegionSelector.java @@ -1,17 +1,22 @@ /* - * This software Copyright by the RPTools.net development team, and licensed under the Affero GPL Version 3 or, at your option, any later version. + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. * - * TokenTool Source Code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * TokenTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * - * You should have received a copy of the GNU Affero General Public License * along with this source Code. If not, please visit and specifically the Affero license text - * at . + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . */ package net.rptools.tokentool.client; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; import java.util.ResourceBundle; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; import javafx.scene.Scene; @@ -21,43 +26,59 @@ import javafx.stage.Stage; import javafx.stage.StageStyle; import net.rptools.tokentool.AppConstants; +import net.rptools.tokentool.AppPreferences; import net.rptools.tokentool.controller.RegionSelector_Controller; import net.rptools.tokentool.controller.TokenTool_Controller; +import net.rptools.tokentool.model.Window_Preferences; import net.rptools.tokentool.util.StageResizeMoveUtil; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; public class RegionSelector { - private static final Logger log = LogManager.getLogger(RegionSelector.class); - - private RegionSelector_Controller regionSelector_Controller; - private Stage stage; - - public RegionSelector(TokenTool_Controller tokenTool_Controller) { - try { - FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource(AppConstants.REGION_SELECTOR_FXML), ResourceBundle.getBundle(AppConstants.TOKEN_TOOL_BUNDLE)); - Parent root = (Parent) fxmlLoader.load(); - regionSelector_Controller = (RegionSelector_Controller) fxmlLoader.getController(); - regionSelector_Controller.setController(tokenTool_Controller); - - stage = new Stage(); - Scene scene = new Scene(root); - - scene.setFill(Color.TRANSPARENT); - // scene.setFill(new Color(0, 0, 0, 0.0)); - stage.initOwner(TokenTool.getInstance().getStage()); - stage.initStyle(StageStyle.TRANSPARENT); - stage.initModality(Modality.APPLICATION_MODAL); - stage.getIcons().add(new Image(getClass().getResourceAsStream(AppConstants.REGION_SELECTOR_ICON))); - stage.setScene(scene); - - StageResizeMoveUtil.addResizeListener(stage); - - stage.show(); - } catch (Exception e) { - log.error(e); - } - } - - public void close() { - stage.close(); - } -} \ No newline at end of file + private static final Logger log = LogManager.getLogger(RegionSelector.class); + + private RegionSelector_Controller regionSelector_Controller; + private Stage stage; + + public RegionSelector(TokenTool_Controller tokenTool_Controller) { + try { + FXMLLoader fxmlLoader = + new FXMLLoader( + getClass().getResource(AppConstants.REGION_SELECTOR_FXML), + ResourceBundle.getBundle(AppConstants.TOKEN_TOOL_BUNDLE)); + Parent root = (Parent) fxmlLoader.load(); + regionSelector_Controller = (RegionSelector_Controller) fxmlLoader.getController(); + regionSelector_Controller.setController(tokenTool_Controller); + + stage = new Stage(); + Scene scene = new Scene(root); + + scene.setFill(Color.TRANSPARENT); + // scene.setFill(new Color(0, 0, 0, 0.0)); + stage.initOwner(TokenTool.getInstance().getStage()); + stage.initStyle(StageStyle.TRANSPARENT); + stage.initModality(Modality.APPLICATION_MODAL); + stage + .getIcons() + .add(new Image(getClass().getResourceAsStream(AppConstants.REGION_SELECTOR_ICON))); + stage.setScene(scene); + + String preferencesJson = + AppPreferences.getPreference(AppPreferences.WINDOW_REGION_SELECTOR_PREFERENCES, null); + if (preferencesJson != null) { + Window_Preferences window_Preferences = + new Gson().fromJson(preferencesJson, new TypeToken() {}.getType()); + window_Preferences.setWindow(stage); + } + + StageResizeMoveUtil.addResizeListener(stage); + stage.show(); + } catch (Exception e) { + log.error("Error creating RegionSelector!", e); + } + } + + public void close() { + stage.close(); + } +} diff --git a/src/main/java/net/rptools/tokentool/client/SplashScreenLoader.java b/src/main/java/net/rptools/tokentool/client/SplashScreenLoader.java index 840c9a8..31524e6 100644 --- a/src/main/java/net/rptools/tokentool/client/SplashScreenLoader.java +++ b/src/main/java/net/rptools/tokentool/client/SplashScreenLoader.java @@ -1,15 +1,20 @@ /* - * This software Copyright by the RPTools.net development team, and licensed under the Affero GPL Version 3 or, at your option, any later version. + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. * - * TokenTool Source Code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * TokenTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * - * You should have received a copy of the GNU Affero General Public License * along with this source Code. If not, please visit and specifically the Affero license text - * at . + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . */ package net.rptools.tokentool.client; import java.util.ResourceBundle; - import javafx.animation.FadeTransition; import javafx.application.Preloader; import javafx.event.ActionEvent; @@ -17,69 +22,70 @@ import javafx.fxml.FXMLLoader; import javafx.scene.Scene; import javafx.scene.image.Image; +import javafx.scene.layout.StackPane; import javafx.stage.*; import javafx.util.Duration; +import net.rptools.tokentool.AppConstants; import net.rptools.tokentool.controller.SplashScreen_Controller; -import javafx.scene.layout.StackPane; public class SplashScreenLoader extends Preloader { - private final String TOKEN_TOOL_SPLASH_ICON = "/net/rptools/tokentool/image/token_tool_splash_icon.png"; - private final String SPLASH_SCREEN_FXML = "/net/rptools/tokentool/view/SplashScreenLoader.fxml"; - private final String TOKEN_TOOL_BUNDLE = "net.rptools.tokentool.i18n.TokenTool"; - - private Stage stage; - private StackPane root; - private SplashScreen_Controller controller; + private Stage stage; + private StackPane root; + private SplashScreen_Controller controller; - public void start(Stage stage) throws Exception { - setUserAgentStylesheet(STYLESHEET_CASPIAN); // I like the look of the this progress bar better for this screen... + public void start(Stage stage) throws Exception { + setUserAgentStylesheet( + STYLESHEET_CASPIAN); // I like the look of the this progress bar better for this screen... - FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource(SPLASH_SCREEN_FXML), ResourceBundle.getBundle(TOKEN_TOOL_BUNDLE)); - root = (StackPane) fxmlLoader.load(); - controller = (SplashScreen_Controller) fxmlLoader.getController(); - controller.setVersionLabel(TokenTool.getVersion()); + FXMLLoader fxmlLoader = + new FXMLLoader( + getClass().getResource(AppConstants.SPLASH_SCREEN_FXML), + ResourceBundle.getBundle(AppConstants.TOKEN_TOOL_BUNDLE)); + root = (StackPane) fxmlLoader.load(); + controller = (SplashScreen_Controller) fxmlLoader.getController(); + controller.setVersionLabel(TokenTool.getVersion()); - Scene scene = new Scene(root); - stage.initStyle(StageStyle.UNDECORATED); - stage.getIcons().add(new Image(getClass().getResourceAsStream(TOKEN_TOOL_SPLASH_ICON))); - stage.setScene(scene); - stage.show(); + Scene scene = new Scene(root); + stage.initStyle(StageStyle.UNDECORATED); + stage.getIcons().add(new Image(getClass().getResourceAsStream(AppConstants.TOKEN_TOOL_ICON))); + stage.setScene(scene); + stage.show(); - this.stage = stage; - } + this.stage = stage; + } - @Override - public void handleStateChangeNotification(StateChangeNotification evt) { - if (evt.getType() == StateChangeNotification.Type.BEFORE_START) { - if (stage.isShowing()) { - // fade out, hide stage at the end of animation - FadeTransition ft = new FadeTransition( - Duration.millis(1000), stage.getScene().getRoot()); - ft.setFromValue(1.0); - ft.setToValue(0.0); - final Stage s = stage; - EventHandler eh = new EventHandler() { - public void handle(ActionEvent t) { - s.hide(); - } - }; - ft.setOnFinished(eh); - ft.play(); - } else { - stage.hide(); - } - } - } + @Override + public void handleStateChangeNotification(StateChangeNotification evt) { + if (evt.getType() == StateChangeNotification.Type.BEFORE_START) { + if (stage.isShowing()) { + // fade out, hide stage at the end of animation + FadeTransition ft = new FadeTransition(Duration.millis(1000), stage.getScene().getRoot()); + ft.setFromValue(1.0); + ft.setToValue(0.0); + final Stage s = stage; + EventHandler eh = + new EventHandler() { + public void handle(ActionEvent t) { + s.hide(); + } + }; + ft.setOnFinished(eh); + ft.play(); + } else { + stage.hide(); + } + } + } - @Override - public void handleApplicationNotification(PreloaderNotification pn) { - if (pn instanceof ProgressNotification) { - // expect application to send us progress notifications with progress ranging from 0 to 1.0 - double v = ((ProgressNotification) pn).getProgress(); - controller.setLoadProgress(v); - } else if (pn instanceof StateChangeNotification) { - // hide after get any state update from application - stage.hide(); - } - } -} \ No newline at end of file + @Override + public void handleApplicationNotification(PreloaderNotification pn) { + if (pn instanceof ProgressNotification) { + // expect application to send us progress notifications with progress ranging from 0 to 1.0 + double v = ((ProgressNotification) pn).getProgress(); + controller.setLoadProgress(v); + } else if (pn instanceof StateChangeNotification) { + // hide after get any state update from application + stage.hide(); + } + } +} diff --git a/src/main/java/net/rptools/tokentool/client/TokenTool.java b/src/main/java/net/rptools/tokentool/client/TokenTool.java index d0e0837..068b0b8 100644 --- a/src/main/java/net/rptools/tokentool/client/TokenTool.java +++ b/src/main/java/net/rptools/tokentool/client/TokenTool.java @@ -1,35 +1,30 @@ /* - * This software Copyright by the RPTools.net development team, and licensed under the Affero GPL Version 3 or, at your option, any later version. + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. * - * TokenTool Source Code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * TokenTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * - * You should have received a copy of the GNU Affero General Public License * along with this source Code. If not, please visit and specifically the Affero license text - * at . + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . */ package net.rptools.tokentool.client; +import io.sentry.Sentry; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.Comparator; import java.util.ResourceBundle; - -import org.apache.commons.cli.CommandLine; -import org.apache.commons.cli.CommandLineParser; -import org.apache.commons.cli.DefaultParser; -import org.apache.commons.cli.Options; -import org.apache.commons.cli.ParseException; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.core.Appender; -import org.apache.logging.log4j.core.appender.FileAppender; - import javafx.application.Application; import javafx.application.ConditionalFeature; import javafx.application.Platform; import javafx.application.Preloader; -import javafx.event.EventHandler; import javafx.fxml.FXMLLoader; import javafx.scene.Scene; import javafx.scene.control.TreeItem; @@ -37,264 +32,267 @@ import javafx.scene.image.ImageView; import javafx.scene.layout.BorderPane; import javafx.stage.Stage; -import javafx.stage.WindowEvent; +import javax.imageio.spi.IIORegistry; import net.rptools.tokentool.AppConstants; import net.rptools.tokentool.AppPreferences; import net.rptools.tokentool.AppSetup; import net.rptools.tokentool.controller.TokenTool_Controller; import net.rptools.tokentool.util.I18N; import net.rptools.tokentool.util.ImageUtil; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.ThreadContext; +import org.apache.logging.log4j.core.Appender; +import org.apache.logging.log4j.core.appender.FileAppender; /** - * * @author Jamz - * - * To see splashscreen during testing, use JVM arg: -Djavafx.preloader=net.rptools.tokentool.client.SplashScreenLoader Otherwise splashscreen will only show when defined as - * JavaFX-Preloader-Class in the JAR manifest. - * + *

To see splashscreen during testing, use JVM arg: + * -Djavafx.preloader=net.rptools.tokentool.client.SplashScreenLoader Otherwise splashscreen + * will only show when defined as JavaFX-Preloader-Class in the JAR manifest. */ public class TokenTool extends Application { - private static TokenTool appInstance; - - private static Logger log; // Don't instantiate until AppSetup gets and sets user_home/logs directory in AppSetup - - private static BorderPane root; - private static TokenTool_Controller tokentool_Controller; - - private static String VERSION = ""; - private static String VENDOR = ""; - - private static final int THUMB_SIZE = 100; - - private static int overlayCount = 0; - private static int loadCount = 1; - private static double deltaX = 0; - private static double deltaY = 0; - - private static TreeItem overlayTreeItems; - private static Stage stage; - - @Override - public void init() throws Exception { - appInstance = this; - VERSION = getVersion(); - - // Lets install/update the overlays if newer version - AppSetup.install(VERSION); - log = LogManager.getLogger(TokenTool.class); - log.info("3D Hardware Available? " + Platform.isSupported(ConditionalFeature.SCENE3D)); - - // Now lets cache any overlays we find and update preLoader with progress - overlayCount = (int) Files.walk(AppConstants.OVERLAY_DIR.toPath()).filter(Files::isRegularFile).count(); - overlayTreeItems = cacheOverlays(AppConstants.OVERLAY_DIR, null, THUMB_SIZE); - - // All Done! - notifyPreloader(new Preloader.ProgressNotification(1.0)); - } - - @Override - public void start(Stage primaryStage) throws IOException { - stage = primaryStage; - setUserAgentStylesheet(STYLESHEET_MODENA); // Setting the style back to the new Modena - FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource(AppConstants.TOKEN_TOOL_FXML), ResourceBundle.getBundle(AppConstants.TOKEN_TOOL_BUNDLE)); - root = fxmlLoader.load(); - tokentool_Controller = (TokenTool_Controller) fxmlLoader.getController(); - - Scene scene = new Scene(root); - primaryStage.setTitle(I18N.getString("TokenTool.stage.title")); - primaryStage.getIcons().add(new Image(getClass().getResourceAsStream(AppConstants.TOKEN_TOOL_ICON))); - primaryStage.setScene(scene); - - primaryStage.widthProperty().addListener((obs, oldVal, newVal) -> { - if (Double.isNaN(oldVal.doubleValue())) - return; - - deltaX += newVal.doubleValue() - oldVal.doubleValue(); - - // Only adjust on even width adjustments - if (deltaX > 1 || deltaX < -1) { - if (deltaX % 2 == 0) { - tokentool_Controller.updatePortraitLocation(deltaX, 0); - deltaX = 0; - } else { - tokentool_Controller.updatePortraitLocation(deltaX - 1, 0); - deltaX = 1; - } - } - }); - - primaryStage.heightProperty().addListener((obs, oldVal, newVal) -> { - if (Double.isNaN(oldVal.doubleValue())) - return; - - deltaY += newVal.doubleValue() - oldVal.doubleValue(); - - // Only adjust on even width adjustments - if (deltaY > 1 || deltaY < -1) { - if (deltaY % 2 == 0) { - tokentool_Controller.updatePortraitLocation(0, deltaY); - deltaY = 0; - } else { - tokentool_Controller.updatePortraitLocation(0, deltaY - 1); - deltaY = 1; - } - } - }); - - primaryStage.setOnCloseRequest(new EventHandler() { - @Override - public void handle(WindowEvent event) { - tokentool_Controller.exitApplication(); - } - }); - - // Load all the overlays into the treeview - tokentool_Controller.updateOverlayTreeview(overlayTreeItems); - - // Restore saved settings - AppPreferences.restorePreferences(tokentool_Controller); - - // Add recent list to treeview - tokentool_Controller.updateOverlayTreeViewRecentFolder(true); - - // Set the Overlay Options accordion to be default open view - tokentool_Controller.expandOverlayOptionsPane(true); - - primaryStage.show(); - - // Finally, update token preview image after everything is done loading - Platform.runLater(() -> tokentool_Controller.updateTokenPreviewImageView()); - } - - public static TokenTool getInstance() { - return appInstance; - } - - public Stage getStage() { - return stage; - } - - /** - * - * @author Jamz - * @throws IOException - * @since 2.0 - * - * This method loads and processes all the overlays found in user.home/overlays and it can take a minute to load as it creates thumbnail versions for the comboBox so we call this during the - * init and display progress in the preLoader (splash screen). - * - */ - private TreeItem cacheOverlays(File dir, TreeItem parent, int THUMB_SIZE) throws IOException { - TreeItem root = new TreeItem<>(dir.toPath()); - root.setExpanded(false); - - log.debug("caching " + dir.getAbsolutePath()); - - File[] files = dir.listFiles(); - for (File file : files) { - if (file.isDirectory()) { - cacheOverlays(file, root, THUMB_SIZE); - } else { - Path filePath = file.toPath(); - TreeItem imageNode = new TreeItem<>(filePath, ImageUtil.getOverlayThumb(new ImageView(), filePath)); - root.getChildren().add(imageNode); - - notifyPreloader(new Preloader.ProgressNotification((double) loadCount++ / overlayCount)); - } - } - - if (parent != null) { - // When we show the overlay image, the TreeItem value is "" so we need to - // sort those to the bottom for a cleaner look and keep sub dir's at the top. - // If a node has no children then it's an overlay, otherwise it's a directory... - root.getChildren().sort(new Comparator>() { - @Override - public int compare(TreeItem o1, TreeItem o2) { - if (o1.getChildren().size() == 0 && o2.getChildren().size() == 0) - return 0; - else if (o1.getChildren().size() == 0) - return Integer.MAX_VALUE; - else if (o2.getChildren().size() == 0) - return Integer.MIN_VALUE; - else - return o1.getValue().compareTo(o2.getValue()); - } - }); - - parent.getChildren().add(root); - } - - return root; - } - - public static String getVersion() { - if (!VERSION.isEmpty()) - return VERSION; - - VERSION = "DEVELOPMENT"; - - if (TokenTool.class.getPackage().getImplementationVersion() != null) { - VERSION = TokenTool.class.getPackage().getImplementationVersion().trim(); - } - - return VERSION; - } - - public static String getVendor() { - if (!VENDOR.isEmpty()) - return VENDOR; - - if (TokenTool.class.getPackage().getImplementationVendor() != null) { - VENDOR = TokenTool.class.getPackage().getImplementationVendor().trim(); - } - - return VENDOR; - } - - private static String getCommandLineStringOption(Options options, String searchValue, String[] args) { - CommandLineParser parser = new DefaultParser(); - - try { - CommandLine cmd = parser.parse(options, args); - - if (cmd.hasOption(searchValue)) { - return cmd.getOptionValue(searchValue); - } - } catch (ParseException e1) { - // TODO Auto-generated catch block - e1.printStackTrace(); - } - - return ""; - } - - public static String getLoggerFileName() { - org.apache.logging.log4j.core.Logger loggerImpl = (org.apache.logging.log4j.core.Logger) log; - Appender appender = loggerImpl.getAppenders().get("LogFile"); - - if (appender != null) - return ((FileAppender) appender).getFileName(); - else - return "NOT_CONFIGURED"; - } - - /** - * Legacy, it simply launches the FX Application which calls init() then start() - * - * @author Jamz - * @since 2.0 - * - * @param args - * the command line arguments - */ - public static void main(String[] args) { - Options cmdOptions = new Options(); - cmdOptions.addOption("v", "version", true, "override version number"); //$NON-NLS-2$ //$NON-NLS-3$ - cmdOptions.addOption("n", "vendor", true, "override vendor"); //$NON-NLS-2$ //$NON-NLS-3$ - - VERSION = getCommandLineStringOption(cmdOptions, "version", args); - VENDOR = getCommandLineStringOption(cmdOptions, "vendor", args); - - launch(args); - } -} \ No newline at end of file + private static TokenTool appInstance; + + private static Logger + log; // Don't instantiate until AppSetup gets and sets user_home/logs directory in AppSetup + + private static BorderPane root; + private static TokenTool_Controller tokentool_Controller; + + private static String VERSION = ""; + private static String VENDOR = ""; + + private static final int THUMB_SIZE = 100; + + private static int overlayCount = 0; + private static int loadCount = 1; + + private static TreeItem overlayTreeItems; + private static Stage stage; + + static { + // This will inject additional data tags in log4j2 which will be picked up by Sentry.io + System.setProperty("log4j2.isThreadContextMapInheritable", "true"); + ThreadContext.put( + "OS", System.getProperty("os.name")); // Added to the JavaFX Application Thread thread... + } + + @Override + public void init() throws Exception { + // Since we are using multiple plugins (Twelve Monkeys for PSD and JAI for jpeg2000) in the same + // uber jar, + // the META-INF/services/javax.imageio.spi.ImageReaderSpi gets overwritten. So we need to + // register them manually: + // https://github.com/jai-imageio/jai-imageio-core/issues/29 + IIORegistry registry = IIORegistry.getDefaultInstance(); + registry.registerServiceProvider(new com.github.jaiimageio.jpeg2000.impl.J2KImageReaderSpi()); + + appInstance = this; + VERSION = getVersion(); + + // Lets install/update the overlays if newer version + AppSetup.install(VERSION); + log = LogManager.getLogger(TokenTool.class); + + // Log some basic info + log.info("Environment: " + Sentry.getStoredClient().getEnvironment()); + if (!Sentry.getStoredClient().getEnvironment().toLowerCase().equals("production")) + log.info("Not in Production mode and thus will not log any events to Sentry.io"); + + log.info("Release: " + Sentry.getStoredClient().getRelease()); + log.info("OS: " + ThreadContext.get("OS")); + log.info("3D Hardware Available? " + Platform.isSupported(ConditionalFeature.SCENE3D)); + + // Now lets cache any overlays we find and update preLoader with progress + overlayCount = + (int) Files.walk(AppConstants.OVERLAY_DIR.toPath()).filter(Files::isRegularFile).count(); + overlayTreeItems = cacheOverlays(AppConstants.OVERLAY_DIR, null, THUMB_SIZE); + + // All Done! + notifyPreloader(new Preloader.ProgressNotification(1.0)); + } + + @Override + public void start(Stage primaryStage) { + stage = primaryStage; + setUserAgentStylesheet(STYLESHEET_MODENA); // Setting the style back to the new Modena + FXMLLoader fxmlLoader = + new FXMLLoader( + getClass().getResource(AppConstants.TOKEN_TOOL_FXML), + ResourceBundle.getBundle(AppConstants.TOKEN_TOOL_BUNDLE)); + + try { + root = fxmlLoader.load(); + } catch (IOException e) { + log.error("Error loading " + AppConstants.TOKEN_TOOL_FXML, e); + } + + tokentool_Controller = (TokenTool_Controller) fxmlLoader.getController(); + + Scene scene = new Scene(root); + primaryStage.setTitle(I18N.getString("TokenTool.stage.title")); + primaryStage + .getIcons() + .add(new Image(getClass().getResourceAsStream(AppConstants.TOKEN_TOOL_ICON))); + primaryStage.setScene(scene); + + // Load all the overlays into the treeview + tokentool_Controller.updateOverlayTreeview(overlayTreeItems); + + // Restore saved settings + AppPreferences.restorePreferences(tokentool_Controller); + tokentool_Controller.updateTokenPreviewImageView(); + + // Add recent list to treeview + tokentool_Controller.updateOverlayTreeViewRecentFolder(true); + + // Set the Overlay Options accordion to be default open view + tokentool_Controller.expandOverlayOptionsPane(true); + + primaryStage.setOnCloseRequest(e -> tokentool_Controller.exitApplication()); + primaryStage.show(); + + // Finally, update token preview image after everything is done loading + Platform.runLater(() -> tokentool_Controller.updateTokenPreviewImageView()); + } + + @Override + public void stop() { + // Make sure any hanging threads are closed as well... + System.exit(0); + } + + public static TokenTool getInstance() { + return appInstance; + } + + public Stage getStage() { + return stage; + } + + /** + * @author Jamz + * @throws IOException + * @since 2.0 + *

This method loads and processes all the overlays found in user.home/overlays and it can + * take a minute to load as it creates thumbnail versions for the comboBox so we call this + * during the init and display progress in the preLoader (splash screen). + */ + private TreeItem cacheOverlays(File dir, TreeItem parent, int THUMB_SIZE) + throws IOException { + TreeItem root = new TreeItem<>(dir.toPath()); + root.setExpanded(false); + + log.debug("caching " + dir.getAbsolutePath()); + + File[] files = dir.listFiles(); + for (File file : files) { + if (file.isDirectory()) { + cacheOverlays(file, root, THUMB_SIZE); + } else { + Path filePath = file.toPath(); + TreeItem imageNode = + new TreeItem<>(filePath, ImageUtil.getOverlayThumb(new ImageView(), filePath)); + root.getChildren().add(imageNode); + + notifyPreloader(new Preloader.ProgressNotification((double) loadCount++ / overlayCount)); + } + } + + if (parent != null) { + // When we show the overlay image, the TreeItem value is "" so we need to + // sort those to the bottom for a cleaner look and keep sub dir's at the top. + // If a node has no children then it's an overlay, otherwise it's a directory... + root.getChildren() + .sort( + new Comparator>() { + @Override + public int compare(TreeItem o1, TreeItem o2) { + if (o1.getChildren().size() == 0 && o2.getChildren().size() == 0) return 0; + else if (o1.getChildren().size() == 0) return Integer.MAX_VALUE; + else if (o2.getChildren().size() == 0) return Integer.MIN_VALUE; + else return o1.getValue().compareTo(o2.getValue()); + } + }); + + parent.getChildren().add(root); + } + + return root; + } + + public static String getVersion() { + if (!VERSION.isEmpty()) return VERSION; + + VERSION = "DEVELOPMENT"; + + if (TokenTool.class.getPackage().getImplementationVersion() != null) { + VERSION = TokenTool.class.getPackage().getImplementationVersion().trim(); + } + + return VERSION; + } + + public static String getVendor() { + if (!VENDOR.isEmpty()) return VENDOR; + + if (TokenTool.class.getPackage().getImplementationVendor() != null) { + VENDOR = TokenTool.class.getPackage().getImplementationVendor().trim(); + } + + return VENDOR; + } + + private static String getCommandLineStringOption( + Options options, String searchValue, String[] args) { + CommandLineParser parser = new DefaultParser(); + + try { + CommandLine cmd = parser.parse(options, args); + + if (cmd.hasOption(searchValue)) { + return cmd.getOptionValue(searchValue); + } + } catch (ParseException e1) { + // We don't have the logger instance at this point yet... + e1.printStackTrace(); + } + + return ""; + } + + public static String getLoggerFileName() { + org.apache.logging.log4j.core.Logger loggerImpl = (org.apache.logging.log4j.core.Logger) log; + Appender appender = loggerImpl.getAppenders().get("LogFile"); + + if (appender != null) return ((FileAppender) appender).getFileName(); + else return "NOT_CONFIGURED"; + } + + /** + * Legacy, it simply launches the FX Application which calls init() then start(). Also sets/calls + * the preloader class + * + * @author Jamz + * @since 2.0 + * @param args the command line arguments + */ + public static void main(String[] args) { + Options cmdOptions = new Options(); + cmdOptions.addOption("v", "version", true, "override version number"); + cmdOptions.addOption("n", "vendor", true, "override vendor"); + + VERSION = getCommandLineStringOption(cmdOptions, "version", args); + VENDOR = getCommandLineStringOption(cmdOptions, "vendor", args); + + System.setProperty("javafx.preloader", "net.rptools.tokentool.client.SplashScreenLoader"); + + launch(args); + } +} diff --git a/src/main/java/net/rptools/tokentool/controller/Credits_Controller.java b/src/main/java/net/rptools/tokentool/controller/Credits_Controller.java index 63036e3..f94382b 100644 --- a/src/main/java/net/rptools/tokentool/controller/Credits_Controller.java +++ b/src/main/java/net/rptools/tokentool/controller/Credits_Controller.java @@ -1,40 +1,51 @@ /* - * This software Copyright by the RPTools.net development team, and licensed under the Affero GPL Version 3 or, at your option, any later version. + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. * - * TokenTool Source Code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * TokenTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * - * You should have received a copy of the GNU Affero General Public License * along with this source Code. If not, please visit and specifically the Affero license text - * at . + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . */ package net.rptools.tokentool.controller; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.Hyperlink; import javafx.scene.control.Label; import net.rptools.tokentool.AppConstants; import net.rptools.tokentool.client.TokenTool; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; public class Credits_Controller { - private static final Logger log = LogManager.getLogger(Credits_Controller.class); + private static final Logger log = LogManager.getLogger(Credits_Controller.class); - @FXML private Hyperlink rptoolsHyperlink; - @FXML private Label versionLabel; + @FXML private Hyperlink rptoolsHyperlink; + @FXML private Label versionLabel; - @FXML - void initialize() { - assert rptoolsHyperlink != null : "fx:id=\"rptoolsHyperlink\" was not injected: check your FXML file 'Credits.fxml'."; - assert versionLabel != null : "fx:id=\"versionLabel\" was not injected: check your FXML file 'Credits.fxml'."; + @FXML + void initialize() { + assert rptoolsHyperlink != null + : "fx:id=\"rptoolsHyperlink\" was not injected: check your FXML file '" + + AppConstants.CREDITS_FXML + + "'."; + assert versionLabel != null + : "fx:id=\"versionLabel\" was not injected: check your FXML file '" + + AppConstants.CREDITS_FXML + + "'."; - versionLabel.setText(versionLabel.getText() + " " + TokenTool.getVersion()); - } + versionLabel.setText(versionLabel.getText() + " " + TokenTool.getVersion()); + } - @FXML - void rptoolsHyperlink_onAction(ActionEvent event) { - log.info("Launching browser for URL " + AppConstants.RPTOOLS_URL); - TokenTool.getInstance().getHostServices().showDocument(AppConstants.RPTOOLS_URL); - } + @FXML + void rptoolsHyperlink_onAction(ActionEvent event) { + log.info("Launching browser for URL " + AppConstants.RPTOOLS_URL); + TokenTool.getInstance().getHostServices().showDocument(AppConstants.RPTOOLS_URL); + } } diff --git a/src/main/java/net/rptools/tokentool/controller/ManageOverlays_Controller.java b/src/main/java/net/rptools/tokentool/controller/ManageOverlays_Controller.java index 8a3cd8f..9120760 100644 --- a/src/main/java/net/rptools/tokentool/controller/ManageOverlays_Controller.java +++ b/src/main/java/net/rptools/tokentool/controller/ManageOverlays_Controller.java @@ -1,10 +1,16 @@ /* - * This software Copyright by the RPTools.net development team, and licensed under the Affero GPL Version 3 or, at your option, any later version. + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. * - * TokenTool Source Code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * TokenTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * - * You should have received a copy of the GNU Affero General Public License * along with this source Code. If not, please visit and specifically the Affero license text - * at . + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . */ package net.rptools.tokentool.controller; @@ -16,15 +22,6 @@ import java.util.Optional; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; - -import javax.imageio.ImageIO; - -import org.apache.commons.io.FileUtils; -import org.apache.commons.io.FilenameUtils; -import org.apache.commons.io.filefilter.TrueFileFilter; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - import javafx.application.Platform; import javafx.concurrent.Task; import javafx.embed.swing.SwingFXUtils; @@ -53,384 +50,452 @@ import javafx.scene.layout.VBox; import javafx.stage.FileChooser; import javafx.stage.Stage; +import javax.imageio.ImageIO; import net.rptools.tokentool.AppConstants; import net.rptools.tokentool.AppSetup; import net.rptools.tokentool.model.OverlayTreeItem; import net.rptools.tokentool.util.FileSaveUtil; import net.rptools.tokentool.util.I18N; import net.rptools.tokentool.util.ImageUtil; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.io.filefilter.TrueFileFilter; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; public class ManageOverlays_Controller { - private static final Logger log = LogManager.getLogger(ManageOverlays_Controller.class); - private static Thread loadOverlaysThread = new Thread(); - private static ExecutorService executorService; - private static File currentDirectory; - private static File lastSelectedDirectory; - private static ToggleGroup overlayToggleGroup = new ToggleGroup(); - - @FXML private FlowPane overlayViewFlowPane; - @FXML private TreeView overlayTreeView; - @FXML private VBox detailsVBox; - @FXML private Label overlayName; - @FXML private Label overlayDescription; - @FXML private Label overlayDimensions; - @FXML private ImageView overlayLayerImage; - @FXML private ImageView overlayLayerMask; - @FXML private Button addOverlayButton; - @FXML private Button deleteOverlayButton; - @FXML private Button addFolderButton; - @FXML private Button deleteFolderButton; - @FXML private Button restoreButton; - - @FXML - void initialize() { - assert overlayViewFlowPane != null : "fx:id=\"overlayViewFlowPane\" was not injected: check your FXML file 'ManageOverlays.fxml'."; - assert overlayTreeView != null : "fx:id=\"overlayTreeView\" was not injected: check your FXML file 'ManageOverlays.fxml'."; - assert detailsVBox != null : "fx:id=\"detailsVBox\" was not injected: check your FXML file 'ManageOverlays.fxml'."; - assert overlayName != null : "fx:id=\"overlayName\" was not injected: check your FXML file 'ManageOverlays.fxml'."; - assert overlayDescription != null : "fx:id=\"overlayDescription\" was not injected: check your FXML file 'ManageOverlays.fxml'."; - assert overlayDimensions != null : "fx:id=\"overlayDimensions\" was not injected: check your FXML file 'ManageOverlays.fxml'."; - assert overlayLayerImage != null : "fx:id=\"overlayLayerImage\" was not injected: check your FXML file 'ManageOverlays.fxml'."; - assert overlayLayerMask != null : "fx:id=\"overlayLayerMask\" was not injected: check your FXML file 'ManageOverlays.fxml'."; - assert addOverlayButton != null : "fx:id=\"addOverlayButton\" was not injected: check your FXML file 'ManageOverlays.fxml'."; - assert deleteOverlayButton != null : "fx:id=\"deleteOverlayButton\" was not injected: check your FXML file 'ManageOverlays.fxml'."; - assert addFolderButton != null : "fx:id=\"addFolderButton\" was not injected: check your FXML file 'ManageOverlays.fxml'."; - assert deleteFolderButton != null : "fx:id=\"deleteFolderButton\" was not injected: check your FXML file 'ManageOverlays.fxml'."; - assert restoreButton != null : "fx:id=\"restoreButton\" was not injected: check your FXML file 'ManageOverlays.fxml'."; - - executorService = Executors.newSingleThreadScheduledExecutor(runable -> { - loadOverlaysThread = Executors.defaultThreadFactory().newThread(runable); - loadOverlaysThread.setDaemon(true); - return loadOverlaysThread; - }); - - // Add a listener to the TreeView - overlayTreeView.getSelectionModel() - .selectedItemProperty() - .addListener((observable, oldValue, newValue) -> loadImages(newValue)); - - displayTreeView(); - } - - public void displayTreeView() { - TreeItem root = new OverlayTreeItem(AppConstants.OVERLAY_DIR); - root.setExpanded(true); - overlayTreeView.setRoot(root); - - overlayTreeView.setCellFactory(treeView -> new TreeCell() { - @Override - public void updateItem(Path path, boolean empty) { - super.updateItem(path, empty); - if (empty) { - setText(null); - } else { - setText(path.getFileName().toString()); - } - } - }); - } - - private void loadImages(TreeItem treeItem) { - overlayViewFlowPane.getChildren().clear(); - if (treeItem != null) - loadImages(treeItem.getValue().toFile()); - } - - private void loadImages(File dir) { - // Clear Details panel - clearDetails(); - - currentDirectory = dir; - File[] files = dir.listFiles(ImageUtil.SUPPORTED_FILENAME_FILTER); - - Task task = new Task() { - @Override - public Void call() { - for (File file : files) { - Path filePath = file.toPath(); - - if (loadOverlaysThread.isInterrupted()) { - Platform.runLater(() -> overlayViewFlowPane.getChildren().clear()); - break; - } - - try { - ToggleButton overlayButton = new ToggleButton(); - ImageView imageViewNode = ImageUtil.getOverlayThumb(new ImageView(), filePath); - - overlayButton.getStyleClass().add("overlay-toggle-button"); - overlayButton.setGraphic(imageViewNode); - overlayButton.setUserData(file); - overlayButton.setToggleGroup(overlayToggleGroup); - - overlayButton.addEventHandler(ActionEvent.ACTION, event -> { - // No modifier keys used so add toggle group back to all buttons - resetToggleGroup(); - - // Also set button to selected due to resetting toggle groups & no unselecting needed, makes for better interface IMO - overlayButton.setSelected(true); - - // Update the Details panel with the last selected overlay - File overlayFile = (File) overlayButton.getUserData(); - updateDetails(overlayFile, (ImageView) overlayButton.getGraphic(), overlayButton.isSelected()); - - // Consume the event, no more logic needed - event.consume(); - }); - - overlayButton.setOnMouseClicked(new EventHandler() { - @Override - public void handle(MouseEvent event) { - // Allow multiple selections if shortcutKey+left_mouse is pressed - if (event.getButton().equals(MouseButton.PRIMARY) && event.isShortcutDown()) { - // Update the Details panel with the last selected overlay - File overlayFile = (File) overlayButton.getUserData(); - updateDetails(overlayFile, (ImageView) overlayButton.getGraphic(), true); - - // Remove the toggle group to allow multiple toggle button selection - overlayButton.setToggleGroup(null); - - // Select the button - overlayButton.setSelected(true); - - // Consume the event, no more logic needed - event.consume(); - } - } - }); - - Platform.runLater(() -> overlayViewFlowPane.getChildren().add(overlayButton)); - } catch (IOException e) { - log.error("Loading image: " + filePath.getFileName(), e); - } - - } - - return null; - } - }; - - loadOverlaysThread.interrupt(); - executorService.execute(task); - } - - private void updateDetails(File overlayFile, ImageView overlayImage, boolean selected) { - if (selected) { - int w = (int) overlayImage.getImage().getWidth(); - int h = (int) overlayImage.getImage().getHeight(); - - overlayName.setText(FilenameUtils.getBaseName(overlayFile.getName())); - overlayDescription.setText(ImageUtil.getFileType(overlayFile)); - overlayDimensions.setText(w + " x " + h); - overlayLayerImage.setImage(overlayImage.getImage()); - ; - - try { - overlayLayerMask = ImageUtil.getMaskImage(overlayLayerMask, overlayFile.toPath()); - } catch (IOException e) { - log.error("Updating details for: " + overlayFile.getAbsolutePath(), e); - } - } else { - clearDetails(); - } - } - - private void clearDetails() { - overlayName.setText(""); - overlayDescription.setText(""); - overlayDimensions.setText(""); - overlayLayerImage.setImage(null); - overlayLayerMask.setImage(null); - - } - - private void resetToggleGroup() { - for (Node overlay : overlayViewFlowPane.getChildren()) { - ToggleButton overlayButton = (ToggleButton) overlay; - if (overlayButton.getToggleGroup() == null) - overlayButton.setToggleGroup(overlayToggleGroup); - } - } - - private boolean confirmDelete(LinkedList overlayFiles) { - String confirmationText = I18N.getString("ManageOverlays.dialog.delete.confirmation"); - - if (overlayFiles.isEmpty()) - return false; - else if (overlayFiles.size() == 1) { - confirmationText += overlayFiles.get(0).getName() + "?"; - } else { - confirmationText += I18N.getString("ManageOverlays.dialog.delete.confirmation.these") + overlayFiles.size() + I18N.getString("ManageOverlays.dialog.delete.confirmation.overlays"); - } - - Alert alert = new Alert(AlertType.CONFIRMATION); - alert.setTitle(I18N.getString("ManageOverlays.dialog.delete.title")); - alert.setContentText(confirmationText); - - Optional result = alert.showAndWait(); - - if ((result.isPresent()) && (result.get() == ButtonType.OK)) { - return true; - } - - return false; - } - - private boolean confirmDelete(File dir) { - String confirmationText = I18N.getString("ManageOverlays.dialog.delete.dir.confirmation"); - long dirSize = FileUtils.listFiles(dir, ImageUtil.SUPPORTED_FILE_FILTER, TrueFileFilter.INSTANCE).size(); - - if (dirSize == 0) { - confirmationText += dir.getName() + I18N.getString("ManageOverlays.dialog.delete.dir.confirmation.directory"); - } else { - confirmationText += dir.getName() + I18N.getString("ManageOverlays.dialog.delete.dir.directory_containing") + dirSize + I18N.getString("ManageOverlays.dialog.delete.dir.overlays"); - } - - Alert alert = new Alert(AlertType.CONFIRMATION); - alert.setTitle(I18N.getString("ManageOverlays.dialog.delete.dir.title")); - alert.setContentText(confirmationText); - - Optional result = alert.showAndWait(); - - if ((result.isPresent()) && (result.get() == ButtonType.OK)) { - return true; - } - - return false; - } - - @FXML - void deleteOverlayButton_onAction(ActionEvent event) { - LinkedList overlayFiles = new LinkedList(); - - for (Node overlay : overlayViewFlowPane.getChildren()) { - ToggleButton overlayButton = (ToggleButton) overlay; - if (overlayButton.isSelected()) - overlayFiles.add((File) overlayButton.getUserData()); - } - - if (confirmDelete(overlayFiles)) { - for (File file : overlayFiles) { - log.info("Deleting: " + file.getName()); - file.delete(); - } - - loadImages(overlayTreeView.getSelectionModel().getSelectedItem()); - } - } - - @FXML - void deleteFolderButton_onAction(ActionEvent event) { - if (currentDirectory.equals(AppConstants.OVERLAY_DIR)) - return; - - if (confirmDelete(currentDirectory)) { - try { - FileUtils.forceDelete(currentDirectory); - } catch (IOException e) { - log.info("Deleting: " + currentDirectory.getAbsolutePath()); - } - - displayTreeView(); - } - } - - @FXML - void addOverlayButton_onAction(ActionEvent event) { - FileChooser fileChooser = new FileChooser(); - fileChooser.setTitle(I18N.getString("ManageOverlays.filechooser.overlay.title")); - fileChooser.getExtensionFilters().addAll(ImageUtil.GET_EXTENSION_FILTERS()); - - if (lastSelectedDirectory != null) - fileChooser.setInitialDirectory(lastSelectedDirectory); - - List selectedFiles = fileChooser.showOpenMultipleDialog((Stage) addOverlayButton.getScene().getWindow()); - - if (selectedFiles != null) { - for (File selectedFile : selectedFiles) { - FileSaveUtil.copyFile(selectedFile, currentDirectory); - } - - lastSelectedDirectory = selectedFiles.get(0).getParentFile(); - loadImages(overlayTreeView.getSelectionModel().getSelectedItem()); - } - } - - @FXML - void addFolderButton_onAction(ActionEvent event) { - TextInputDialog dialog = new TextInputDialog(); - dialog.setTitle(I18N.getString("ManageOverlays.filechooser.folder.title")); - dialog.setContentText(I18N.getString("ManageOverlays.filechooser.folder.content_text")); - - Optional result = dialog.showAndWait(); - result.ifPresent(name -> { - if (FileSaveUtil.makeDir(name, currentDirectory)) { - displayTreeView(); - } - ; - }); - } - - @FXML - void restoreButton_onAction(ActionEvent event) { - Alert alert = new Alert(AlertType.CONFIRMATION); - alert.setTitle(I18N.getString("ManageOverlays.dialog.restore.overlays.title")); - alert.setContentText(I18N.getString("ManageOverlays.dialog.restore.overlays.content_text")); - - Optional result = alert.showAndWait(); - - if ((result.isPresent()) && (result.get() == ButtonType.OK)) { - log.info("Restoring default images..."); - try { - AppSetup.installDefaultOverlays(); - } catch (IOException e) { - log.error("Error restoring default overlays!", e); - } - - displayTreeView(); - } - } - - @FXML - void overlayViewFlowPane_DragDone(DragEvent event) { - loadImages(overlayTreeView.getSelectionModel().getSelectedItem()); - } - - @FXML - void overlayViewFlowPane_DragDropped(DragEvent event) { - Dragboard db = event.getDragboard(); - if (db.hasImage()) { - try { - // Prompt for name & return file name - File newOverlayFile = new File(currentDirectory.getCanonicalPath() + "/somefilename.png"); - ImageIO.write(SwingFXUtils.fromFXImage(db.getImage(), null), "png", newOverlayFile); - } catch (IOException e) { - log.error("Error writing new overlay image.", e); - } - - loadImages(overlayTreeView.getSelectionModel().getSelectedItem()); - event.setDropCompleted(true); - } else if (db.hasFiles()) { - db.getFiles().forEach(file -> { - FileSaveUtil.copyFile(file, currentDirectory); - }); - loadImages(overlayTreeView.getSelectionModel().getSelectedItem()); - event.setDropCompleted(true); - } else if (db.hasUrl()) { - FileSaveUtil.copyFile(new File(db.getUrl()), currentDirectory); - loadImages(overlayTreeView.getSelectionModel().getSelectedItem()); - event.setDropCompleted(true); - } - } - - @FXML - void overlayViewFlowPane_DragOver(DragEvent event) { - if (event.getDragboard().hasImage() || event.getDragboard().hasFiles() || event.getDragboard().hasUrl()) { - // TODO: Set Pane color to an alpha green - event.acceptTransferModes(TransferMode.COPY); - } else { - // TODO: Set Pane color to an alpha red? - event.acceptTransferModes(TransferMode.ANY); - } - } + private static final Logger log = LogManager.getLogger(ManageOverlays_Controller.class); + private static Thread loadOverlaysThread = new Thread(); + private static ExecutorService executorService; + private static File currentDirectory; + private static File lastSelectedDirectory; + private static ToggleGroup overlayToggleGroup = new ToggleGroup(); + + @FXML private FlowPane overlayViewFlowPane; + @FXML private TreeView overlayTreeView; + @FXML private VBox detailsVBox; + @FXML private Label overlayName; + @FXML private Label overlayDescription; + @FXML private Label overlayDimensions; + @FXML private ImageView overlayLayerImage; + @FXML private ImageView overlayLayerMask; + @FXML private Button addOverlayButton; + @FXML private Button deleteOverlayButton; + @FXML private Button addFolderButton; + @FXML private Button deleteFolderButton; + @FXML private Button restoreButton; + + @FXML + void initialize() { + assert overlayViewFlowPane != null + : "fx:id=\"overlayViewFlowPane\" was not injected: check your FXML file '" + + AppConstants.MANAGE_OVERLAYS_FXML + + "'."; + assert overlayTreeView != null + : "fx:id=\"overlayTreeView\" was not injected: check your FXML file '" + + AppConstants.MANAGE_OVERLAYS_FXML + + "'."; + assert detailsVBox != null + : "fx:id=\"detailsVBox\" was not injected: check your FXML file '" + + AppConstants.MANAGE_OVERLAYS_FXML + + "'."; + assert overlayName != null + : "fx:id=\"overlayName\" was not injected: check your FXML file '" + + AppConstants.MANAGE_OVERLAYS_FXML + + "'."; + assert overlayDescription != null + : "fx:id=\"overlayDescription\" was not injected: check your FXML file '" + + AppConstants.MANAGE_OVERLAYS_FXML + + "'."; + assert overlayDimensions != null + : "fx:id=\"overlayDimensions\" was not injected: check your FXML file '" + + AppConstants.MANAGE_OVERLAYS_FXML + + "'."; + assert overlayLayerImage != null + : "fx:id=\"overlayLayerImage\" was not injected: check your FXML file '" + + AppConstants.MANAGE_OVERLAYS_FXML + + "'."; + assert overlayLayerMask != null + : "fx:id=\"overlayLayerMask\" was not injected: check your FXML file '" + + AppConstants.MANAGE_OVERLAYS_FXML + + "'."; + assert addOverlayButton != null + : "fx:id=\"addOverlayButton\" was not injected: check your FXML file '" + + AppConstants.MANAGE_OVERLAYS_FXML + + "'."; + assert deleteOverlayButton != null + : "fx:id=\"deleteOverlayButton\" was not injected: check your FXML file '" + + AppConstants.MANAGE_OVERLAYS_FXML + + "'."; + assert addFolderButton != null + : "fx:id=\"addFolderButton\" was not injected: check your FXML file '" + + AppConstants.MANAGE_OVERLAYS_FXML + + "'."; + assert deleteFolderButton != null + : "fx:id=\"deleteFolderButton\" was not injected: check your FXML file '" + + AppConstants.MANAGE_OVERLAYS_FXML + + "'."; + assert restoreButton != null + : "fx:id=\"restoreButton\" was not injected: check your FXML file '" + + AppConstants.MANAGE_OVERLAYS_FXML + + "'."; + + executorService = + Executors.newSingleThreadScheduledExecutor( + runable -> { + loadOverlaysThread = Executors.defaultThreadFactory().newThread(runable); + loadOverlaysThread.setDaemon(true); + return loadOverlaysThread; + }); + + // Add a listener to the TreeView + overlayTreeView + .getSelectionModel() + .selectedItemProperty() + .addListener((observable, oldValue, newValue) -> loadImages(newValue)); + + displayTreeView(); + } + + public void displayTreeView() { + TreeItem root = new OverlayTreeItem(AppConstants.OVERLAY_DIR); + root.setExpanded(true); + overlayTreeView.setRoot(root); + + overlayTreeView.setCellFactory( + treeView -> + new TreeCell() { + @Override + public void updateItem(Path path, boolean empty) { + super.updateItem(path, empty); + if (empty) { + setText(null); + } else { + setText(path.getFileName().toString()); + } + } + }); + } + + private void loadImages(TreeItem treeItem) { + overlayViewFlowPane.getChildren().clear(); + if (treeItem != null) loadImages(treeItem.getValue().toFile()); + } + + private void loadImages(File dir) { + // Clear Details panel + clearDetails(); + + currentDirectory = dir; + File[] files = dir.listFiles(ImageUtil.SUPPORTED_FILENAME_FILTER); + + Task task = + new Task() { + @Override + public Void call() { + for (File file : files) { + Path filePath = file.toPath(); + + if (loadOverlaysThread.isInterrupted()) { + Platform.runLater(() -> overlayViewFlowPane.getChildren().clear()); + break; + } + + try { + ToggleButton overlayButton = new ToggleButton(); + ImageView imageViewNode = ImageUtil.getOverlayThumb(new ImageView(), filePath); + + overlayButton.getStyleClass().add("overlay-toggle-button"); + overlayButton.setGraphic(imageViewNode); + overlayButton.setUserData(file); + overlayButton.setToggleGroup(overlayToggleGroup); + + overlayButton.addEventHandler( + ActionEvent.ACTION, + event -> { + // No modifier keys used so add toggle group back to all buttons + resetToggleGroup(); + + // Also set button to selected due to resetting toggle groups & no unselecting + // needed, makes for better interface IMO + overlayButton.setSelected(true); + + // Update the Details panel with the last selected overlay + File overlayFile = (File) overlayButton.getUserData(); + updateDetails( + overlayFile, + (ImageView) overlayButton.getGraphic(), + overlayButton.isSelected()); + + // Consume the event, no more logic needed + event.consume(); + }); + + overlayButton.setOnMouseClicked( + new EventHandler() { + @Override + public void handle(MouseEvent event) { + // Allow multiple selections if shortcutKey+left_mouse is pressed + if (event.getButton().equals(MouseButton.PRIMARY) + && event.isShortcutDown()) { + // Update the Details panel with the last selected overlay + File overlayFile = (File) overlayButton.getUserData(); + updateDetails(overlayFile, (ImageView) overlayButton.getGraphic(), true); + + // Remove the toggle group to allow multiple toggle button selection + overlayButton.setToggleGroup(null); + + // Select the button + overlayButton.setSelected(true); + + // Consume the event, no more logic needed + event.consume(); + } + } + }); + + Platform.runLater(() -> overlayViewFlowPane.getChildren().add(overlayButton)); + } catch (IOException e) { + log.error("Loading image: " + filePath.getFileName(), e); + } + } + + return null; + } + }; + + loadOverlaysThread.interrupt(); + executorService.execute(task); + } + + private void updateDetails(File overlayFile, ImageView overlayImage, boolean selected) { + if (selected) { + int w = (int) overlayImage.getImage().getWidth(); + int h = (int) overlayImage.getImage().getHeight(); + + overlayName.setText(FilenameUtils.getBaseName(overlayFile.getName())); + overlayDescription.setText(ImageUtil.getFileType(overlayFile)); + overlayDimensions.setText(w + " x " + h); + overlayLayerImage.setImage(overlayImage.getImage()); + ; + + try { + overlayLayerMask = ImageUtil.getMaskImage(overlayLayerMask, overlayFile.toPath()); + } catch (IOException e) { + log.error("Updating details for: " + overlayFile.getAbsolutePath(), e); + } + } else { + clearDetails(); + } + } + + private void clearDetails() { + overlayName.setText(""); + overlayDescription.setText(""); + overlayDimensions.setText(""); + overlayLayerImage.setImage(null); + overlayLayerMask.setImage(null); + } + + private void resetToggleGroup() { + for (Node overlay : overlayViewFlowPane.getChildren()) { + ToggleButton overlayButton = (ToggleButton) overlay; + if (overlayButton.getToggleGroup() == null) overlayButton.setToggleGroup(overlayToggleGroup); + } + } + + private boolean confirmDelete(LinkedList overlayFiles) { + String confirmationText = I18N.getString("ManageOverlays.dialog.delete.confirmation"); + + if (overlayFiles.isEmpty()) return false; + else if (overlayFiles.size() == 1) { + confirmationText += overlayFiles.get(0).getName() + "?"; + } else { + confirmationText += + I18N.getString("ManageOverlays.dialog.delete.confirmation.these") + + overlayFiles.size() + + I18N.getString("ManageOverlays.dialog.delete.confirmation.overlays"); + } + + Alert alert = new Alert(AlertType.CONFIRMATION); + alert.setHeaderText(I18N.getString("TokenTool.dialog.confirmation.header")); + alert.setTitle(I18N.getString("ManageOverlays.dialog.delete.title")); + alert.setContentText(confirmationText); + + Optional result = alert.showAndWait(); + + if ((result.isPresent()) && (result.get() == ButtonType.OK)) { + return true; + } + + return false; + } + + private boolean confirmDelete(File dir) { + String confirmationText = I18N.getString("ManageOverlays.dialog.delete.dir.confirmation"); + long dirSize = + FileUtils.listFiles(dir, ImageUtil.SUPPORTED_FILE_FILTER, TrueFileFilter.INSTANCE).size(); + + if (dirSize == 0) { + confirmationText += + dir.getName() + I18N.getString("ManageOverlays.dialog.delete.dir.confirmation.directory"); + } else { + confirmationText += + dir.getName() + + I18N.getString("ManageOverlays.dialog.delete.dir.directory_containing") + + dirSize + + I18N.getString("ManageOverlays.dialog.delete.dir.overlays"); + } + + Alert alert = new Alert(AlertType.CONFIRMATION); + alert.setHeaderText(I18N.getString("TokenTool.dialog.confirmation.header")); + alert.setTitle(I18N.getString("ManageOverlays.dialog.delete.dir.title")); + alert.setContentText(confirmationText); + + Optional result = alert.showAndWait(); + + if ((result.isPresent()) && (result.get() == ButtonType.OK)) { + return true; + } + + return false; + } + + @FXML + void deleteOverlayButton_onAction(ActionEvent event) { + LinkedList overlayFiles = new LinkedList(); + + for (Node overlay : overlayViewFlowPane.getChildren()) { + ToggleButton overlayButton = (ToggleButton) overlay; + if (overlayButton.isSelected()) overlayFiles.add((File) overlayButton.getUserData()); + } + + if (confirmDelete(overlayFiles)) { + for (File file : overlayFiles) { + log.info("Deleting: " + file.getName()); + file.delete(); + } + + loadImages(overlayTreeView.getSelectionModel().getSelectedItem()); + } + } + + @FXML + void deleteFolderButton_onAction(ActionEvent event) { + if (currentDirectory.equals(AppConstants.OVERLAY_DIR)) return; + + if (confirmDelete(currentDirectory)) { + try { + FileUtils.forceDelete(currentDirectory); + } catch (IOException e) { + log.info("Deleting: " + currentDirectory.getAbsolutePath()); + } + + displayTreeView(); + } + } + + @FXML + void addOverlayButton_onAction(ActionEvent event) { + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle(I18N.getString("ManageOverlays.filechooser.overlay.title")); + fileChooser.getExtensionFilters().addAll(ImageUtil.GET_EXTENSION_FILTERS()); + + if (lastSelectedDirectory != null) fileChooser.setInitialDirectory(lastSelectedDirectory); + + List selectedFiles = + fileChooser.showOpenMultipleDialog((Stage) addOverlayButton.getScene().getWindow()); + + if (selectedFiles != null) { + for (File selectedFile : selectedFiles) { + FileSaveUtil.copyFile(selectedFile, currentDirectory); + } + + lastSelectedDirectory = selectedFiles.get(0).getParentFile(); + loadImages(overlayTreeView.getSelectionModel().getSelectedItem()); + } + } + + @FXML + void addFolderButton_onAction(ActionEvent event) { + TextInputDialog dialog = new TextInputDialog(); + dialog.setHeaderText(I18N.getString("TokenTool.dialog.confirmation.header")); + dialog.setTitle(I18N.getString("ManageOverlays.filechooser.folder.title")); + dialog.setContentText(I18N.getString("ManageOverlays.filechooser.folder.content_text")); + + Optional result = dialog.showAndWait(); + result.ifPresent( + name -> { + if (FileSaveUtil.makeDir(name, currentDirectory)) { + displayTreeView(); + } + ; + }); + } + + @FXML + void restoreButton_onAction(ActionEvent event) { + Alert alert = new Alert(AlertType.CONFIRMATION); + alert.setHeaderText(I18N.getString("TokenTool.dialog.confirmation.header")); + alert.setTitle(I18N.getString("ManageOverlays.dialog.restore.overlays.title")); + alert.setContentText(I18N.getString("ManageOverlays.dialog.restore.overlays.content_text")); + + Optional result = alert.showAndWait(); + + if ((result.isPresent()) && (result.get() == ButtonType.OK)) { + log.info("Restoring default images..."); + try { + AppSetup.installDefaultOverlays(); + } catch (IOException e) { + log.error("Error restoring default overlays!", e); + } + + displayTreeView(); + } + } + + @FXML + void overlayViewFlowPane_DragDone(DragEvent event) { + loadImages(overlayTreeView.getSelectionModel().getSelectedItem()); + } + + @FXML + void overlayViewFlowPane_DragDropped(DragEvent event) { + Dragboard db = event.getDragboard(); + if (db.hasImage()) { + try { + // Prompt for name & return file name + File newOverlayFile = new File(currentDirectory.getCanonicalPath() + "/somefilename.png"); + ImageIO.write(SwingFXUtils.fromFXImage(db.getImage(), null), "png", newOverlayFile); + } catch (IOException e) { + log.error("Error writing new overlay image.", e); + } + + loadImages(overlayTreeView.getSelectionModel().getSelectedItem()); + event.setDropCompleted(true); + } else if (db.hasFiles()) { + db.getFiles() + .forEach( + file -> { + FileSaveUtil.copyFile(file, currentDirectory); + }); + loadImages(overlayTreeView.getSelectionModel().getSelectedItem()); + event.setDropCompleted(true); + } else if (db.hasUrl()) { + FileSaveUtil.copyFile(new File(db.getUrl()), currentDirectory); + loadImages(overlayTreeView.getSelectionModel().getSelectedItem()); + event.setDropCompleted(true); + } + } + + @FXML + void overlayViewFlowPane_DragOver(DragEvent event) { + if (event.getDragboard().hasImage() + || event.getDragboard().hasFiles() + || event.getDragboard().hasUrl()) { + event.acceptTransferModes(TransferMode.COPY); + } else { + event.acceptTransferModes(TransferMode.ANY); + } + } } diff --git a/src/main/java/net/rptools/tokentool/controller/PdfViewer_Controller.java b/src/main/java/net/rptools/tokentool/controller/PdfViewer_Controller.java new file mode 100644 index 0000000..e1740e7 --- /dev/null +++ b/src/main/java/net/rptools/tokentool/controller/PdfViewer_Controller.java @@ -0,0 +1,347 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * TokenTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.tokentool.controller; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.ResourceBundle; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; +import javafx.animation.FadeTransition; +import javafx.animation.PauseTransition; +import javafx.application.Platform; +import javafx.beans.binding.Bindings; +import javafx.concurrent.Task; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.scene.Node; +import javafx.scene.control.Pagination; +import javafx.scene.control.ProgressBar; +import javafx.scene.control.ProgressIndicator; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.TextField; +import javafx.scene.control.ToggleButton; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.input.MouseEvent; +import javafx.scene.input.ScrollEvent; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.Pane; +import javafx.scene.layout.TilePane; +import javafx.stage.Stage; +import javafx.util.Callback; +import javafx.util.Duration; +import net.rptools.tokentool.AppConstants; +import net.rptools.tokentool.model.PdfModel; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class PdfViewer_Controller implements Initializable { + private static final Logger log = LogManager.getLogger(PdfViewer_Controller.class); + + @FXML private AnchorPane pdfAnchorPane; + @FXML private Pagination pdfViewPagination; + @FXML private TextField pageNumberTextField; + @FXML private ScrollPane imageScrollPane; + @FXML private TilePane imageTilePane; + @FXML private ProgressIndicator pdfProgressIndicator; + @FXML private ProgressIndicator extractProgressIndicator; + @FXML private Pane viewPortPane; + @FXML private ScrollPane imageTileScrollpane; + + private PdfModel pdfModel; + private ImageView pdfImageView = new ImageView(); + + private static ExecutorService renderPdfPageService; + private static ExecutorService extractImagesService; + private AtomicInteger workerThreads = new AtomicInteger(0); + private AtomicInteger extractThreads = new AtomicInteger(0); + + @Override + public void initialize(URL url, ResourceBundle rb) { + assert pdfAnchorPane != null + : "fx:id=\"pdfAnchorPane\" was not injected: check your FXML file '" + + AppConstants.PDF_VIEW_FXML + + "'."; + assert pdfViewPagination != null + : "fx:id=\"pdfViewPagination\" was not injected: check your FXML file '" + + AppConstants.PDF_VIEW_FXML + + "'."; + assert pageNumberTextField != null + : "fx:id=\"pageNumberTextField\" was not injected: check your FXML file '" + + AppConstants.PDF_VIEW_FXML + + "'."; + assert imageScrollPane != null + : "fx:id=\"imageScrollPane\" was not injected: check your FXML file '" + + AppConstants.PDF_VIEW_FXML + + "'."; + assert imageTilePane != null + : "fx:id=\"imageFlowPane\" was not injected: check your FXML file '" + + AppConstants.PDF_VIEW_FXML + + "'."; + assert pdfProgressIndicator != null + : "fx:id=\"pdfProgressIndicator\" was not injected: check your FXML file '" + + AppConstants.PDF_VIEW_FXML + + "'."; + assert extractProgressIndicator != null + : "fx:id=\"extractProgressIndicator\" was not injected: check your FXML file '" + + AppConstants.PDF_VIEW_FXML + + "'."; + assert viewPortPane != null + : "fx:id=\"viewPortPane\" was not injected: check your FXML file '" + + AppConstants.PDF_VIEW_FXML + + "'."; + assert imageTileScrollpane != null + : "fx:id=\"imageTileScrollpane\" was not injected: check your FXML file '" + + AppConstants.PDF_VIEW_FXML + + "'."; + + pdfProgressIndicator.setProgress(ProgressBar.INDETERMINATE_PROGRESS); + extractProgressIndicator.setProgress(ProgressBar.INDETERMINATE_PROGRESS); + pdfViewPagination.requestFocus(); + + renderPdfPageService = Executors.newWorkStealingPool(); + extractImagesService = Executors.newSingleThreadExecutor(); + } + + public void loadPDF(File pdfFile, TokenTool_Controller tokenTool_Controller, Stage stage) { + try { + pdfModel = new PdfModel(pdfFile, tokenTool_Controller); + } catch (IOException e) { + log.error("Error loading PDF " + pdfFile.getAbsolutePath(), e); + } + + pdfViewPagination.setPageCount(pdfModel.numPages()); + + // Set paginations's image to resize with the window. Note: Had to use stage because binding to + // other panes caused "weirdness" + // ...adjusting the height to account for the pagination buttons (didn't care to see them over + // the PDF image) + // ...adjusting the width to account for tiles + scrollbar + pdfImageView.setPreserveRatio(true); + pdfImageView.setTranslateY(-7); + pdfImageView.fitHeightProperty().bind(Bindings.subtract(stage.heightProperty(), 120)); + pdfImageView + .fitWidthProperty() + .bind(Bindings.subtract(stage.widthProperty(), imageTileScrollpane.getWidth() + 30)); + + pdfViewPagination.setPageFactory( + new Callback() { + public Node call(final Integer pageIndex) { + workerThreads.incrementAndGet(); + imageTilePane.getChildren().clear(); + + // First, blank the page out + pdfImageView.setImage(null); + + // Execute the render off the UI thread... + RenderPdfPageTask renderPdfPageTask = new RenderPdfPageTask(pageIndex); + renderPdfPageService.execute(renderPdfPageTask); + + return pdfImageView; + } + }); + } + + private class RenderPdfPageTask extends Task { + Integer pageIndex; + + RenderPdfPageTask(Integer pageIndex) { + this.pageIndex = pageIndex; + } + + @Override + protected Void call() throws Exception { + // For debugging and tracking the thread... + Thread.currentThread().setName("RenderPdfPageTask-Page-" + (pageIndex + 1)); + + long startTime = System.currentTimeMillis(); + + pdfProgressIndicator.setProgress(ProgressBar.INDETERMINATE_PROGRESS); + + // Don't show the progressIndicator for brief transitions... + PauseTransition pause = new PauseTransition(Duration.millis(400)); + pause.setOnFinished( + event -> { + pdfProgressIndicator.setVisible(true); + pdfProgressIndicator.setOpacity(1); + }); + pause.play(); + + // Do the actual work + Image image = pdfModel.getImage(pageIndex); + + pdfImageView.setImage(image); + + // Skip the animation for quick page turns + long loadTime = System.currentTimeMillis() - startTime; + if (loadTime < 500) { + pause.stop(); + pdfProgressIndicator.setVisible(false); + } else { + pdfProgressIndicator.setVisible(true); + + FadeTransition fadeTransition = + new FadeTransition(Duration.millis(500), pdfProgressIndicator); + fadeTransition.setFromValue(1.0); + fadeTransition.setToValue(0.0); + fadeTransition.play(); + } + + return null; + } + + @Override + protected void done() { + // Since we are rendering in multiple threads, lets make sure the current page is shown when + // we are all done! + if (workerThreads.decrementAndGet() == 0) { + pdfImageView.setImage(pdfModel.getImage(pdfViewPagination.getCurrentPageIndex())); + + extractThreads.incrementAndGet(); + pdfModel.interrupt(); + extractImagesService.execute(new ExtractPdfImages(pageIndex)); + } + } + } + + private class ExtractPdfImages extends Task { + Integer pageIndex; + ArrayList imageButtons = new ArrayList(); + + ExtractPdfImages(Integer pageIndex) { + this.pageIndex = pageIndex; + } + + @Override + protected Void call() throws Exception { + // For debugging and tracking the thread... + Thread.currentThread().setName("ExtractPdfPageTask-Page-" + (pageIndex + 1)); + + long startTime = System.currentTimeMillis(); + + extractProgressIndicator.setProgress(ProgressBar.INDETERMINATE_PROGRESS); + + // Don't show the progressIndicator for brief transitions... + PauseTransition pause = new PauseTransition(Duration.millis(400)); + pause.setOnFinished( + event -> { + extractProgressIndicator.setVisible(true); + extractProgressIndicator.setOpacity(1); + }); + pause.play(); + + // Do the actual work... + extractProgressIndicator.setProgress(ProgressBar.INDETERMINATE_PROGRESS); + log.info("Extracting..."); + imageButtons = pdfModel.extractImages(pdfViewPagination.getCurrentPageIndex()); + + // Skip the animation for quick page turns + long loadTime = System.currentTimeMillis() - startTime; + if (loadTime < 500) { + pause.stop(); + extractProgressIndicator.setVisible(false); + } else { + extractProgressIndicator.setVisible(true); + + FadeTransition fadeTransition = + new FadeTransition(Duration.millis(500), extractProgressIndicator); + fadeTransition.setFromValue(1.0); + fadeTransition.setToValue(0.0); + fadeTransition.play(); + } + + return null; + } + + @Override + protected void done() { + Platform.runLater( + () -> { + if (extractThreads.decrementAndGet() == 0) { + log.info("Adding " + imageButtons.size()); + // log.info("imageTilePane.getChildren() " + imageTilePane.getChildren().size()); + try { + imageTilePane.getChildren().clear(); + imageTilePane.getChildren().addAll(imageButtons); + } catch (IllegalArgumentException e) { + log.error("Error adding tiled image buttons.", e); + } + // log.info("Done..."); + } + }); + } + } + + class getPageImageView extends Task { + Integer pageIndex; + + getPageImageView(Integer pageIndex) { + this.pageIndex = pageIndex; + } + + @Override + protected Node call() throws Exception { + + return null; + } + } + + // private void extractImages() { + // imageTilePane.getChildren().clear(); + // pdfModel.extractImages(imageTilePane, pdfViewPagination.getCurrentPageIndex()); + // } + + public void close() { + pdfModel.close(); + } + + @FXML + void pdfViewPagination_OnScroll(ScrollEvent event) { + int delta = 1; + if (event.getDeltaX() > 1 || event.getDeltaY() > 1) delta = -1; + + pdfViewPagination.setCurrentPageIndex(pdfViewPagination.getCurrentPageIndex() + delta); + } + + @FXML + void pdfViewPagination_onMouseClick(MouseEvent event) { + pdfViewPagination.setCurrentPageIndex(pdfViewPagination.getCurrentPageIndex()); + } + + @FXML + void pageNumberTextField_onMouseClicked(MouseEvent event) { + pageNumberTextField.setOpacity(1); + pageNumberTextField.selectAll(); + } + + @FXML + void pageNumberTextField_onAction(ActionEvent event) { + int pageNumber = Integer.parseInt(pageNumberTextField.getText()); + + if (pageNumber > pdfViewPagination.getPageCount()) + pageNumber = pdfViewPagination.getPageCount(); + + if (pageNumber > 0) pdfViewPagination.setCurrentPageIndex(pageNumber - 1); + + pageNumberTextField.setText(pdfViewPagination.getCurrentPageIndex() + 1 + ""); + pdfViewPagination.requestFocus(); + pageNumberTextField.setOpacity(0); + } +} diff --git a/src/main/java/net/rptools/tokentool/controller/RegionSelector_Controller.java b/src/main/java/net/rptools/tokentool/controller/RegionSelector_Controller.java index c91eab4..55fd287 100644 --- a/src/main/java/net/rptools/tokentool/controller/RegionSelector_Controller.java +++ b/src/main/java/net/rptools/tokentool/controller/RegionSelector_Controller.java @@ -1,10 +1,16 @@ /* - * This software Copyright by the RPTools.net development team, and licensed under the Affero GPL Version 3 or, at your option, any later version. + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. * - * TokenTool Source Code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * TokenTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * - * You should have received a copy of the GNU Affero General Public License * along with this source Code. If not, please visit and specifically the Affero license text - * at . + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . */ package net.rptools.tokentool.controller; @@ -12,10 +18,7 @@ import java.awt.Rectangle; import java.awt.Robot; import java.awt.image.BufferedImage; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - +import javafx.application.Platform; import javafx.embed.swing.SwingFXUtils; import javafx.event.ActionEvent; import javafx.fxml.FXML; @@ -23,43 +26,65 @@ import javafx.scene.control.Button; import javafx.scene.layout.StackPane; import javafx.stage.Stage; +import net.rptools.tokentool.AppConstants; +import net.rptools.tokentool.AppPreferences; +import net.rptools.tokentool.model.Window_Preferences; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; public class RegionSelector_Controller { - private static final Logger log = LogManager.getLogger(RegionSelector_Controller.class); - private TokenTool_Controller tokenTool_Controller; + private static final Logger log = LogManager.getLogger(RegionSelector_Controller.class); + private TokenTool_Controller tokenTool_Controller; + + @FXML private StackPane selectionStackPane; + @FXML private Button captureButton; - @FXML private StackPane selectionStackPane; - @FXML private Button captureButton; + @FXML + void initialize() { + assert selectionStackPane != null + : "fx:id=\"selectionStackPane\" was not injected: check your FXML file '" + + AppConstants.REGION_SELECTOR_FXML + + "'."; + assert captureButton != null + : "fx:id=\"captureButton\" was not injected: check your FXML file '" + + AppConstants.REGION_SELECTOR_FXML + + "'."; + } - @FXML - void initialize() { - assert selectionStackPane != null : "fx:id=\"selectionStackPane\" was not injected: check your FXML file 'RegionSelector.fxml'."; - assert captureButton != null : "fx:id=\"captureButton\" was not injected: check your FXML file 'RegionSelector.fxml'."; - } + @FXML + void captureButton_onAction(ActionEvent event) { + Stage stage = (Stage) captureButton.getScene().getWindow(); + Scene scene = stage.getScene(); + int x = (int) stage.getX(); + int y = (int) stage.getY(); + int sceneWidth = (int) scene.getWidth(); + int sceneHeight = (int) scene.getHeight(); - @FXML - void captureButton_onAction(ActionEvent event) { - Stage stage = (Stage) captureButton.getScene().getWindow(); - Scene scene = stage.getScene(); - int x = (int) stage.getX(); - int y = (int) stage.getY(); - int sceneWidth = (int) scene.getWidth(); - int sceneHeight = (int) scene.getHeight(); + stage.hide(); + log.debug("Rect: " + new Rectangle(x, y, sceneWidth, sceneHeight)); - try { - stage.hide(); - // Robot robot = new Robot(GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice()); - log.info("Rect: " + new Rectangle(x, y, sceneWidth, sceneHeight)); - BufferedImage bi = new Robot().createScreenCapture(new Rectangle(x, y, sceneWidth, sceneHeight)); - tokenTool_Controller.updatePortrait(SwingFXUtils.toFXImage(bi, null)); - } catch (AWTException e) { - log.error(e); - } + // Test to see if this works better on Linux. Linux would still capture this stage in the + // image... + Platform.runLater( + () -> { + try { + // Robot robot = new + // Robot(GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice()); + BufferedImage bi = + new Robot().createScreenCapture(new Rectangle(x, y, sceneWidth, sceneHeight)); + tokenTool_Controller.updateImage(SwingFXUtils.toFXImage(bi, null)); - stage.close(); - } + AppPreferences.setPreference( + AppPreferences.WINDOW_REGION_SELECTOR_PREFERENCES, + new Window_Preferences(stage).toJson()); + stage.close(); + } catch (AWTException e) { + log.error("Error capturing screen shot!", e); + } + }); + } - public void setController(TokenTool_Controller controller) { - tokenTool_Controller = controller; - } + public void setController(TokenTool_Controller controller) { + tokenTool_Controller = controller; + } } diff --git a/src/main/java/net/rptools/tokentool/controller/SplashScreen_Controller.java b/src/main/java/net/rptools/tokentool/controller/SplashScreen_Controller.java index 600f7a8..60329b3 100644 --- a/src/main/java/net/rptools/tokentool/controller/SplashScreen_Controller.java +++ b/src/main/java/net/rptools/tokentool/controller/SplashScreen_Controller.java @@ -1,10 +1,16 @@ /* - * This software Copyright by the RPTools.net development team, and licensed under the Affero GPL Version 3 or, at your option, any later version. + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. * - * TokenTool Source Code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * TokenTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * - * You should have received a copy of the GNU Affero General Public License * along with this source Code. If not, please visit and specifically the Affero license text - * at . + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . */ package net.rptools.tokentool.controller; @@ -12,44 +18,57 @@ import javafx.scene.control.Label; import javafx.scene.control.ProgressBar; import javafx.scene.layout.StackPane; +import net.rptools.tokentool.AppConstants; public class SplashScreen_Controller { - private static String versionPrefix; + private static String versionPrefix; - @FXML private StackPane splashLayout; + @FXML private StackPane splashLayout; - @FXML private ProgressBar loadProgress; + @FXML private ProgressBar loadProgress; - @FXML private Label progressLabel; - @FXML private Label versionLabel; + @FXML private Label progressLabel; + @FXML private Label versionLabel; - @FXML // This method is called by the FXMLLoader when initialization is complete - void initialize() { - assert splashLayout != null : "fx:id=\"splashLayout\" was not injected: check your FXML file 'SplashScreenLoader.fxml'."; - assert loadProgress != null : "fx:id=\"loadProgress\" was not injected: check your FXML file 'SplashScreenLoader.fxml'."; - assert progressLabel != null : "fx:id=\"progressText\" was not injected: check your FXML file 'SplashScreenLoader.fxml'."; - assert versionLabel != null : "fx:id=\"versionLabel\" was not injected: check your FXML file 'SplashScreenLoader.fxml'."; + @FXML // This method is called by the FXMLLoader when initialization is complete + void initialize() { + assert splashLayout != null + : "fx:id=\"splashLayout\" was not injected: check your FXML file '" + + AppConstants.SPLASH_SCREEN_FXML + + "'."; + assert loadProgress != null + : "fx:id=\"loadProgress\" was not injected: check your FXML file '" + + AppConstants.SPLASH_SCREEN_FXML + + "'."; + assert progressLabel != null + : "fx:id=\"progressText\" was not injected: check your FXML file '" + + AppConstants.SPLASH_SCREEN_FXML + + "'."; + assert versionLabel != null + : "fx:id=\"versionLabel\" was not injected: check your FXML file '" + + AppConstants.SPLASH_SCREEN_FXML + + "'."; - versionPrefix = getVersionLabel() + " "; - } + versionPrefix = getVersionLabel() + " "; + } - public void setLoadProgress(Double progress) { - this.loadProgress.setProgress(progress); - } + public void setLoadProgress(Double progress) { + this.loadProgress.setProgress(progress); + } - public String getProgressLabel() { - return progressLabel.getText(); - } + public String getProgressLabel() { + return progressLabel.getText(); + } - public void setProgressLabel(String text) { - this.progressLabel.setText(text); - } + public void setProgressLabel(String text) { + this.progressLabel.setText(text); + } - public String getVersionLabel() { - return versionLabel.getText(); - } + public String getVersionLabel() { + return versionLabel.getText(); + } - public void setVersionLabel(String text) { - this.versionLabel.setText(versionPrefix + text); - } + public void setVersionLabel(String text) { + this.versionLabel.setText(versionPrefix + text); + } } diff --git a/src/main/java/net/rptools/tokentool/controller/TokenTool_Controller.java b/src/main/java/net/rptools/tokentool/controller/TokenTool_Controller.java index 40f0252..5bccf24 100644 --- a/src/main/java/net/rptools/tokentool/controller/TokenTool_Controller.java +++ b/src/main/java/net/rptools/tokentool/controller/TokenTool_Controller.java @@ -1,14 +1,24 @@ /* - * This software Copyright by the RPTools.net development team, and licensed under the Affero GPL Version 3 or, at your option, any later version. + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. * - * TokenTool Source Code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * TokenTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * - * You should have received a copy of the GNU Affero General Public License * along with this source Code. If not, please visit and specifically the Affero license text - * at . + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . */ package net.rptools.tokentool.controller; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import java.awt.Graphics2D; import java.awt.Point; +import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.nio.file.Files; @@ -21,17 +31,12 @@ import java.util.Map; import java.util.Map.Entry; import java.util.NavigableSet; +import java.util.Optional; import java.util.TreeSet; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.UnaryOperator; -import javax.imageio.ImageIO; - -import org.apache.commons.io.FilenameUtils; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - import javafx.animation.FadeTransition; import javafx.application.Platform; import javafx.beans.binding.Bindings; @@ -46,11 +51,16 @@ import javafx.geometry.Insets; import javafx.scene.Cursor; import javafx.scene.Group; +import javafx.scene.control.Alert; +import javafx.scene.control.Alert.AlertType; +import javafx.scene.control.ButtonType; import javafx.scene.control.CheckBox; import javafx.scene.control.ColorPicker; import javafx.scene.control.Label; +import javafx.scene.control.MenuButton; import javafx.scene.control.MenuItem; import javafx.scene.control.ProgressBar; +import javafx.scene.control.RadioMenuItem; import javafx.scene.control.ScrollPane; import javafx.scene.control.Slider; import javafx.scene.control.Spinner; @@ -70,6 +80,7 @@ import javafx.scene.input.ClipboardContent; import javafx.scene.input.DragEvent; import javafx.scene.input.Dragboard; +import javafx.scene.input.KeyEvent; import javafx.scene.input.MouseDragEvent; import javafx.scene.input.MouseEvent; import javafx.scene.input.RotateEvent; @@ -80,1077 +91,1753 @@ import javafx.scene.layout.BackgroundFill; import javafx.scene.layout.BorderPane; import javafx.scene.layout.CornerRadii; +import javafx.scene.layout.Pane; import javafx.scene.layout.StackPane; import javafx.scene.paint.Color; import javafx.stage.FileChooser; +import javafx.stage.Stage; import javafx.util.Duration; +import javafx.util.StringConverter; +import javax.imageio.ImageIO; import net.rptools.tokentool.AppConstants; import net.rptools.tokentool.AppPreferences; import net.rptools.tokentool.client.Credits; import net.rptools.tokentool.client.ManageOverlays; +import net.rptools.tokentool.client.PdfViewer; import net.rptools.tokentool.client.RegionSelector; +import net.rptools.tokentool.client.TokenTool; +import net.rptools.tokentool.model.ImageView_Preferences; +import net.rptools.tokentool.model.Window_Preferences; import net.rptools.tokentool.util.FileSaveUtil; import net.rptools.tokentool.util.I18N; import net.rptools.tokentool.util.ImageUtil; +import org.apache.commons.io.FilenameUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; public class TokenTool_Controller { - @FXML private MenuItem fileManageOverlaysMenu; - @FXML private MenuItem fileSaveAsMenu; - @FXML private MenuItem fileExitMenu; - - @FXML private MenuItem editCaptureScreenMenu; - @FXML private MenuItem editCopyImageMenu; - @FXML private MenuItem editPasteImageMenu; - - @FXML private MenuItem helpAboutMenu; - - @FXML private TitledPane saveOptionsPane; - @FXML private TitledPane overlayOptionsPane; - @FXML private TitledPane backgroundOptionsPane; - @FXML private TitledPane zoomOptionsPane; - - @FXML private StackPane compositeTokenPane; - @FXML private BorderPane tokenPreviewPane; - @FXML private ScrollPane portraitScrollPane; - - @FXML private Group compositeGroup; - - @FXML private TreeView overlayTreeView; - - @FXML private ImageView portraitImageView; // The bottom "Portrait" layer - @FXML private ImageView maskImageView; // The mask layer used to crop the Portrait layer - @FXML private ImageView overlayImageView; // The overlay layer to apply on top of everything - @FXML private ImageView tokenImageView; - - @FXML private CheckBox useFileNumberingCheckbox; - @FXML private CheckBox overlayUseAsBaseCheckbox; - @FXML private CheckBox clipPortraitCheckbox; - - @FXML private TextField fileNameTextField; - @FXML private Label fileNameSuffixLabel; - @FXML private TextField fileNameSuffixTextField; - @FXML private Label overlayNameLabel; - @FXML private ColorPicker backgroundColorPicker; - @FXML private ToggleButton overlayAspectToggleButton; - - @FXML private Slider portraitTransparencySlider; - @FXML private Slider portraitBlurSlider; - @FXML private Slider portraitGlowSlider; - - @FXML private Slider overlayTransparencySlider; - - @FXML private Spinner overlayWidthSpinner; - @FXML private Spinner overlayHeightSpinner; - - @FXML private ProgressBar overlayTreeProgressBar; - @FXML private Label progressBarLabel; - - private static final Logger log = LogManager.getLogger(TokenTool_Controller.class); - - private static ExecutorService executorService; - private static Thread loadOverlaysThread = new Thread(); - private static AtomicInteger loadCount = new AtomicInteger(0); - - private static int overlayCount; - - private static TreeItem treeItems; - private static TreeItem lastSelectedItem; - private static TreeItem recentFolder = new TreeItem<>(new File(AppConstants.OVERLAY_DIR, "Recent").toPath(), null); - - private static Map> recentOverlayTreeItems = new LinkedHashMap>() { - private static final long serialVersionUID = 2579964060760662199L; - - @Override - protected boolean removeEldestEntry(Map.Entry> eldest) { - return size() > AppConstants.MAX_RECENT_SIZE; - } - }; - - private Point dragStart = new Point(); - private Point portraitImageStart = new Point(); - private FileSaveUtil fileSaveUtil = new FileSaveUtil(); - - @SuppressWarnings("unused") private RegionSelector regionSelector; - - // A custom set of Width/Height sizes to use for Overlays - private NavigableSet overlaySpinnerSteps = new TreeSet(Arrays.asList(50d, 100d, 128d, 150d, 200d, - 256d, 300d, 400d, 500d, 512d, 600d, 700d, 750d, 800d, 900d, 1000d)); - - @FXML - void initialize() { - // Note: A Pane is added to the compositeTokenPane so the ScrollPane doesn't consume the mouse events - assert fileManageOverlaysMenu != null : "fx:id=\"fileManageOverlaysMenu\" was not injected: check your FXML file 'TokenTool.fxml'."; - assert fileSaveAsMenu != null : "fx:id=\"fileSaveAsMenu\" was not injected: check your FXML file 'TokenTool.fxml'."; - assert fileExitMenu != null : "fx:id=\"fileExitMenu\" was not injected: check your FXML file 'TokenTool.fxml'."; - - assert editCaptureScreenMenu != null : "fx:id=\"editCaptureScreenMenu\" was not injected: check your FXML file 'TokenTool.fxml'."; - assert editCopyImageMenu != null : "fx:id=\"editCopyImageMenu\" was not injected: check your FXML file 'TokenTool.fxml'."; - assert editPasteImageMenu != null : "fx:id=\"editPasteImageMenu\" was not injected: check your FXML file 'TokenTool.fxml'."; - - assert helpAboutMenu != null : "fx:id=\"helpAboutMenu\" was not injected: check your FXML file 'TokenTool.fxml'."; - - assert saveOptionsPane != null : "fx:id=\"saveOptionsPane\" was not injected: check your FXML file 'TokenTool.fxml'."; - assert overlayOptionsPane != null : "fx:id=\"overlayOptionsPane\" was not injected: check your FXML file 'TokenTool.fxml'."; - assert backgroundOptionsPane != null : "fx:id=\"backgroundOptionsPane\" was not injected: check your FXML file 'TokenTool.fxml'."; - assert zoomOptionsPane != null : "fx:id=\"zoomOptionsPane\" was not injected: check your FXML file 'TokenTool.fxml'."; - - assert compositeTokenPane != null : "fx:id=\"compositeTokenPane\" was not injected: check your FXML file 'TokenTool.fxml'."; - assert tokenPreviewPane != null : "fx:id=\"tokenPreviewPane\" was not injected: check your FXML file 'TokenTool.fxml'."; - assert portraitScrollPane != null : "fx:id=\"portraitScrollPane\" was not injected: check your FXML file 'TokenTool.fxml'."; - - assert compositeGroup != null : "fx:id=\"compositeGroup\" was not injected: check your FXML file 'TokenTool.fxml'."; - - assert overlayTreeView != null : "fx:id=\"overlayTreeview\" was not injected: check your FXML file 'TokenTool.fxml'."; - - assert portraitImageView != null : "fx:id=\"portraitImageView\" was not injected: check your FXML file 'TokenTool.fxml'."; - assert maskImageView != null : "fx:id=\"maskImageView\" was not injected: check your FXML file 'TokenTool.fxml'."; - assert overlayImageView != null : "fx:id=\"overlayImageView\" was not injected: check your FXML file 'TokenTool.fxml'."; - assert tokenImageView != null : "fx:id=\"tokenImageView\" was not injected: check your FXML file 'TokenTool.fxml'."; - - assert useFileNumberingCheckbox != null : "fx:id=\"useFileNumberingCheckbox\" was not injected: check your FXML file 'TokenTool.fxml'."; - assert overlayUseAsBaseCheckbox != null : "fx:id=\"overlayUseAsBaseCheckbox\" was not injected: check your FXML file 'TokenTool.fxml'."; - assert clipPortraitCheckbox != null : "fx:id=\"clipPortraitCheckbox\" was not injected: check your FXML file 'TokenTool.fxml'."; - - assert fileNameTextField != null : "fx:id=\"fileNameTextField\" was not injected: check your FXML file 'TokenTool.fxml'."; - assert fileNameSuffixLabel != null : "fx:id=\"fileNameSuffixLabel\" was not injected: check your FXML file 'TokenTool.fxml'."; - assert fileNameSuffixTextField != null : "fx:id=\"fileNameSuffixTextField\" was not injected: check your FXML file 'TokenTool.fxml'."; - assert overlayNameLabel != null : "fx:id=\"overlayNameLabel\" was not injected: check your FXML file 'TokenTool.fxml'."; - assert backgroundColorPicker != null : "fx:id=\"backgroundColorPicker\" was not injected: check your FXML file 'TokenTool.fxml'."; - assert overlayAspectToggleButton != null : "fx:id=\"overlayAspectToggleButton\" was not injected: check your FXML file 'TokenTool.fxml'."; - - assert portraitTransparencySlider != null : "fx:id=\"portraitTransparencySlider\" was not injected: check your FXML file 'TokenTool.fxml'."; - assert portraitBlurSlider != null : "fx:id=\"portraitBlurSlider\" was not injected: check your FXML file 'TokenTool.fxml'."; - assert portraitGlowSlider != null : "fx:id=\"portraitGlowSlider\" was not injected: check your FXML file 'TokenTool.fxml'."; - - assert overlayTransparencySlider != null : "fx:id=\"overlayTransparencySlider\" was not injected: check your FXML file 'TokenTool.fxml'."; - - assert overlayWidthSpinner != null : "fx:id=\"overlayWidthSpinner\" was not injected: check your FXML file 'TokenTool.fxml'."; - assert overlayHeightSpinner != null : "fx:id=\"overlayHeightSpinner\" was not injected: check your FXML file 'TokenTool.fxml'."; - - assert overlayTreeProgressBar != null : "fx:id=\"overlayTreeProgressIndicator\" was not injected: check your FXML file 'ManageOverlays.fxml'."; - - executorService = Executors.newCachedThreadPool(runable -> { - loadOverlaysThread = Executors.defaultThreadFactory().newThread(runable); - loadOverlaysThread.setDaemon(true); - return loadOverlaysThread; - }); - - overlayTreeView.setShowRoot(false); - overlayTreeView.getSelectionModel().selectedItemProperty() - .addListener((observable, oldValue, newValue) -> updateCompositImageView((TreeItem) newValue)); - - addPseudoClassToLeafs(overlayTreeView); - - // Bind color picker to compositeTokenPane background fill - backgroundColorPicker.setValue(Color.TRANSPARENT); - ObjectProperty background = compositeTokenPane.backgroundProperty(); - background.bind(Bindings.createObjectBinding(() -> { - BackgroundFill fill = new BackgroundFill(backgroundColorPicker.getValue(), CornerRadii.EMPTY, Insets.EMPTY); - return new Background(fill); - }, backgroundColorPicker.valueProperty())); - - // Bind transparency slider to portraitImageView opacity - portraitTransparencySlider.valueProperty().addListener(new ChangeListener() { - public void changed(ObservableValue ov, - Number old_val, Number new_val) { - portraitImageView.setOpacity(new_val.doubleValue()); - updateTokenPreviewImageView(); - } - }); - - // // Restrict text field to valid filename characters - // Pattern validDoubleText = Pattern.compile("[^a-zA-Z0-9\\\\._ \\\\/`~!@#$%\\\\^&\\\\(\\\\)\\\\-\\\\=\\\\+\\\\[\\\\]\\\\{\\\\}',\\\\\\\\:]"); - // Pattern validText = Pattern.compile("[^a-zA-Z0-9 ]"); - // TextFormatter<> textFormatter = new TextFormatter<>( - // change -> { - // String newText = change.getControlNewText(); - // if (validText.matcher(newText).matches()) { - // return change; - // } else - // return null; - // }); - - // UnaryOperator filter = new UnaryOperator() { - // @Override - // public TextFormatter.Change apply(TextFormatter.Change t) { - // String validText = "[^a-zA-Z0-9]"; - // - // if (t.isReplaced()) - // if (t.getText().matches(validText)) - // t.setText(t.getControlText().substring(t.getRangeStart(), t.getRangeEnd())); - // - // if (t.isAdded()) { - // if (t.getText().matches(validText)) { - // return null; - // } - // } - // - // return t; - // } - // }; - - UnaryOperator filter = change -> { - String text = change.getText(); - - if (text.matches(AppConstants.VALID_FILE_NAME_PATTERN)) { - return change; - } else { - change.setText(FileSaveUtil.cleanFileName(text)); - ; - return change; - } - // - // return null; - }; - TextFormatter textFormatter = new TextFormatter<>(filter); - fileNameTextField.setTextFormatter(textFormatter); - - // Effects - GaussianBlur gaussianBlur = new GaussianBlur(0); - Glow glow = new Glow(0); - gaussianBlur.setInput(glow); - - // Bind blur slider to portraitImageView opacity - portraitBlurSlider.valueProperty().addListener(new ChangeListener() { - public void changed(ObservableValue ov, - Number old_val, Number new_val) { - gaussianBlur.setRadius(new_val.doubleValue()); - portraitImageView.setEffect(gaussianBlur); - updateTokenPreviewImageView(); - } - }); - - // Bind glow slider to portraitImageView opacity - portraitGlowSlider.valueProperty().addListener(new ChangeListener() { - public void changed(ObservableValue ov, - Number old_val, Number new_val) { - glow.setLevel(new_val.doubleValue()); - portraitImageView.setEffect(gaussianBlur); - updateTokenPreviewImageView(); - } - }); - - // Bind transparency slider to overlayImageView opacity - overlayTransparencySlider.valueProperty().addListener(new ChangeListener() { - public void changed(ObservableValue ov, - Number old_val, Number new_val) { - overlayImageView.setOpacity(new_val.doubleValue()); - updateTokenPreviewImageView(); - } - }); - - // Bind width/height spinners to overlay width/height - overlayWidthSpinner.getValueFactory().valueProperty() - .bindBidirectional(overlayHeightSpinner.getValueFactory().valueProperty()); - overlayWidthSpinner.valueProperty() - .addListener((observable, oldValue, newValue) -> overlayWidthSpinner_onTextChanged(oldValue, newValue)); - overlayHeightSpinner.valueProperty().addListener( - (observable, oldValue, newValue) -> overlayHeightSpinner_onTextChanged(oldValue, newValue)); - } - - @FXML - void removeBackgroundButton_onAction(ActionEvent event) { - backgroundColorPicker.setValue(Color.TRANSPARENT); - updateTokenPreviewImageView(); - } - - @FXML - void fileManageOverlaysMenu_onAction(ActionEvent event) { - @SuppressWarnings("unused") - ManageOverlays manageOverlays = new ManageOverlays(this); - } - - @FXML - void fileSaveAsMenu_onAction(ActionEvent event) { - saveToken(); - } - - @FXML - void fileExitMenu_onAction(ActionEvent event) { - exitApplication(); - } - - @FXML - void editCaptureScreenMenu_onAction(ActionEvent event) { - regionSelector = new RegionSelector(this); - } - - @FXML - void editCopyImageMenu_onAction(ActionEvent event) { - Clipboard clipboard = Clipboard.getSystemClipboard(); - ClipboardContent content = new ClipboardContent(); - - // for paste as file, e.g. in Windows Explorer - try { - File tempTokenFile = fileSaveUtil.getTempFileName(false, useFileNumberingCheckbox.isSelected(), - fileNameTextField.getText(), fileNameSuffixTextField); - - writeTokenImage(tempTokenFile); - content.putFiles(java.util.Collections.singletonList(tempTokenFile)); - tempTokenFile.deleteOnExit(); - } catch (Exception e) { - log.error(e); - } - - // for paste as image, e.g. in GIMP - content.putImage(tokenImageView.getImage()); - - // Finally, put contents on clip board - clipboard.setContent(content); - } - - @FXML - void editPasteImageMenu_onAction(ActionEvent event) { - Clipboard clipboard = Clipboard.getSystemClipboard(); - Image originalImage = portraitImageView.getImage(); - - // Strangely, we get an error if we try to paste an image we put in the clipboard ourselves but File works ok? - // -Dprism.order=sw also fixes it but not sure why... - // So lets just check for File first... - if (clipboard.hasFiles()) { - clipboard.getFiles().forEach(file -> { - try { - Image cbImage = new Image(file.toURI().toURL().toExternalForm()); - - if (cbImage != null) - updatePortrait(cbImage); - - updateFileNameTextField(FilenameUtils.getBaseName(file.toURI().toURL().toExternalForm())); - } catch (Exception e) { - log.error("Could not load image " + file); - e.printStackTrace(); - } - }); - } else if (clipboard.hasImage()) { - try { - Image cbImage = clipboard.getImage(); - if (cbImage != null) - updatePortrait(cbImage); - } catch (IllegalArgumentException e) { - log.info(e); - updatePortrait(originalImage); - } - } else if (clipboard.hasUrl()) { - try { - Image cbImage = new Image(clipboard.getUrl()); - if (cbImage != null) - updatePortrait(cbImage); - - updateFileNameTextField(FileSaveUtil.searchURL(clipboard.getUrl())); - } catch (IllegalArgumentException e) { - log.info(e); - } - } else if (clipboard.hasString()) { - try { - Image cbImage = new Image(clipboard.getString()); - if (cbImage != null) - updatePortrait(cbImage); - - updateFileNameTextField(FileSaveUtil.searchURL(clipboard.getString())); - } catch (IllegalArgumentException e) { - log.info(e); - } - } - } - - @FXML - void helpAboutMenu_onAction(ActionEvent event) { - @SuppressWarnings("unused") - Credits credits = new Credits(this); - } - - @FXML - void useFileNumberingCheckbox_onAction(ActionEvent event) { - fileNameSuffixLabel.setDisable(!useFileNumberingCheckbox.isSelected()); - fileNameSuffixTextField.setDisable(!useFileNumberingCheckbox.isSelected()); - } - - @FXML - void compositeTokenPane_MouseDragged(MouseEvent event) { - portraitImageView.setTranslateX(event.getX() - dragStart.x + portraitImageStart.x); - portraitImageView.setTranslateY(event.getY() - dragStart.y + portraitImageStart.y); - - updateTokenPreviewImageView(); - } - - @FXML - void compositeTokenPane_MousePressed(MouseEvent event) { - dragStart.setLocation(event.getX(), event.getY()); - portraitImageStart.setLocation(portraitImageView.getTranslateX(), portraitImageView.getTranslateY()); - portraitImageView.setCursor(Cursor.MOVE); - } - - @FXML - void compositeTokenPane_MouseReleased(MouseEvent event) { - portraitImageView.setCursor(Cursor.HAND); - updateTokenPreviewImageView(); - } - - @FXML - void compositeTokenPane_MouseEntered(MouseEvent event) { - portraitImageView.setCursor(Cursor.HAND); - } - - @FXML - void compositeTokenPane_MouseDragExited(MouseDragEvent event) { - } - - @FXML - void compositeTokenPane_MouseExited(MouseEvent event) { - } - - @FXML - void compositeTokenPane_MouseMoved(MouseEvent event) { - } - - @FXML - void compositeTokenPane_OnScroll(ScrollEvent event) { - // if event is touch enabled, skip this as it will be handled by onZoom & onRotate handlers - if (event.isDirect()) - return; - - if (event.isShiftDown()) { - // Note: OK, this is stupid but on Windows shift + mousewheel returns X delta but on Ubuntu it returns Y delta... - double delta = event.getDeltaY(); - if (delta == 0) - delta = event.getDeltaX(); - - Double r = portraitImageView.getRotate() + delta / 20; - - if (r < -360d || r > 360d) - r = 0d; - - portraitImageView.setRotate(r); - } else { - Double scale = portraitImageView.getScaleY() * Math.pow(1.001, event.getDeltaY()); - - portraitImageView.setScaleX(scale); - portraitImageView.setScaleY(scale); - } - - event.consume(); - updateTokenPreviewImageView(); - } - - @FXML - void compositeTokenPane_OnZoom(ZoomEvent event) { - Double scale = portraitImageView.getScaleY() * event.getZoomFactor(); - - portraitImageView.setScaleX(scale); - portraitImageView.setScaleY(scale); - } - - @FXML - void compositeTokenPane_OnRotate(RotateEvent event) { - log.info("isDirect(): " + event.isDirect()); - log.info("getTotalAngle" + event.getTotalAngle()); - - double r = portraitImageView.getRotate() + (event.getAngle() * 0.75); - if (r < -360d || r > 360d) - r = 0d; - - portraitImageView.setRotate(r); - event.consume(); - } - - @FXML - void compositeTokenPane_DragDropped(DragEvent event) { - Dragboard db = event.getDragboard(); - - // Strangely, we get an error if we try to paste an image we put in the clipboard ourselves but File works ok? - // -Dprism.order=sw also fixes it but not sure why... - // So lets just check for File first... - if (db.hasFiles()) { - db.getFiles().forEach(file -> { - try { - updateFileNameTextField(FilenameUtils.getBaseName(file.toURI().toURL().toExternalForm())); - updatePortrait(new Image(file.toURI().toURL().toExternalForm())); - } catch (Exception e) { - log.error("Could not load image " + file, e); - } - }); - event.setDropCompleted(true); - } else if (db.hasImage()) { - updatePortrait(db.getImage()); - event.setDropCompleted(true); - } else if (db.hasUrl()) { - updateFileNameTextField(FileSaveUtil.searchURL(db.getUrl())); - updatePortrait(new Image(db.getUrl())); - event.setDropCompleted(true); - } - } - - @FXML - void compositeTokenPane_DragDone(DragEvent event) { - updateTokenPreviewImageView(); - } - - @FXML - void compositeTokenPane_DragOver(DragEvent event) { - if (event.getDragboard().hasImage() || event.getDragboard().hasFiles() || event.getDragboard().hasUrl()) { - // Set Pane color to an alpha green - event.acceptTransferModes(TransferMode.COPY); - } else { - // Set Pane color to an alpha red? - event.acceptTransferModes(TransferMode.ANY); - } - } - - @FXML - void tokenImageView_OnDragDetected(MouseEvent event) { - Dragboard db = tokenImageView.startDragAndDrop(TransferMode.ANY); - ClipboardContent content = new ClipboardContent(); - - boolean saveAsToken = false; - - try { - File tempTokenFile = fileSaveUtil.getTempFileName(saveAsToken, useFileNumberingCheckbox.isSelected(), - fileNameTextField.getText(), fileNameSuffixTextField); - - writeTokenImage(tempTokenFile); - content.putFiles(java.util.Collections.singletonList(tempTokenFile)); - tempTokenFile.deleteOnExit(); - } catch (Exception e) { - log.error(e); - } finally { - content.putImage(tokenImageView.getImage()); - db.setContent(content); - event.consume(); - } - } - - @FXML - void tokenImageView_OnDragDone(DragEvent event) { - if (event.getAcceptedTransferMode() != null) - updateOverlayTreeViewRecentFolder(true); - } - - @FXML - void overlayUseAsBaseCheckbox_onAction(ActionEvent event) { - if (overlayUseAsBaseCheckbox.isSelected()) - compositeGroup.toBack(); - else - portraitScrollPane.toBack(); - - updateTokenPreviewImageView(); - } - - @FXML - void backgroundColorPicker_onAction(ActionEvent event) { - updateTokenPreviewImageView(); - } - - @FXML - void overlayAspectToggleButton_onAction(ActionEvent event) { - if (overlayAspectToggleButton.isSelected()) { - overlayImageView.setPreserveRatio(true); - maskImageView.setPreserveRatio(true); - overlayWidthSpinner.getValueFactory().valueProperty() - .bindBidirectional(overlayHeightSpinner.getValueFactory().valueProperty()); - } else { - overlayImageView.setPreserveRatio(false); - maskImageView.setPreserveRatio(false); - overlayWidthSpinner.getValueFactory().valueProperty() - .unbindBidirectional(overlayHeightSpinner.getValueFactory().valueProperty()); - } - - updateTokenPreviewImageView(); - } - - void overlayWidthSpinner_onTextChanged(double oldValue, double newValue) { - if (newValue < overlaySpinnerSteps.first()) - newValue = overlaySpinnerSteps.first(); - - if (newValue > overlaySpinnerSteps.last()) - newValue = overlaySpinnerSteps.last(); - - if (newValue > oldValue) - overlayWidthSpinner.getValueFactory().setValue(overlaySpinnerSteps.ceiling(newValue)); - else - overlayWidthSpinner.getValueFactory().setValue(overlaySpinnerSteps.floor(newValue)); - - overlayImageView.setFitWidth(overlayWidthSpinner.getValue()); - maskImageView.setFitWidth(overlayWidthSpinner.getValue()); - - updateTokenPreviewImageView(); - } - - void overlayHeightSpinner_onTextChanged(double oldValue, double newValue) { - if (newValue < overlaySpinnerSteps.first()) - newValue = overlaySpinnerSteps.first(); - - if (newValue > overlaySpinnerSteps.last()) - newValue = overlaySpinnerSteps.last(); - - if (newValue > oldValue) - overlayHeightSpinner.getValueFactory().setValue(overlaySpinnerSteps.ceiling(newValue)); - else - overlayHeightSpinner.getValueFactory().setValue(overlaySpinnerSteps.floor(newValue)); - - overlayImageView.setFitHeight(overlayHeightSpinner.getValue()); - maskImageView.setFitHeight(overlayHeightSpinner.getValue()); - - updateTokenPreviewImageView(); - } - - public Map> getRecentOverlayTreeItems() { - return recentOverlayTreeItems; - } - - public void updateRecentOverlayTreeItems(Path filePath) { - try { - TreeItem recentOverlay = new TreeItem(filePath, ImageUtil.getOverlayThumb(new ImageView(), filePath)); - - // Remove first so if it is on the list it forces to top of list - recentOverlayTreeItems.remove(filePath); - recentOverlayTreeItems.put(filePath, recentOverlay); - } catch (IOException e) { - log.error("Error loading recent overlay preference for " + filePath.toString()); - } - } - - public void expandOverlayOptionsPane(boolean expand) { - overlayOptionsPane.setExpanded(expand); - } - - public void expandBackgroundOptionsPane(boolean expand) { - backgroundOptionsPane.setExpanded(expand); - } - - public void updateOverlayTreeview(TreeItem overlayTreeItems) { - overlayTreeView.setRoot(overlayTreeItems); - } - - public void updateTokenPreviewImageView() { - tokenImageView.setImage(ImageUtil.composePreview(compositeTokenPane, backgroundColorPicker.getValue(), - portraitImageView, maskImageView, overlayImageView, overlayUseAsBaseCheckbox.isSelected(), clipPortraitCheckbox.isSelected())); - tokenImageView.setPreserveRatio(true); - } - - private void saveToken() { - FileChooser fileChooser = new FileChooser(); - - try { - File tokenFile = fileSaveUtil.getFileName(false, useFileNumberingCheckbox.isSelected(), fileNameTextField.getText(), fileNameSuffixTextField); - fileChooser.setInitialFileName(tokenFile.getName()); - if (tokenFile.getParentFile() != null) - if (tokenFile.getParentFile().isDirectory()) - fileChooser.setInitialDirectory(tokenFile.getParentFile()); - } catch (IOException e1) { - log.error("Error writing token!", e1); - } - - fileChooser.getExtensionFilters().addAll(AppConstants.IMAGE_EXTENSION_FILTER); - fileChooser.setTitle(I18N.getString("TokenTool.save.filechooser.title")); - fileChooser.setSelectedExtensionFilter(AppConstants.IMAGE_EXTENSION_FILTER); - - File tokenSaved = fileChooser.showSaveDialog(saveOptionsPane.getScene().getWindow()); - - if (tokenSaved == null) - return; - - writeTokenImage(tokenSaved); - - updateFileNameTextField(FilenameUtils.getBaseName(tokenSaved.getName())); - FileSaveUtil.setLastFile(tokenSaved); - updateOverlayTreeViewRecentFolder(true); - } - - private boolean writeTokenImage(File tokenFile) { - try { - Image tokenImage; - if (clipPortraitCheckbox.isSelected()) - tokenImage = ImageUtil.resizeCanvas(tokenImageView.getImage(), getOverlayWidth(), getOverlayHeight()); - else - tokenImage = tokenImageView.getImage(); - - return ImageIO.write(SwingFXUtils.fromFXImage(tokenImage, null), "png", tokenFile); - } catch (IOException e) { - log.error("Unable to write token to file: " + tokenFile.getAbsolutePath(), e); - } catch (IndexOutOfBoundsException e) { - log.error("Image width/height out of bounds: " + getOverlayWidth() + " x " + getOverlayHeight(), e); - } - - return false; - } - - public void updateOverlayTreeViewRecentFolder(boolean selectMostRecent) { - if (lastSelectedItem != null) - updateRecentOverlayTreeItems(lastSelectedItem.getValue()); - - // Update Recent Overlay List - if (!recentOverlayTreeItems.isEmpty()) { - // Remember current selection (adding/removing tree items messes with the selection model) - // int selectedItem = overlayTreeView.getSelectionModel().getSelectedIndex(); - overlayTreeView.getSelectionModel().clearSelection(); - - // Clear current folder - recentFolder.getChildren().clear(); - - // Add recent list to recentFolder in reverse order so most recent is at the top - ListIterator>> iter = new ArrayList<>(recentOverlayTreeItems.entrySet()).listIterator(recentOverlayTreeItems.size()); - while (iter.hasPrevious()) - recentFolder.getChildren().add(iter.previous().getValue()); - - if (overlayTreeView.getRoot().getChildren().indexOf(recentFolder) == -1) { - overlayTreeView.getRoot().getChildren().add(recentFolder); - } else { - overlayTreeView.getRoot().getChildren().remove(recentFolder); - overlayTreeView.getRoot().getChildren().add(recentFolder); - } - - // Auto expand recent folder... - recentFolder.setExpanded(true); - - addPseudoClassToLeafs(overlayTreeView); - - // Set the selected index back to what it was unless... - if (selectMostRecent) { - overlayTreeView.getSelectionModel().select(recentFolder.getChildren().get(0)); - } else { - // overlayTreeView.getSelectionModel().clearAndSelect(selectedItem); - } - - } - } - - private void addPseudoClassToLeafs(TreeView tree) { - PseudoClass leaf = PseudoClass.getPseudoClass("leaf"); - - tree.setCellFactory(tv -> { - TreeCell cell = new TreeCell<>(); - cell.itemProperty().addListener((obs, oldValue, newValue) -> { - if (newValue == null) { - cell.setText(""); - cell.setGraphic(null); - } else { - cell.setText(newValue.toFile().getName()); - cell.setGraphic(cell.getTreeItem().getGraphic()); - } - }); - cell.treeItemProperty().addListener((obs, oldTreeItem, newTreeItem) -> cell.pseudoClassStateChanged(leaf, - newTreeItem != null && newTreeItem.isLeaf())); - return cell; - }); - } - - public void updatePortrait(Image newPortraitImage) { - double w = newPortraitImage.getWidth(); - double h = newPortraitImage.getHeight(); - double pw = portraitScrollPane.getWidth(); - double ph = portraitScrollPane.getHeight(); - - portraitImageView.setImage(newPortraitImage); - - portraitImageView.setTranslateX((pw - w) / 2); - portraitImageView.setTranslateY((ph - h) / 2); - portraitImageView.setScaleX(1); - portraitImageView.setScaleY(1); - portraitImageView.setRotate(0d); - - updateTokenPreviewImageView(); - } - - private void updateCompositImageView(TreeItem treeNode) { - // Node removed... - if (treeNode == null) - return; - - // I'm not a leaf on the wind! (Sub directory node) - if (treeNode.getChildren().size() > 0) - return; - - try { - Path filePath = treeNode.getValue(); - lastSelectedItem = treeNode; - - // Set the Image Views - maskImageView = ImageUtil.getMaskImage(maskImageView, filePath); - overlayImageView = ImageUtil.getOverlayImage(overlayImageView, filePath); - - // Set the text label - overlayNameLabel.setText(FilenameUtils.getBaseName(filePath.toFile().getName())); - - updateTokenPreviewImageView(); - } catch (IOException e) { - // Not a valid URL, most likely this is just because it's a directory node. - e.printStackTrace(); - } - } - - public Color getBackgroundColor() { - return backgroundColorPicker.getValue(); - } - - public void setBackgroundColor(Color newColor) { - backgroundColorPicker.setValue(newColor); - } - - public void refreshCache() { - overlayTreeProgressBar.setStyle(""); - overlayTreeProgressBar.setVisible(true); - overlayTreeProgressBar.setOpacity(1.0); - overlayNameLabel.setOpacity(0.0); - progressBarLabel.setVisible(true); - updateOverlayTreeview(null); - - try { - loadCount.set(0); - overlayCount = (int) Files.walk(AppConstants.OVERLAY_DIR.toPath()).filter(Files::isRegularFile).count(); - log.info("overlayCount: " + overlayCount); - - treeItems = cacheOverlays(AppConstants.OVERLAY_DIR, null, AppConstants.THUMB_SIZE); - } catch (IOException e) { - log.error("Error reloading overlay cache!", e); - } - } - - private void treeViewFinish() { - log.info("***treeViewFinish called"); - // Sort the nodes off of root - treeItems = sortTreeNodes(treeItems); - - updateOverlayTreeview(treeItems); - addPseudoClassToLeafs(overlayTreeView); - updateOverlayTreeViewRecentFolder(false); - - // overlayNameLabel.setVisible(true); - overlayTreeProgressBar.setStyle("-fx-accent: forestgreen;"); - progressBarLabel.setVisible(false); - - FadeTransition fadeOut = new FadeTransition(Duration.millis(2000)); - fadeOut.setNode(overlayTreeProgressBar); - fadeOut.setFromValue(1.0); - fadeOut.setToValue(0.0); - fadeOut.setCycleCount(1); - fadeOut.setAutoReverse(false); - fadeOut.playFromStart(); - - FadeTransition fadeIn = new FadeTransition(Duration.millis(4000)); - fadeIn.setNode(overlayNameLabel); - fadeIn.setFromValue(0.0); - fadeIn.setToValue(1.0); - fadeIn.setCycleCount(1); - fadeIn.setAutoReverse(false); - fadeIn.playFromStart(); - - } - - private TreeItem cacheOverlays(File dir, TreeItem parent, int THUMB_SIZE) throws IOException { - log.info("Caching " + dir.getAbsolutePath()); - - TreeItem root = new TreeItem<>(dir.toPath()); - root.setExpanded(false); - File[] files = dir.listFiles(); - final String I18N_CACHE_TEXT = I18N.getString("TokenTool.treeview.caching"); - - final Task task = new Task() { - @Override - protected Void call() throws Exception { - for (File file : files) { - if (loadOverlaysThread.isInterrupted()) - break; - - if (file.isDirectory()) { - cacheOverlays(file, root, THUMB_SIZE); - } else { - Path filePath = file.toPath(); - TreeItem imageNode = new TreeItem<>(filePath, ImageUtil.getOverlayThumb(new ImageView(), filePath)); - root.getChildren().add(imageNode); - loadCount.getAndIncrement(); - overlayTreeProgressBar.progressProperty().set(loadCount.doubleValue() / overlayCount); - } - } - - if (parent != null) { - // When we show the overlay image, the TreeItem value is empty so we need to - // sort those to the bottom for a cleaner look and keep sub dir's at the top. - // If a node has no children then it's an overlay, otherwise it's a directory... - root.getChildren().sort(new Comparator>() { - @Override - public int compare(TreeItem o1, TreeItem o2) { - if (o1.getChildren().size() == 0 && o2.getChildren().size() == 0) - return 0; - else if (o1.getChildren().size() == 0) - return Integer.MAX_VALUE; - else if (o2.getChildren().size() == 0) - return Integer.MIN_VALUE; - else - return o1.getValue().compareTo(o2.getValue()); - } - }); - - parent.getChildren().add(root); - - parent.getChildren().sort(new Comparator>() { - @Override - public int compare(TreeItem o1, TreeItem o2) { - if (o1.getChildren().size() == 0 && o2.getChildren().size() == 0) - return 0; - else if (o1.getChildren().size() == 0) - return Integer.MAX_VALUE; - else if (o2.getChildren().size() == 0) - return Integer.MIN_VALUE; - else - return o1.getValue().compareTo(o2.getValue()); - } - }); - } - - return null; - } - }; - - overlayTreeProgressBar.progressProperty().addListener(observable -> { - Platform.runLater(() -> progressBarLabel.setText(I18N_CACHE_TEXT + Math.round(overlayCount - loadCount.doubleValue()) + "...")); - }); - - // Only add this listener to the parent task so it's only called once - if (parent == null) { - overlayTreeProgressBar.progressProperty().addListener(observable -> { - Platform.runLater(() -> { - if (overlayTreeProgressBar.getProgress() >= 1) - treeViewFinish(); - }); - }); - } - - executorService.execute(task); - return root; - } - - private TreeItem sortTreeNodes(TreeItem tree) { - // Sort the nodes off of root - tree.getChildren().sort(new Comparator>() { - @Override - public int compare(TreeItem o1, TreeItem o2) { - if (o1.getChildren().size() == 0 && o2.getChildren().size() == 0) - return 0; - else if (o1.getChildren().size() == 0) - return Integer.MAX_VALUE; - else if (o2.getChildren().size() == 0) - return Integer.MIN_VALUE; - else - return o1.getValue().compareTo(o2.getValue()); - } - }); - - return tree; - } - - /* - * getter/setter methods, mainly for user preferences - */ - public double getOverlayWidth() { - return overlayWidthSpinner.getValue(); - } - - public void setOverlayWidth(double newValue) { - overlayWidthSpinner.getValueFactory().setValue(overlaySpinnerSteps.ceiling(newValue)); - } - - public double getOverlayHeight() { - return overlayHeightSpinner.getValue(); - } - - public void setOverlayHeight(double newValue) { - overlayHeightSpinner.getValueFactory().setValue(overlaySpinnerSteps.ceiling(newValue)); - } - - public boolean getOverlayAspect() { - return overlayAspectToggleButton.isSelected(); - } - - public void setOverlayAspect(boolean selected) { - // UI normally starts this toggle as selected == aspect locked - if (!selected) - overlayAspectToggleButton.fire(); - } - - public boolean getOverlayUseAsBase() { - return overlayUseAsBaseCheckbox.isSelected(); - } - - public void setOverlayUseAsBase(boolean selected) { - if (selected) - overlayUseAsBaseCheckbox.fire(); - } - - public boolean getClipPortraitCheckbox() { - return clipPortraitCheckbox.isSelected(); - } - - public void setClipPortraitCheckbox(boolean selected) { - if (selected) - clipPortraitCheckbox.fire(); - } - - public String getFileNameTextField() { - return fileNameTextField.getText(); - } - - public void setFileNameTextField(String text) { - fileNameTextField.setText(text); - } - - public void updateFileNameTextField(String text) { - if (!getUseFileNumberingCheckbox()) - if (text == null || text.isEmpty()) - fileNameTextField.setText(AppConstants.DEFAULT_TOKEN_NAME); - else - fileNameTextField.setText(FileSaveUtil.cleanFileName(text)); - } - - public boolean getUseFileNumberingCheckbox() { - return useFileNumberingCheckbox.isSelected(); - } - - public void setUseFileNumberingCheckbox(boolean selected) { - if (selected) - useFileNumberingCheckbox.fire(); - } - - public String getFileNameSuffixTextField() { - return fileNameSuffixTextField.getText(); - } - - public void setFileNameSuffixTextField(String text) { - fileNameSuffixTextField.setText(text); - } - - public Image getPortraitImage() { - return portraitImageView.getImage(); - } - - public void setPortraitImage(Image newPortraitImage, double x, double y, double r, double s) { - updatePortrait(newPortraitImage); - portraitImageView.setTranslateX(x); - portraitImageView.setTranslateY(y); - portraitImageView.setRotate(r); - portraitImageView.setScaleX(s); - portraitImageView.setScaleY(s); - } - - public void updatePortraitLocation(double xDelta, double yDelta) { - if (xDelta != 0) - portraitImageView.setTranslateX(portraitImageView.getTranslateX() + (xDelta / 2)); - - if (yDelta != 0) - portraitImageView.setTranslateY(portraitImageView.getTranslateY() + (yDelta / 2)); - } - - public ImageView getPortraitImageView() { - return portraitImageView; - } - - public void exitApplication() { - try { - AppPreferences.savePreferences(this); - } catch (Exception e) { - log.error("Error saving preferences!", e); - } finally { - log.info("Exiting application."); - Platform.exit(); - } - } + @FXML private MenuItem fileOpenPDF_Menu; + @FXML private MenuItem fileManageOverlaysMenu; + @FXML private MenuItem fileSaveAsMenu; + @FXML private MenuItem fileExitMenu; + + @FXML private MenuItem editCaptureScreenMenu; + @FXML private MenuItem editCopyImageMenu; + @FXML private MenuItem editPasteImageMenu; + + @FXML private MenuItem helpAboutMenu; + + @FXML private TitledPane saveOptionsPane; + @FXML private TitledPane overlayOptionsPane; + @FXML private TitledPane backgroundOptionsPane; + @FXML private TitledPane zoomOptionsPane; + @FXML private StackPane compositeTokenPane; + @FXML private BorderPane tokenPreviewPane; + @FXML private ScrollPane portraitScrollPane; + + @FXML private Group compositeGroup; + @FXML private Pane dndHighlights; + + @FXML private TreeView overlayTreeView; + + @FXML private StackPane imagesStackPane; + @FXML private ImageView backgroundImageView; // The background image layer + @FXML private ImageView portraitImageView; // The bottom "Portrait" layer + @FXML private ImageView maskImageView; // The mask layer used to crop the Portrait layer + @FXML private ImageView overlayImageView; // The overlay layer to apply on top of everything + @FXML private ImageView tokenImageView; // The final token image created + + @FXML private CheckBox useFileNumberingCheckbox; + @FXML private CheckBox useTokenNameCheckbox; + @FXML private CheckBox savePortraitOnDragCheckbox; + @FXML private CheckBox useBackgroundOnDragCheckbox; + @FXML private CheckBox overlayUseAsBaseCheckbox; + @FXML private CheckBox clipPortraitCheckbox; + + @FXML private TextField fileNameTextField; + @FXML private Label fileNameSuffixLabel; + @FXML private TextField fileNameSuffixTextField; + @FXML private TextField portraitNameTextField; + @FXML private Label portraitNameSuffixLabel; + @FXML private TextField portraitNameSuffixTextField; + + @FXML private Label overlayNameLabel; + @FXML private Label overlayInfoLabel; + @FXML private ColorPicker backgroundColorPicker; + @FXML private ToggleButton overlayAspectToggleButton; + + @FXML private Slider portraitTransparencySlider; + @FXML private Slider portraitBlurSlider; + @FXML private Slider portraitGlowSlider; + + @FXML private Slider overlayTransparencySlider; + + @FXML private Spinner overlayWidthSpinner; + @FXML private Spinner overlayHeightSpinner; + + @FXML private ProgressBar overlayTreeProgressBar; + @FXML private Label progressBarLabel; + + @FXML private MenuButton layerMenuButton; + @FXML private RadioMenuItem backgroundMenuItem; + @FXML private RadioMenuItem portraitMenuItem; + @FXML private RadioMenuItem overlayMenuItem; + + private static final Logger log = LogManager.getLogger(TokenTool_Controller.class); + + private static ExecutorService executorService; + private static Thread loadOverlaysThread = new Thread(); + private static AtomicInteger loadCount = new AtomicInteger(0); + + private static int overlayCount; + + private static TreeItem treeItems; + private static TreeItem lastSelectedItem; + private static TreeItem recentFolder = + new TreeItem<>(new File(AppConstants.OVERLAY_DIR, "Recent").toPath(), null); + + private static Map> recentOverlayTreeItems = + new LinkedHashMap>() { + private static final long serialVersionUID = 2579964060760662199L; + + @Override + protected boolean removeEldestEntry(Map.Entry> eldest) { + return size() > AppConstants.MAX_RECENT_SIZE; + } + }; + + private static Point dragStart = new Point(); + private static Point currentImageOffset = new Point(); + + private FileSaveUtil fileSaveUtil = new FileSaveUtil(); + + // A custom set of Width/Height sizes to use for Overlays + private NavigableSet overlaySpinnerSteps = + new TreeSet( + Arrays.asList( + 50, 100, 128, 150, 200, 256, 300, 400, 500, 512, 600, 700, 750, 800, 900, 1000)); + + @FXML + void initialize() { + // Note: A Pane is added to the compositeTokenPane so the ScrollPane doesn't consume the mouse + // events + assert fileOpenPDF_Menu != null + : "fx:id=\"fileOpenPDF_Menu\" was not injected: check your FXML file 'TokenTool.fxml'."; + assert fileManageOverlaysMenu != null + : "fx:id=\"fileManageOverlaysMenu\" was not injected: check your FXML file 'TokenTool.fxml'."; + assert fileSaveAsMenu != null + : "fx:id=\"fileSaveAsMenu\" was not injected: check your FXML file 'TokenTool.fxml'."; + assert fileExitMenu != null + : "fx:id=\"fileExitMenu\" was not injected: check your FXML file 'TokenTool.fxml'."; + + assert editCaptureScreenMenu != null + : "fx:id=\"editCaptureScreenMenu\" was not injected: check your FXML file 'TokenTool.fxml'."; + assert editCopyImageMenu != null + : "fx:id=\"editCopyImageMenu\" was not injected: check your FXML file 'TokenTool.fxml'."; + assert editPasteImageMenu != null + : "fx:id=\"editPasteImageMenu\" was not injected: check your FXML file 'TokenTool.fxml'."; + + assert helpAboutMenu != null + : "fx:id=\"helpAboutMenu\" was not injected: check your FXML file 'TokenTool.fxml'."; + + assert saveOptionsPane != null + : "fx:id=\"saveOptionsPane\" was not injected: check your FXML file 'TokenTool.fxml'."; + assert overlayOptionsPane != null + : "fx:id=\"overlayOptionsPane\" was not injected: check your FXML file 'TokenTool.fxml'."; + assert backgroundOptionsPane != null + : "fx:id=\"backgroundOptionsPane\" was not injected: check your FXML file 'TokenTool.fxml'."; + assert zoomOptionsPane != null + : "fx:id=\"zoomOptionsPane\" was not injected: check your FXML file 'TokenTool.fxml'."; + + assert compositeTokenPane != null + : "fx:id=\"compositeTokenPane\" was not injected: check your FXML file 'TokenTool.fxml'."; + assert tokenPreviewPane != null + : "fx:id=\"tokenPreviewPane\" was not injected: check your FXML file 'TokenTool.fxml'."; + assert portraitScrollPane != null + : "fx:id=\"portraitScrollPane\" was not injected: check your FXML file 'TokenTool.fxml'."; + + assert compositeGroup != null + : "fx:id=\"compositeGroup\" was not injected: check your FXML file 'TokenTool.fxml'."; + assert dndHighlights != null + : "fx:id=\"dndHighlights\" was not injected: check your FXML file 'TokenTool.fxml'."; + + assert overlayTreeView != null + : "fx:id=\"overlayTreeview\" was not injected: check your FXML file 'TokenTool.fxml'."; + + assert backgroundImageView != null + : "fx:id=\"backgroundImageView\" was not injected: check your FXML file 'TokenTool.fxml'."; + assert portraitImageView != null + : "fx:id=\"portraitImageView\" was not injected: check your FXML file 'TokenTool.fxml'."; + assert maskImageView != null + : "fx:id=\"maskImageView\" was not injected: check your FXML file 'TokenTool.fxml'."; + assert overlayImageView != null + : "fx:id=\"overlayImageView\" was not injected: check your FXML file 'TokenTool.fxml'."; + assert tokenImageView != null + : "fx:id=\"tokenImageView\" was not injected: check your FXML file 'TokenTool.fxml'."; + + assert useFileNumberingCheckbox != null + : "fx:id=\"useFileNumberingCheckbox\" was not injected: check your FXML file 'TokenTool.fxml'."; + assert useTokenNameCheckbox != null + : "fx:id=\"useTokenNameCheckbox\" was not injected: check your FXML file 'TokenTool.fxml'."; + assert savePortraitOnDragCheckbox != null + : "fx:id=\"savePortraitOnDragCheckbox\" was not injected: check your FXML file 'TokenTool.fxml'."; + assert useBackgroundOnDragCheckbox != null + : "fx:id=\"useBackgroundOnDragCheckbox\" was not injected: check your FXML file 'TokenTool.fxml'."; + assert overlayUseAsBaseCheckbox != null + : "fx:id=\"overlayUseAsBaseCheckbox\" was not injected: check your FXML file 'TokenTool.fxml'."; + assert clipPortraitCheckbox != null + : "fx:id=\"clipPortraitCheckbox\" was not injected: check your FXML file 'TokenTool.fxml'."; + + assert fileNameTextField != null + : "fx:id=\"fileNameTextField\" was not injected: check your FXML file 'TokenTool.fxml'."; + assert fileNameSuffixLabel != null + : "fx:id=\"fileNameSuffixLabel\" was not injected: check your FXML file 'TokenTool.fxml'."; + assert fileNameSuffixTextField != null + : "fx:id=\"fileNameSuffixTextField\" was not injected: check your FXML file 'TokenTool.fxml'."; + assert portraitNameTextField != null + : "fx:id=\"portraitNameTextField\" was not injected: check your FXML file 'TokenTool.fxml'."; + assert portraitNameSuffixLabel != null + : "fx:id=\"portraitNameSuffixLabel\" was not injected: check your FXML file 'TokenTool.fxml'."; + assert portraitNameSuffixTextField != null + : "fx:id=\"portraitNameSuffixTextField\" was not injected: check your FXML file 'TokenTool.fxml'."; + + assert overlayNameLabel != null + : "fx:id=\"overlayNameLabel\" was not injected: check your FXML file 'TokenTool.fxml'."; + assert overlayInfoLabel != null + : "fx:id=\"overlayInfoLabel\" was not injected: check your FXML file 'TokenTool.fxml'."; + assert backgroundColorPicker != null + : "fx:id=\"backgroundColorPicker\" was not injected: check your FXML file 'TokenTool.fxml'."; + assert overlayAspectToggleButton != null + : "fx:id=\"overlayAspectToggleButton\" was not injected: check your FXML file 'TokenTool.fxml'."; + + assert portraitTransparencySlider != null + : "fx:id=\"portraitTransparencySlider\" was not injected: check your FXML file 'TokenTool.fxml'."; + assert portraitBlurSlider != null + : "fx:id=\"portraitBlurSlider\" was not injected: check your FXML file 'TokenTool.fxml'."; + assert portraitGlowSlider != null + : "fx:id=\"portraitGlowSlider\" was not injected: check your FXML file 'TokenTool.fxml'."; + + assert overlayTransparencySlider != null + : "fx:id=\"overlayTransparencySlider\" was not injected: check your FXML file 'TokenTool.fxml'."; + + assert overlayWidthSpinner != null + : "fx:id=\"overlayWidthSpinner\" was not injected: check your FXML file 'TokenTool.fxml'."; + assert overlayHeightSpinner != null + : "fx:id=\"overlayHeightSpinner\" was not injected: check your FXML file 'TokenTool.fxml'."; + + assert overlayTreeProgressBar != null + : "fx:id=\"overlayTreeProgressIndicator\" was not injected: check your FXML file 'ManageOverlays.fxml'."; + + assert layerMenuButton != null + : "fx:id=\"layerMenuButton\" was not injected: check your FXML file 'TokenTool.fxml'."; + assert backgroundMenuItem != null + : "fx:id=\"backgroundMenuItem\" was not injected: check your FXML file 'TokenTool.fxml'."; + assert portraitMenuItem != null + : "fx:id=\"portraitMenuItem\" was not injected: check your FXML file 'TokenTool.fxml'."; + assert overlayMenuItem != null + : "fx:id=\"overlayMenuItem\" was not injected: check your FXML file 'TokenTool.fxml'."; + + // We're getting the defaults set by the FXML before updating them with the saved preferences... + AppConstants.DEFAULT_MASK_IMAGE = maskImageView.getImage(); + AppConstants.DEFAULT_OVERLAY_IMAGE = overlayImageView.getImage(); + AppConstants.DEFAULT_PORTRAIT_IMAGE = portraitImageView.getImage(); + AppConstants.DEFAULT_PORTRAIT_IMAGE_X = portraitImageView.getTranslateX(); + AppConstants.DEFAULT_PORTRAIT_IMAGE_Y = portraitImageView.getTranslateY(); + AppConstants.DEFAULT_PORTRAIT_IMAGE_SCALE = portraitImageView.getScaleY(); + AppConstants.DEFAULT_PORTRAIT_IMAGE_ROTATE = portraitImageView.getRotate(); + AppConstants.DEFAULT_SAVE_PORTRAIT_ON_DRAG = getSavePortraitOnDragCheckbox(); + AppConstants.DEFAULT_USE_BACKGROUND_ON_DRAG = getUseBackgroundOnDragCheckbox(); + AppConstants.DEFAULT_PORTRAIT_NAME_TEXT_FIELD = getPortraitNameTextField(); + AppConstants.DEFAULT_USE_TOKEN_NAME = getUseTokenNameCheckbox(); + AppConstants.DEFAULT_PORTRAIT_NAME_SUFFIX_TEXT_FIELD = getPortraitNameSuffixTextField(); + + executorService = + Executors.newCachedThreadPool( + runable -> { + loadOverlaysThread = Executors.defaultThreadFactory().newThread(runable); + loadOverlaysThread.setDaemon(true); + return loadOverlaysThread; + }); + + overlayTreeView.setShowRoot(false); + overlayTreeView + .getSelectionModel() + .selectedItemProperty() + .addListener( + (observable, oldValue, newValue) -> updateCompositImageView((TreeItem) newValue)); + + addPseudoClassToLeafs(overlayTreeView); + + // Bind color picker to compositeTokenPane background fill + backgroundColorPicker.setValue(Color.TRANSPARENT); + ObjectProperty background = compositeTokenPane.backgroundProperty(); + background.bind( + Bindings.createObjectBinding( + () -> { + BackgroundFill fill = + new BackgroundFill( + backgroundColorPicker.getValue(), CornerRadii.EMPTY, Insets.EMPTY); + return new Background(fill); + }, + backgroundColorPicker.valueProperty())); + + // Bind transparency slider to portraitImageView opacity + portraitTransparencySlider + .valueProperty() + .addListener( + new ChangeListener() { + public void changed( + ObservableValue ov, Number old_val, Number new_val) { + portraitImageView.setOpacity(new_val.doubleValue()); + updateTokenPreviewImageView(); + } + }); + + // Set filters and bindings for file name inputs + UnaryOperator filter = + change -> { + String text = change.getText(); + + if (text.matches(AppConstants.VALID_FILE_NAME_PATTERN)) { + return change; + } else { + change.setText(FileSaveUtil.cleanFileName(text)); + ; + return change; + } + // + // return null; + }; + + fileNameTextField.setTextFormatter(new TextFormatter<>(filter)); + portraitNameTextField.setTextFormatter(new TextFormatter<>(filter)); + portraitNameTextField + .textProperty() + .bind(fileNameTextField.textProperty().concat(portraitNameSuffixTextField.textProperty())); + + // Bind portrait name to token name if useTokenNameCheckbox is checked + portraitNameTextField.disableProperty().bind(useTokenNameCheckbox.selectedProperty()); + portraitNameSuffixLabel.disableProperty().bind(useTokenNameCheckbox.selectedProperty().not()); + portraitNameSuffixTextField + .disableProperty() + .bind(useTokenNameCheckbox.selectedProperty().not()); + + // Bind the use background on drag to save portrait on drag checkbox + useBackgroundOnDragCheckbox + .disableProperty() + .bind(savePortraitOnDragCheckbox.selectedProperty().not()); + + useTokenNameCheckbox + .selectedProperty() + .addListener( + (obs, wasSelected, isNowSelected) -> { + if (isNowSelected) { + portraitNameTextField + .textProperty() + .bind( + fileNameTextField + .textProperty() + .concat(portraitNameSuffixTextField.textProperty())); + } else { + portraitNameTextField.textProperty().unbind(); + } + }); + + /* Effects */ + GaussianBlur gaussianBlur = new GaussianBlur(0); + Glow glow = new Glow(0); + gaussianBlur.setInput(glow); + + // Bind blur slider to portraitImageView opacity + portraitBlurSlider + .valueProperty() + .addListener( + new ChangeListener() { + public void changed( + ObservableValue ov, Number old_val, Number new_val) { + gaussianBlur.setRadius(new_val.doubleValue()); + portraitImageView.setEffect(gaussianBlur); + updateTokenPreviewImageView(); + } + }); + + // Bind glow slider to portraitImageView opacity + portraitGlowSlider + .valueProperty() + .addListener( + new ChangeListener() { + public void changed( + ObservableValue ov, Number old_val, Number new_val) { + glow.setLevel(new_val.doubleValue()); + portraitImageView.setEffect(gaussianBlur); + updateTokenPreviewImageView(); + } + }); + + // Bind transparency slider to overlayImageView opacity + overlayTransparencySlider + .valueProperty() + .addListener( + new ChangeListener() { + public void changed( + ObservableValue ov, Number old_val, Number new_val) { + overlayImageView.setOpacity(new_val.doubleValue()); + updateTokenPreviewImageView(); + } + }); + + // Bind width/height spinners to overlay width/height + overlayWidthSpinner + .getValueFactory() + .valueProperty() + .bindBidirectional(overlayHeightSpinner.getValueFactory().valueProperty()); + overlayWidthSpinner + .valueProperty() + .addListener( + (observable, oldValue, newValue) -> + overlayWidthSpinner_onTextChanged(oldValue, newValue)); + overlayWidthSpinner + .getValueFactory() + .setConverter( + new StringConverter() { + @Override + public String toString(Integer object) { + return object.toString(); + } + + @Override + public Integer fromString(String string) { + int value = 256; + + try { + value = Integer.parseInt(string); + } catch (NumberFormatException e) { + log.debug("Invalid overlay size entered.", e); + } + + return value; + } + }); + overlayHeightSpinner + .valueProperty() + .addListener( + (observable, oldValue, newValue) -> + overlayHeightSpinner_onTextChanged(oldValue, newValue)); + overlayHeightSpinner + .getValueFactory() + .setConverter( + new StringConverter() { + @Override + public String toString(Integer object) { + return object.toString(); + } + + @Override + public Integer fromString(String string) { + int value = 256; + + try { + value = Integer.parseInt(string); + } catch (NumberFormatException e) { + log.info("NOPE"); + } + + return value; + } + }); + + // Bind the background/portrait pane widths to keep things centered. + // Otherwise StackPane sets width/height to largest value from the ImageView nodes within it + imagesStackPane.minWidthProperty().bind(compositeTokenPane.widthProperty()); + imagesStackPane.minHeightProperty().bind(compositeTokenPane.heightProperty()); + } + + @FXML + void removeBackgroundColorButton_OnAction(ActionEvent event) { + backgroundColorPicker.setValue(Color.TRANSPARENT); + updateTokenPreviewImageView(); + } + + @FXML + void changeBackgroundImageButton_OnAction(ActionEvent event) { + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle(I18N.getString("TokenTool.openBackgroundImage.filechooser.title")); + fileChooser.getExtensionFilters().addAll(AppConstants.IMAGE_EXTENSION_FILTER); + + File lastBackgroundImageFile = + new File(AppPreferences.getPreference(AppPreferences.LAST_BACKGROUND_IMAGE_FILE, "")); + + if (lastBackgroundImageFile.exists()) fileChooser.setInitialDirectory(lastBackgroundImageFile); + else if (lastBackgroundImageFile.getParentFile() != null) + fileChooser.setInitialDirectory(lastBackgroundImageFile.getParentFile()); + + File selectedImageFile = + fileChooser.showOpenDialog((Stage) compositeGroup.getScene().getWindow()); + + if (selectedImageFile != null) { + try { + updateBackground(new Image(selectedImageFile.toURI().toString())); + AppPreferences.setPreference( + AppPreferences.LAST_BACKGROUND_IMAGE_FILE, + selectedImageFile.getParentFile().getCanonicalPath()); + } catch (IOException e) { + log.error("Error loading Image " + selectedImageFile.getAbsolutePath()); + } + } + + updateTokenPreviewImageView(); + backgroundMenuItem.fire(); // Set current layer to background + } + + @FXML + void removeBackgroundImageButton_OnAction(ActionEvent event) { + backgroundImageView.setImage(null); + updateTokenPreviewImageView(); + portraitMenuItem.fire(); // Set current layer to portrait + } + + @FXML + void changePortraitImageButton_OnAction(ActionEvent event) { + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle(I18N.getString("TokenTool.openPortraitImage.filechooser.title")); + fileChooser.getExtensionFilters().addAll(AppConstants.IMAGE_EXTENSION_FILTER); + + File lastPortraitImageFile = + new File(AppPreferences.getPreference(AppPreferences.LAST_PORTRAIT_IMAGE_FILE, "")); + + if (lastPortraitImageFile.exists()) fileChooser.setInitialDirectory(lastPortraitImageFile); + else if (lastPortraitImageFile.getParentFile() != null) + fileChooser.setInitialDirectory(lastPortraitImageFile.getParentFile()); + + File selectedImageFile = + fileChooser.showOpenDialog((Stage) compositeGroup.getScene().getWindow()); + + if (selectedImageFile != null) { + try { + updatePortrait(new Image(selectedImageFile.toURI().toString())); + AppPreferences.setPreference( + AppPreferences.LAST_PORTRAIT_IMAGE_FILE, + selectedImageFile.getParentFile().getCanonicalPath()); + } catch (IOException e) { + log.error("Error loading Image " + selectedImageFile.getAbsolutePath()); + } + } + + updateTokenPreviewImageView(); + portraitMenuItem.fire(); // Set current layer to portrait + } + + @FXML + void removePortraitImageButton_OnAction(ActionEvent event) { + portraitImageView.setImage(null); + updateTokenPreviewImageView(); + portraitMenuItem.fire(); // Set current layer to portrait + } + + @FXML + void fileOpenPDF_Menu_OnAction(ActionEvent event) { + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle(I18N.getString("TokenTool.openPDF.filechooser.title")); + fileChooser.getExtensionFilters().add(ImageUtil.SUPPORTED_PDF_EXTENSION_FILTER); + + File lastPdfFile = new File(AppPreferences.getPreference(AppPreferences.LAST_PDF_FILE, "")); + + if (lastPdfFile.exists()) fileChooser.setInitialDirectory(lastPdfFile); + else if (lastPdfFile.getParentFile() != null) + fileChooser.setInitialDirectory(lastPdfFile.getParentFile()); + + File selectedPDF = fileChooser.showOpenDialog((Stage) compositeGroup.getScene().getWindow()); + + if (selectedPDF != null) { + try { + new PdfViewer(selectedPDF, this); + AppPreferences.setPreference( + AppPreferences.LAST_PDF_FILE, selectedPDF.getParentFile().getCanonicalPath()); + } catch (IOException e) { + log.error("Error loading PDF " + selectedPDF.getAbsolutePath()); + } + } + } + + @FXML + void fileManageOverlaysMenu_OnAction(ActionEvent event) { + new ManageOverlays(this); + } + + @FXML + void fileSaveAsMenu_OnAction(ActionEvent event) { + saveToken(); + } + + @FXML + void fileExitMenu_OnAction(ActionEvent event) { + exitApplication(); + } + + @FXML + void editCaptureScreenMenu_OnAction(ActionEvent event) { + new RegionSelector(this); + } + + @FXML + void editCopyImageMenu_OnAction(ActionEvent event) { + Clipboard clipboard = Clipboard.getSystemClipboard(); + ClipboardContent content = new ClipboardContent(); + + // for paste as file, e.g. in Windows Explorer + try { + File tempTokenFile = + fileSaveUtil.getTempFileName( + false, + useFileNumberingCheckbox.isSelected(), + fileNameTextField.getText(), + fileNameSuffixTextField); + + writeTokenImage(tempTokenFile); + content.putFiles(java.util.Collections.singletonList(tempTokenFile)); + tempTokenFile.deleteOnExit(); + } catch (Exception e) { + log.error(e); + } + + // for paste as image, e.g. in GIMP + content.putImage(tokenImageView.getImage()); + + // Finally, put contents on clip board + clipboard.setContent(content); + } + + @FXML + void editPasteImageMenu_OnAction(ActionEvent event) { + Clipboard clipboard = Clipboard.getSystemClipboard(); + Image originalImage = portraitImageView.getImage(); + + // Strangely, we get an error if we try to paste an image we put in the clipboard ourselves but + // File works ok? + // -Dprism.order=sw also fixes it but not sure why... + // So lets just check for File first... + if (clipboard.hasFiles()) { + clipboard + .getFiles() + .forEach( + file -> { + try { + Image cbImage = new Image(file.toURI().toURL().toExternalForm()); + + if (cbImage != null) + updateImage( + cbImage, FilenameUtils.getBaseName(file.toURI().toURL().toExternalForm())); + } catch (Exception e) { + log.error("Could not load image " + file); + e.printStackTrace(); + } + }); + } else if (clipboard.hasImage()) { + try { + Image cbImage = clipboard.getImage(); + if (cbImage != null) updateImage(cbImage); + } catch (IllegalArgumentException e) { + log.info(e); + updatePortrait(originalImage); + } + } else if (clipboard.hasUrl()) { + try { + Image cbImage = new Image(clipboard.getUrl()); + if (cbImage != null) updateImage(cbImage, FileSaveUtil.searchURL(clipboard.getUrl())); + } catch (IllegalArgumentException e) { + log.info(e); + } + } else if (clipboard.hasString()) { + try { + Image cbImage = new Image(clipboard.getString()); + if (cbImage != null) updateImage(cbImage, FileSaveUtil.searchURL(clipboard.getString())); + } catch (IllegalArgumentException e) { + log.info(e); + } + } + } + + @FXML + void helpAboutMenu_OnAction(ActionEvent event) { + new Credits(this); + } + + @FXML + void helpResetMenu_OnAction(ActionEvent event) { + String confirmationText = I18N.getString("TokenTool.dialog.reset.confirmation.text"); + + Alert alert = new Alert(AlertType.CONFIRMATION); + alert.setHeaderText(I18N.getString("TokenTool.dialog.confirmation.header")); + alert.setTitle(I18N.getString("TokenTool.dialog.reset.confirmation.title")); + alert.setContentText(confirmationText); + + Optional result = alert.showAndWait(); + + if (!((result.isPresent()) && (result.get() == ButtonType.OK))) return; + + // OK, reset everything! + AppPreferences.removeAllPreferences(); + AppPreferences.restorePreferences(this); + + maskImageView.setImage(AppConstants.DEFAULT_MASK_IMAGE); + overlayImageView.setImage(AppConstants.DEFAULT_OVERLAY_IMAGE); + + portraitImageView.setImage(AppConstants.DEFAULT_PORTRAIT_IMAGE); + portraitImageView.setTranslateX(AppConstants.DEFAULT_PORTRAIT_IMAGE_X); + portraitImageView.setTranslateY(AppConstants.DEFAULT_PORTRAIT_IMAGE_Y); + portraitImageView.setScaleX(AppConstants.DEFAULT_PORTRAIT_IMAGE_SCALE); + portraitImageView.setScaleY(AppConstants.DEFAULT_PORTRAIT_IMAGE_SCALE); + portraitImageView.setRotate(AppConstants.DEFAULT_PORTRAIT_IMAGE_ROTATE); + + portraitMenuItem.fire(); + + recentOverlayTreeItems.clear(); + lastSelectedItem = null; + updateOverlayTreeViewRecentFolder(true); + + Platform.runLater(() -> updateTokenPreviewImageView()); + TokenTool.getInstance().getStage().setMaximized(false); + } + + @FXML + void useFileNumberingCheckbox_OnAction(ActionEvent event) { + fileNameSuffixLabel.setDisable(!useFileNumberingCheckbox.isSelected()); + fileNameSuffixTextField.setDisable(!useFileNumberingCheckbox.isSelected()); + } + + @FXML + void compositeTokenPane_KeyPressed(KeyEvent key) { + if (key.getCode().isArrowKey() + || key.getCode().isNavigationKey() + || key.getCode().isKeypadKey()) { + + double x = getCurrentLayer().getTranslateX(); + double y = getCurrentLayer().getTranslateY(); + + switch (key.getCode()) { + case LEFT: + case KP_LEFT: + case NUMPAD4: + x--; + break; + case RIGHT: + case KP_RIGHT: + case NUMPAD6: + x++; + break; + case UP: + case KP_UP: + case NUMPAD8: + y--; + break; + case DOWN: + case KP_DOWN: + case NUMPAD2: + y++; + break; + case HOME: + case NUMPAD7: + x--; + y--; + break; + case END: + case NUMPAD1: + x--; + y++; + break; + case PAGE_UP: + case NUMPAD9: + x++; + y--; + break; + case PAGE_DOWN: + case NUMPAD3: + x++; + y++; + break; + default: + break; + } + + getCurrentLayer().setTranslateX(x); + getCurrentLayer().setTranslateY(y); + currentImageOffset.setLocation(x, y); + + updateTokenPreviewImageView(); + + key.consume(); + } + } + + private ImageView getCurrentLayer() { + if (backgroundMenuItem.isSelected()) return backgroundImageView; + else return portraitImageView; + } + + @FXML + void compositeTokenPane_MouseDragged(MouseEvent event) { + getCurrentLayer().setTranslateX(event.getX() - dragStart.x + currentImageOffset.x); + getCurrentLayer().setTranslateY(event.getY() - dragStart.y + currentImageOffset.y); + + updateTokenPreviewImageView(); + } + + @FXML + void compositeTokenPane_MousePressed(MouseEvent event) { + dragStart.setLocation(event.getX(), event.getY()); + currentImageOffset.setLocation( + getCurrentLayer().getTranslateX(), getCurrentLayer().getTranslateY()); + portraitImageView.setCursor(Cursor.MOVE); + + // Get focus for arrow keys... + compositeTokenPane.requestFocus(); + } + + @FXML + void compositeTokenPane_MouseReleased(MouseEvent event) { + portraitImageView.setCursor(Cursor.HAND); + updateTokenPreviewImageView(); + } + + @FXML + void compositeTokenPane_MouseEntered(MouseEvent event) { + portraitImageView.setCursor(Cursor.HAND); // TODO: Not working... + } + + @FXML + void compositeTokenPane_MouseDragExited(MouseDragEvent event) {} + + @FXML + void compositeTokenPane_MouseExited(MouseEvent event) {} + + @FXML + void compositeTokenPane_MouseMoved(MouseEvent event) {} + + @FXML + void compositeTokenPane_OnScroll(ScrollEvent event) { + // if event is touch enabled, skip this as it will be handled by onZoom & onRotate handlers + if (event.isDirect()) return; + + if (event.isShiftDown()) { + // Note: OK, this is stupid but on Windows shift + mousewheel returns X delta but on Ubuntu it + // returns Y delta... + double delta = event.getDeltaY(); + if (delta == 0) delta = event.getDeltaX(); + + Double r = getCurrentLayer().getRotate() + delta / 20; + + if (r < -360d || r > 360d) r = 0d; + + getCurrentLayer().setRotate(r); + } else { + Double scale = getCurrentLayer().getScaleY() * Math.pow(1.001, event.getDeltaY()); + + getCurrentLayer().setScaleX(scale); + getCurrentLayer().setScaleY(scale); + } + + event.consume(); + updateTokenPreviewImageView(); + } + + @FXML + void compositeTokenPane_OnZoom(ZoomEvent event) { + Double scale = getCurrentLayer().getScaleY() * event.getZoomFactor(); + + getCurrentLayer().setScaleX(scale); + getCurrentLayer().setScaleY(scale); + } + + @FXML + void compositeTokenPane_OnRotate(RotateEvent event) { + log.info("isDirect(): " + event.isDirect()); + log.info("getTotalAngle" + event.getTotalAngle()); + + double r = getCurrentLayer().getRotate() + (event.getAngle() * 0.75); + if (r < -360d || r > 360d) r = 0d; + + getCurrentLayer().setRotate(r); + event.consume(); + } + + @FXML + void compositeTokenPane_DragDropped(DragEvent event) { + Dragboard db = event.getDragboard(); + + // Strangely, we get an error if we try to paste an image we put in the clipboard ourselves but + // File works ok? + // -Dprism.order=sw also fixes it but not sure why... + // So lets just check for File first... + if (db.hasFiles()) { + db.getFiles() + .forEach( + file -> { + try { + String fileName = FilenameUtils.getName(file.toURI().toURL().toExternalForm()); + + if (FilenameUtils.isExtension(fileName.toLowerCase(), "pdf")) { + Platform.runLater(() -> new PdfViewer(file, this)); + } else { + updateImage( + new Image(file.toURI().toURL().toExternalForm()), + FilenameUtils.getBaseName(fileName)); + } + } catch (Exception e) { + log.error("Could not load image " + file, e); + } + }); + event.setDropCompleted(true); + } else if (db.hasImage()) { + updateImage(db.getImage()); + event.setDropCompleted(true); + } else if (db.hasUrl()) { + updateImage(new Image(db.getUrl()), FileSaveUtil.searchURL(db.getUrl())); + event.setDropCompleted(true); + } + } + + @FXML + void compositeTokenPane_DragDone(DragEvent event) { + updateTokenPreviewImageView(); + } + + @FXML + void compositeTokenPane_DragOver(DragEvent event) { + if (event.getDragboard().hasImage() + || event.getDragboard().hasFiles() + || event.getDragboard().hasUrl()) { + event.acceptTransferModes(TransferMode.COPY); + + // If object is dragged within the outside 15% of the pane, then drop to the background layer, + // otherwise drop to the portrait layer + int borderWidth = + (int) (Math.min(dndHighlights.getWidth(), dndHighlights.getHeight()) * 0.15); + if (event.getX() < borderWidth + || event.getY() < borderWidth + || event.getX() > compositeTokenPane.getWidth() - borderWidth + || event.getY() > compositeTokenPane.getHeight() - borderWidth) { + + StackPane.setMargin(dndHighlights, new Insets(0)); + dndHighlights.setStyle( + "-fx-border-color: #ffff0055; -fx-border-width: " + borderWidth + "px"); + backgroundMenuItem.fire(); + } else { + StackPane.setMargin(dndHighlights, new Insets(borderWidth)); + dndHighlights.setStyle("-fx-background-color: #00ff0055"); + portraitMenuItem.fire(); + } + } else { + // Set Pane color to an alpha red? + event.acceptTransferModes(TransferMode.ANY); + dndHighlights.setStyle("-fx-background-color: #ff000055"); + } + } + + @FXML + void compositeTokenPane_DragExited(DragEvent event) { + dndHighlights.setStyle(""); + } + + @FXML + void tokenImageView_OnDragDetected(MouseEvent event) { + Dragboard db = tokenImageView.startDragAndDrop(TransferMode.COPY); + ClipboardContent content = new ClipboardContent(); + + boolean saveAsToken = false; + + try { + File tempTokenFile; + File tempPortraitFile = null; + ArrayList tempFiles = new ArrayList(); + + // Here we don't advance the fileNameSuffix so portrait name has same number, we'll advance it + // after the second call + tempTokenFile = + fileSaveUtil.getTempFileName( + saveAsToken, + useFileNumberingCheckbox.isSelected(), + getFileNameTextField(), + fileNameSuffixTextField, + false); + writeTokenImage(tempTokenFile); + tempFiles.add(tempTokenFile); + + tempPortraitFile = + fileSaveUtil.getTempFileName( + saveAsToken, + useFileNumberingCheckbox.isSelected(), + getPortraitNameTextField(), + fileNameSuffixTextField, + true); + if (savePortraitOnDragCheckbox.isSelected()) { + tempPortraitFile = writePortraitImage(tempPortraitFile); + if (tempPortraitFile != null) tempFiles.add(tempPortraitFile); + } + + content.putFiles(tempFiles); + + tempTokenFile.deleteOnExit(); + tempPortraitFile.deleteOnExit(); + } catch (Exception e) { + log.error(e); + } finally { + if (event.isPrimaryButtonDown()) content.putImage(tokenImageView.getImage()); + else content.putImage(getPortraitImage()); + + db.setContent(content); + event.consume(); + } + } + + @FXML + void tokenImageView_OnDragDone(DragEvent event) { + if (event.getAcceptedTransferMode() != null) updateOverlayTreeViewRecentFolder(true); + } + + @FXML + void overlayUseAsBaseCheckbox_OnAction(ActionEvent event) { + if (overlayUseAsBaseCheckbox.isSelected()) compositeGroup.toBack(); + else portraitScrollPane.toBack(); + + // Always keep background image in back... + // backgroundImagePane.toBack(); + + updateTokenPreviewImageView(); + } + + @FXML + void backgroundColorPicker_OnAction(ActionEvent event) { + updateTokenPreviewImageView(); + } + + @FXML + void overlayAspectToggleButton_OnAction(ActionEvent event) { + if (overlayAspectToggleButton.isSelected()) { + overlayImageView.setPreserveRatio(true); + maskImageView.setPreserveRatio(true); + overlayWidthSpinner + .getValueFactory() + .valueProperty() + .bindBidirectional(overlayHeightSpinner.getValueFactory().valueProperty()); + } else { + overlayImageView.setPreserveRatio(false); + maskImageView.setPreserveRatio(false); + overlayWidthSpinner + .getValueFactory() + .valueProperty() + .unbindBidirectional(overlayHeightSpinner.getValueFactory().valueProperty()); + } + + updateTokenPreviewImageView(); + } + + @FXML + void backgroundMenuItem_OnAction(ActionEvent event) { + String menuText = ((RadioMenuItem) event.getSource()).getText(); + layerMenuButton.setText(menuText + I18N.getString("controls.layers.menu.layer.text")); + backgroundMenuItem.setSelected(true); + } + + @FXML + void portraitMenuItem_OnAction(ActionEvent event) { + String menuText = ((RadioMenuItem) event.getSource()).getText(); + layerMenuButton.setText(menuText + I18N.getString("controls.layers.menu.layer.text")); + portraitMenuItem.setSelected(true); + } + + void overlayWidthSpinner_onTextChanged(int oldValue, int newValue) { + if (newValue < overlaySpinnerSteps.first()) newValue = overlaySpinnerSteps.first(); + + if (newValue > overlaySpinnerSteps.last()) newValue = overlaySpinnerSteps.last(); + + if (getOverlayAspect()) { + if (newValue > oldValue) + overlayWidthSpinner.getValueFactory().setValue(overlaySpinnerSteps.ceiling(newValue)); + else overlayWidthSpinner.getValueFactory().setValue(overlaySpinnerSteps.floor(newValue)); + } + + overlayImageView.setFitWidth(overlayWidthSpinner.getValue()); + maskImageView.setFitWidth(overlayWidthSpinner.getValue()); + + updateTokenPreviewImageView(); + } + + void overlayHeightSpinner_onTextChanged(int oldValue, int newValue) { + if (newValue < overlaySpinnerSteps.first()) newValue = overlaySpinnerSteps.first(); + + if (newValue > overlaySpinnerSteps.last()) newValue = overlaySpinnerSteps.last(); + + if (getOverlayAspect()) { + if (newValue > oldValue) + overlayHeightSpinner.getValueFactory().setValue(overlaySpinnerSteps.ceiling(newValue)); + else overlayHeightSpinner.getValueFactory().setValue(overlaySpinnerSteps.floor(newValue)); + } + + overlayImageView.setFitHeight(overlayHeightSpinner.getValue()); + maskImageView.setFitHeight(overlayHeightSpinner.getValue()); + + updateTokenPreviewImageView(); + } + + public Map> getRecentOverlayTreeItems() { + return recentOverlayTreeItems; + } + + public void updateRecentOverlayTreeItems(Path filePath) { + try { + TreeItem recentOverlay = + new TreeItem(filePath, ImageUtil.getOverlayThumb(new ImageView(), filePath)); + + // Remove first so if it is on the list it forces to top of list + recentOverlayTreeItems.remove(filePath); + recentOverlayTreeItems.put(filePath, recentOverlay); + } catch (IOException e) { + log.error("Error loading recent overlay preference for " + filePath.toString()); + } + } + + public void expandOverlayOptionsPane(boolean expand) { + overlayOptionsPane.setExpanded(expand); + } + + public void expandBackgroundOptionsPane(boolean expand) { + backgroundOptionsPane.setExpanded(expand); + } + + public void updateOverlayTreeview(TreeItem overlayTreeItems) { + overlayTreeView.setRoot(overlayTreeItems); + } + + public void updateTokenPreviewImageView() { + tokenImageView.setImage( + ImageUtil.composePreview( + compositeTokenPane, + backgroundImageView, + backgroundColorPicker.getValue(), + portraitImageView, + maskImageView, + overlayImageView, + overlayUseAsBaseCheckbox.isSelected(), + clipPortraitCheckbox.isSelected())); + tokenImageView.setPreserveRatio(true); + } + + private void saveToken() { + FileChooser fileChooser = new FileChooser(); + + try { + File tokenFile = + fileSaveUtil.getFileName( + false, + useFileNumberingCheckbox.isSelected(), + fileNameTextField.getText(), + fileNameSuffixTextField, + true); + fileChooser.setInitialFileName(tokenFile.getName()); + if (tokenFile.getParentFile() != null) + if (tokenFile.getParentFile().isDirectory()) + fileChooser.setInitialDirectory(tokenFile.getParentFile()); + } catch (IOException e1) { + log.error("Error writing token!", e1); + } + + fileChooser.getExtensionFilters().addAll(AppConstants.IMAGE_EXTENSION_FILTER); + fileChooser.setTitle(I18N.getString("TokenTool.save.filechooser.title")); + fileChooser.setSelectedExtensionFilter(AppConstants.IMAGE_EXTENSION_FILTER); + + File tokenSaved = fileChooser.showSaveDialog(saveOptionsPane.getScene().getWindow()); + + if (tokenSaved == null) return; + + writeTokenImage(tokenSaved); + + updateFileNameTextField(FilenameUtils.getBaseName(tokenSaved.getName())); + FileSaveUtil.setLastFile(tokenSaved); + updateOverlayTreeViewRecentFolder(true); + } + + private boolean writeTokenImage(File tokenFile) { + try { + Image tokenImage; + if (clipPortraitCheckbox.isSelected()) + tokenImage = + ImageUtil.resizeCanvas( + tokenImageView.getImage(), getOverlayWidth(), getOverlayHeight()); + else tokenImage = tokenImageView.getImage(); + + return ImageIO.write(SwingFXUtils.fromFXImage(tokenImage, null), "png", tokenFile); + } catch (IOException e) { + log.error("Unable to write token to file: " + tokenFile.getAbsolutePath(), e); + } catch (IndexOutOfBoundsException e) { + log.error( + "Image width/height out of bounds: " + getOverlayWidth() + " x " + getOverlayHeight(), e); + } + + return false; + } + + private File writePortraitImage(File tokenFile) { + try { + String imageType = "png"; + Image tokenImage; + tokenImage = getPortraitImage(); + BufferedImage imageRGB = SwingFXUtils.fromFXImage(tokenImage, null); + + if (useBackgroundOnDragCheckbox.isSelected()) { + if (getBackgroundColor() != Color.TRANSPARENT || getBackgroundImage() != null) { + tokenImage = + ImageUtil.autoCropImage(tokenImage, getBackgroundColor(), getBackgroundImage()); + imageType = "jpg"; + + String newFileName = + FilenameUtils.removeExtension(tokenFile.getAbsolutePath()) + "." + imageType; + tokenFile = new File(newFileName); + + // Remove alpha-channel from buffered image + BufferedImage image = SwingFXUtils.fromFXImage(tokenImage, null); + imageRGB = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.OPAQUE); + Graphics2D graphics = imageRGB.createGraphics(); + graphics.drawImage(image, 0, 0, null); + graphics.dispose(); + + ImageIO.write(imageRGB, imageType, tokenFile); + } + } + + if (ImageIO.write(imageRGB, imageType, tokenFile)) return tokenFile; + + } catch (IOException e) { + log.error("Unable to write token to file: " + tokenFile.getAbsolutePath(), e); + } catch (IndexOutOfBoundsException e) { + log.error( + "Image width/height out of bounds: " + getOverlayWidth() + " x " + getOverlayHeight(), e); + } + + return null; + } + + public void updateOverlayTreeViewRecentFolder(boolean selectMostRecent) { + if (lastSelectedItem != null) updateRecentOverlayTreeItems(lastSelectedItem.getValue()); + + log.debug("recentOverlayTreeItems size : " + recentOverlayTreeItems.size()); + + // Update Recent Overlay List + if (!recentOverlayTreeItems.isEmpty()) { + // Remember current selection (adding/removing tree items messes with the selection model) + // int selectedItem = overlayTreeView.getSelectionModel().getSelectedIndex(); + overlayTreeView.getSelectionModel().clearSelection(); + + // Clear current folder + recentFolder.getChildren().clear(); + + // Add recent list to recentFolder in reverse order so most recent is at the top + ListIterator>> iter = + new ArrayList<>(recentOverlayTreeItems.entrySet()) + .listIterator(recentOverlayTreeItems.size()); + while (iter.hasPrevious()) recentFolder.getChildren().add(iter.previous().getValue()); + + if (overlayTreeView.getRoot().getChildren().indexOf(recentFolder) == -1) { + overlayTreeView.getRoot().getChildren().add(recentFolder); + } else { + overlayTreeView.getRoot().getChildren().remove(recentFolder); + overlayTreeView.getRoot().getChildren().add(recentFolder); + } + + // Auto expand recent folder... + recentFolder.setExpanded(true); + + addPseudoClassToLeafs(overlayTreeView); + + // Set the selected index back to what it was unless... + if (selectMostRecent) { + overlayTreeView.getSelectionModel().select(recentFolder.getChildren().get(0)); + } else { + // overlayTreeView.getSelectionModel().clearAndSelect(selectedItem); + } + } else { + overlayTreeView.getSelectionModel().clearSelection(); + recentFolder.getChildren().clear(); + overlayTreeView.getRoot().getChildren().remove(recentFolder); + } + } + + private void addPseudoClassToLeafs(TreeView tree) { + PseudoClass leaf = PseudoClass.getPseudoClass("leaf"); + + tree.setCellFactory( + tv -> { + TreeCell cell = new TreeCell<>(); + cell.itemProperty() + .addListener( + (obs, oldValue, newValue) -> { + if (newValue == null) { + cell.setText(""); + cell.setGraphic(null); + } else { + cell.setText(newValue.toFile().getName()); + cell.setGraphic(cell.getTreeItem().getGraphic()); + } + }); + cell.treeItemProperty() + .addListener( + (obs, oldTreeItem, newTreeItem) -> + cell.pseudoClassStateChanged( + leaf, newTreeItem != null && newTreeItem.isLeaf())); + return cell; + }); + } + + public void updateImage(Image image, String imageName, boolean setBackground) { + if (setBackground) backgroundMenuItem.fire(); + else portraitMenuItem.fire(); + + updateImage(image, imageName); + } + + public void updateImage(Image image) { + updateImage(image, null); + } + + public void updateImage(Image image, String imageName) { + if (backgroundMenuItem.isSelected()) { + updateBackground(image); + } else { + updatePortrait(image); + updateFileNameTextField(imageName); + } + + dndHighlights.setStyle(""); + } + + private void updateBackground(Image newBackgroundImage) { + backgroundImageView.setImage(newBackgroundImage); + + backgroundImageView.setTranslateX(0); + backgroundImageView.setTranslateY(0); + backgroundImageView.setFitWidth(newBackgroundImage.getWidth()); + backgroundImageView.setFitHeight(newBackgroundImage.getHeight()); + backgroundImageView.setScaleX(1); + backgroundImageView.setScaleY(1); + backgroundImageView.setRotate(0d); + + updateTokenPreviewImageView(); + } + + private void updatePortrait(Image newPortraitImage) { + portraitImageView.setImage(newPortraitImage); + + portraitImageView.setTranslateX(0); + portraitImageView.setTranslateY(0); + portraitImageView.setScaleX(1); + portraitImageView.setScaleY(1); + portraitImageView.setRotate(0d); + + updateTokenPreviewImageView(); + } + + private void updateCompositImageView(TreeItem treeNode) { + // Node removed... + if (treeNode == null) return; + + // I'm not a leaf on the wind! (Sub directory node) + if (treeNode.getChildren().size() > 0) return; + + try { + Path filePath = treeNode.getValue(); + lastSelectedItem = treeNode; + + // Set the Image Views + maskImageView = ImageUtil.getMaskImage(maskImageView, filePath); + overlayImageView = ImageUtil.getOverlayImage(overlayImageView, filePath); + + // Set the text label + overlayNameLabel.setText(FilenameUtils.getBaseName(filePath.toFile().getName())); + overlayInfoLabel.setText( + (int) overlayImageView.getImage().getWidth() + + " x " + + (int) overlayImageView.getImage().getHeight()); + + updateTokenPreviewImageView(); + } catch (IOException e) { + // Not a valid URL, most likely this is just because it's a directory node. + e.printStackTrace(); + } + } + + public Color getBackgroundColor() { + return backgroundColorPicker.getValue(); + } + + public void setBackgroundColor(Color newColor) { + backgroundColorPicker.setValue(newColor); + } + + public void refreshCache() { + overlayTreeProgressBar.setStyle(""); + overlayTreeProgressBar.setVisible(true); + overlayTreeProgressBar.setOpacity(1.0); + overlayNameLabel.setOpacity(0.0); + overlayInfoLabel.setOpacity(0.0); + progressBarLabel.setVisible(true); + updateOverlayTreeview(null); + + try { + loadCount.set(0); + overlayCount = + (int) Files.walk(AppConstants.OVERLAY_DIR.toPath()).filter(Files::isRegularFile).count(); + log.info("overlayCount: " + overlayCount); + + treeItems = cacheOverlays(AppConstants.OVERLAY_DIR, null, AppConstants.THUMB_SIZE); + } catch (IOException e) { + log.error("Error reloading overlay cache!", e); + } + } + + private void treeViewFinish() { + log.debug("***treeViewFinish called"); + // Sort the nodes off of root + treeItems = sortTreeNodes(treeItems); + + updateOverlayTreeview(treeItems); + addPseudoClassToLeafs(overlayTreeView); + updateOverlayTreeViewRecentFolder(false); + + overlayTreeProgressBar.setStyle("-fx-accent: forestgreen;"); + progressBarLabel.setVisible(false); + + FadeTransition progressBarFadeOut = new FadeTransition(Duration.millis(2000)); + progressBarFadeOut.setNode(overlayTreeProgressBar); + progressBarFadeOut.setFromValue(1.0); + progressBarFadeOut.setToValue(0.0); + progressBarFadeOut.setCycleCount(1); + progressBarFadeOut.setAutoReverse(false); + progressBarFadeOut.playFromStart(); + + FadeTransition nameFadeIn = new FadeTransition(Duration.millis(4000)); + nameFadeIn.setNode(overlayNameLabel); + nameFadeIn.setFromValue(0.0); + nameFadeIn.setToValue(1.0); + nameFadeIn.setCycleCount(1); + nameFadeIn.setAutoReverse(false); + nameFadeIn.playFromStart(); + + FadeTransition infoFadeIn = new FadeTransition(Duration.millis(4000)); + infoFadeIn.setNode(overlayInfoLabel); + infoFadeIn.setFromValue(0.0); + infoFadeIn.setToValue(1.0); + infoFadeIn.setCycleCount(1); + infoFadeIn.setAutoReverse(false); + infoFadeIn.playFromStart(); + } + + private TreeItem cacheOverlays(File dir, TreeItem parent, int THUMB_SIZE) + throws IOException { + log.debug("Caching " + dir.getAbsolutePath()); + + TreeItem root = new TreeItem<>(dir.toPath()); + root.setExpanded(false); + File[] files = dir.listFiles(); + final String I18N_CACHE_TEXT = I18N.getString("TokenTool.treeview.caching"); + + final Task task = + new Task() { + @Override + protected Void call() throws Exception { + for (File file : files) { + if (loadOverlaysThread.isInterrupted()) break; + + if (file.isDirectory()) { + cacheOverlays(file, root, THUMB_SIZE); + } else { + Path filePath = file.toPath(); + TreeItem imageNode = + new TreeItem<>(filePath, ImageUtil.getOverlayThumb(new ImageView(), filePath)); + root.getChildren().add(imageNode); + loadCount.getAndIncrement(); + overlayTreeProgressBar + .progressProperty() + .set(loadCount.doubleValue() / overlayCount); + } + } + + if (parent != null) { + // When we show the overlay image, the TreeItem value is empty so we need to + // sort those to the bottom for a cleaner look and keep sub dir's at the top. + // If a node has no children then it's an overlay, otherwise it's a directory... + root.getChildren() + .sort( + new Comparator>() { + @Override + public int compare(TreeItem o1, TreeItem o2) { + if (o1.getChildren().size() == 0 && o2.getChildren().size() == 0) + return 0; + else if (o1.getChildren().size() == 0) return Integer.MAX_VALUE; + else if (o2.getChildren().size() == 0) return Integer.MIN_VALUE; + else return o1.getValue().compareTo(o2.getValue()); + } + }); + + parent.getChildren().add(root); + + parent + .getChildren() + .sort( + new Comparator>() { + @Override + public int compare(TreeItem o1, TreeItem o2) { + if (o1.getChildren().size() == 0 && o2.getChildren().size() == 0) + return 0; + else if (o1.getChildren().size() == 0) return Integer.MAX_VALUE; + else if (o2.getChildren().size() == 0) return Integer.MIN_VALUE; + else return o1.getValue().compareTo(o2.getValue()); + } + }); + } + + return null; + } + }; + + overlayTreeProgressBar + .progressProperty() + .addListener( + observable -> { + Platform.runLater( + () -> + progressBarLabel.setText( + I18N_CACHE_TEXT + + Math.round(overlayCount - loadCount.doubleValue()) + + "...")); + }); + + // Only add this listener to the parent task so it's only called once + if (parent == null) { + overlayTreeProgressBar + .progressProperty() + .addListener( + observable -> { + Platform.runLater( + () -> { + if (overlayTreeProgressBar.getProgress() >= 1) treeViewFinish(); + }); + }); + } + + executorService.execute(task); + return root; + } + + private TreeItem sortTreeNodes(TreeItem tree) { + // Sort the nodes off of root + tree.getChildren() + .sort( + new Comparator>() { + @Override + public int compare(TreeItem o1, TreeItem o2) { + if (o1.getChildren().size() == 0 && o2.getChildren().size() == 0) return 0; + else if (o1.getChildren().size() == 0) return Integer.MAX_VALUE; + else if (o2.getChildren().size() == 0) return Integer.MIN_VALUE; + else return o1.getValue().compareTo(o2.getValue()); + } + }); + + return tree; + } + + /* + * getter/setter methods, mainly for user preferences + */ + public int getOverlayWidth() { + return overlayWidthSpinner.getValue(); + } + + public void setOverlayWidth(int newValue) { + overlayWidthSpinner.getValueFactory().setValue(overlaySpinnerSteps.ceiling(newValue)); + } + + public int getOverlayHeight() { + return overlayHeightSpinner.getValue(); + } + + public void setOverlayHeight(int newValue) { + overlayHeightSpinner.getValueFactory().setValue(overlaySpinnerSteps.ceiling(newValue)); + } + + public boolean getOverlayAspect() { + return overlayAspectToggleButton.isSelected(); + } + + public void setOverlayAspect(boolean selected) { + if (selected != overlayAspectToggleButton.isSelected()) overlayAspectToggleButton.fire(); + } + + public boolean getOverlayUseAsBase() { + return overlayUseAsBaseCheckbox.isSelected(); + } + + public void setOverlayUseAsBase(boolean selected) { + if (selected != overlayUseAsBaseCheckbox.isSelected()) overlayUseAsBaseCheckbox.fire(); + } + + public boolean getClipPortraitCheckbox() { + return clipPortraitCheckbox.isSelected(); + } + + public void setClipPortraitCheckbox(boolean selected) { + if (selected != clipPortraitCheckbox.isSelected()) clipPortraitCheckbox.fire(); + } + + public String getFileNameTextField() { + return fileNameTextField.getText(); + } + + public String getPortraitNameTextField() { + return portraitNameTextField.getText(); + } + + public void setPortraitNameTextField(String text) { + if (!portraitNameTextField.isDisabled()) portraitNameTextField.setText(text); + } + + public boolean getUseTokenNameCheckbox() { + return useTokenNameCheckbox.isSelected(); + } + + public void setUseTokenNameCheckbox(boolean selected) { + if (selected != useTokenNameCheckbox.isSelected()) useTokenNameCheckbox.fire(); + } + + public String getPortraitNameSuffixTextField() { + return portraitNameSuffixTextField.getText(); + } + + public void setPortraitNameSuffixTextField(String text) { + portraitNameSuffixTextField.setText(text); + } + + public void setFileNameTextField(String text) { + fileNameTextField.setText(text); + } + + public void updateFileNameTextField(String text) { + if (!getUseFileNumberingCheckbox()) + if (text == null || text.isEmpty()) + fileNameTextField.setText(AppConstants.DEFAULT_TOKEN_NAME); + else fileNameTextField.setText(FileSaveUtil.cleanFileName(text)); + } + + public boolean getUseFileNumberingCheckbox() { + return useFileNumberingCheckbox.isSelected(); + } + + public void setUseFileNumberingCheckbox(boolean selected) { + if (selected != useFileNumberingCheckbox.isSelected()) useFileNumberingCheckbox.fire(); + } + + public String getFileNameSuffixTextField() { + return fileNameSuffixTextField.getText(); + } + + public void setFileNameSuffixTextField(String text) { + fileNameSuffixTextField.setText(text); + } + + public boolean getSavePortraitOnDragCheckbox() { + return savePortraitOnDragCheckbox.isSelected(); + } + + public void setSavePortraitOnDragCheckbox(boolean selected) { + if (selected != savePortraitOnDragCheckbox.isSelected()) savePortraitOnDragCheckbox.fire(); + } + + public boolean getUseBackgroundOnDragCheckbox() { + return useBackgroundOnDragCheckbox.isSelected(); + } + + public void setUseBackgroundOnDragCheckbox(boolean selected) { + if (selected != useBackgroundOnDragCheckbox.isSelected()) useBackgroundOnDragCheckbox.fire(); + } + + // For user preferences... + public void setWindoFrom_Preferences(String preferencesJson) { + if (preferencesJson != null) { + Window_Preferences window_Preferences = + new Gson().fromJson(preferencesJson, new TypeToken() {}.getType()); + window_Preferences.setWindow(TokenTool.getInstance().getStage()); + } + } + + public Image getPortraitImage() { + return portraitImageView.getImage(); + } + + public String getPortrait_Preferences(String filePath) { + return new ImageView_Preferences(portraitImageView, filePath).toJson(); + } + + public void setPortraitFrom_Preferences(String preferencesJson) { + if (preferencesJson != null) { + ImageView_Preferences imageView_Preferences = + new Gson().fromJson(preferencesJson, new TypeToken() {}.getType()); + portraitImageView = imageView_Preferences.toImageView(portraitImageView); + } else { + log.debug("No Preferences currently saved."); + } + } + + public Image getBackgroundImage() { + return backgroundImageView.getImage(); + } + + public String getBackground_Preferences(String filePath) { + return new ImageView_Preferences(backgroundImageView, filePath, getBackgroundColor()).toJson(); + } + + public void setBackgroundFrom_Preferences(String preferencesJson) { + if (preferencesJson != null) { + ImageView_Preferences imageView_Preferences = + new Gson().fromJson(preferencesJson, new TypeToken() {}.getType()); + backgroundImageView = imageView_Preferences.toImageView(backgroundImageView); + + setBackgroundColor(imageView_Preferences.getBackgroundColor()); + } else { + backgroundImageView.setImage(null); + setBackgroundColor(Color.TRANSPARENT); + } + } + + public Slider getPortraitTransparencySlider() { + return portraitTransparencySlider; + } + + public Slider getPortraitBlurSlider() { + return portraitBlurSlider; + } + + public Slider getPortraitGlowSlider() { + return portraitGlowSlider; + } + + public Slider getOverlayTransparencySlider() { + return overlayTransparencySlider; + } + + public void exitApplication() { + try { + // Lets update the recent list to current overlay... + updateOverlayTreeViewRecentFolder(true); + } catch (NullPointerException npe) { + log.info("Unable to updateOverlayTreeViewRecentFolder on exit."); + } + + try { + AppPreferences.savePreferences(this); + log.info("Exiting application."); + executorService.shutdownNow(); + } catch (Exception e) { + log.error("Error saving preferences!", e); + } finally { + Platform.exit(); + } + } } diff --git a/src/main/java/net/rptools/tokentool/model/ImageView_Preferences.java b/src/main/java/net/rptools/tokentool/model/ImageView_Preferences.java new file mode 100644 index 0000000..1d76338 --- /dev/null +++ b/src/main/java/net/rptools/tokentool/model/ImageView_Preferences.java @@ -0,0 +1,138 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * TokenTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.tokentool.model; + +import com.google.gson.Gson; +import java.io.File; +import java.net.MalformedURLException; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.paint.Color; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/* + * Store and return needed ImageView attributes as a JSON for easy storage in user preferences + */ +public class ImageView_Preferences { + private static final Logger log = LogManager.getLogger(ImageView_Preferences.class); + + double translateX, translateY, rotation, scale; + String filePath; + Color backgroundColor; // We'll save the background color with the background imageview... + + public ImageView_Preferences(ImageView imageView, String filePath) { + setRotation(imageView.getRotate()); + setScale(imageView.getScaleY()); + setTranslateX(imageView.getTranslateX()); + setTranslateY(imageView.getTranslateY()); + setFileURI(filePath); + } + + public ImageView_Preferences(ImageView imageView, String filePath, Color color) { + setRotation(imageView.getRotate()); + setScale(imageView.getScaleY()); + setTranslateX(imageView.getTranslateX()); + setTranslateY(imageView.getTranslateY()); + setFileURI(filePath); + + setBackgroundColor(color); + } + + public double getTranslateX() { + return translateX; + } + + public void setTranslateX(double translateX) { + this.translateX = translateX; + } + + public double getTranslateY() { + return translateY; + } + + public void setTranslateY(double translateY) { + this.translateY = translateY; + } + + public double getRotation() { + return rotation; + } + + public void setRotation(double rotation) { + this.rotation = rotation; + } + + public double getScale() { + return scale; + } + + public void setScale(double scale) { + this.scale = scale; + } + + public String getFileURI() { + return filePath; + } + + public void setFileURI(String fileURI) { + this.filePath = fileURI; + } + + public Color getBackgroundColor() { + // stupid error even though it tests as an instanceof Color!? wtf... + // com.google.gson.internal.LinkedTreeMap cannot be cast to + // javafx.graphics@10.0.1/com.sun.prism.paint.Paint + // return backgroundColor; + + return new Color( + backgroundColor.getRed(), + backgroundColor.getGreen(), + backgroundColor.getBlue(), + backgroundColor.getOpacity()); + } + + public void setBackgroundColor(Color backgroundColor) { + this.backgroundColor = backgroundColor; + } + + public ImageView toImageView(ImageView imageView) { + if (filePath != null) { + try { + log.debug("Loading image from preferences " + filePath); + Image image = new Image(new File(filePath).toURI().toURL().toExternalForm()); + imageView.setImage(image); + imageView.setFitWidth(image.getWidth()); + imageView.setFitHeight(image.getHeight()); + } catch (MalformedURLException e) { + log.error("Unable to load image " + filePath, e); + } + } + + imageView.setTranslateX(getTranslateX()); + imageView.setTranslateY(getTranslateY()); + imageView.setRotate(getRotation()); + imageView.setScaleX(getScale()); + imageView.setScaleY(getScale()); + + return imageView; + } + + public String toJson() { + String json = new Gson().toJson(this).toString(); + log.debug("JSON output: " + json); + return json; + } +} diff --git a/src/main/java/net/rptools/tokentool/model/OverlayTreeItem.java b/src/main/java/net/rptools/tokentool/model/OverlayTreeItem.java index f361743..ae41594 100644 --- a/src/main/java/net/rptools/tokentool/model/OverlayTreeItem.java +++ b/src/main/java/net/rptools/tokentool/model/OverlayTreeItem.java @@ -1,10 +1,16 @@ /* - * This software Copyright by the RPTools.net development team, and licensed under the Affero GPL Version 3 or, at your option, any later version. + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. * - * TokenTool Source Code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * TokenTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * - * You should have received a copy of the GNU Affero General Public License * along with this source Code. If not, please visit and specifically the Affero license text - * at . + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . */ package net.rptools.tokentool.model; @@ -14,83 +20,83 @@ import java.nio.file.Path; import java.util.stream.Collectors; import java.util.stream.Stream; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.scene.control.TreeItem; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; public class OverlayTreeItem extends TreeItem { - private static final Logger log = LogManager.getLogger(OverlayTreeItem.class); + private static final Logger log = LogManager.getLogger(OverlayTreeItem.class); - private boolean isFirstTimeChildren = true; - private boolean isFirstTimeLeaf = true; - private boolean isLeaf; + private boolean isFirstTimeChildren = true; + private boolean isFirstTimeLeaf = true; + private boolean isLeaf; - public boolean isDirectory() { - return Files.isDirectory(getValue()); - } + public boolean isDirectory() { + return Files.isDirectory(getValue()); + } - public OverlayTreeItem(Path f) { - super(f); - } + public OverlayTreeItem(Path f) { + super(f); + } - public OverlayTreeItem(File f) { - super(f.toPath()); - } + public OverlayTreeItem(File f) { + super(f.toPath()); + } - @Override - public ObservableList> getChildren() { - if (isFirstTimeChildren) { - isFirstTimeChildren = false; + @Override + public ObservableList> getChildren() { + if (isFirstTimeChildren) { + isFirstTimeChildren = false; - /* - * First getChildren() call, so we actually go off and determine the children of the File contained in this TreeItem. - */ - super.getChildren().setAll(buildChildren()); - } - return super.getChildren(); - } + /* + * First getChildren() call, so we actually go off and determine the children of the File contained in this TreeItem. + */ + super.getChildren().setAll(buildChildren()); + } + return super.getChildren(); + } - @Override - public boolean isLeaf() { - if (isFirstTimeLeaf) { - isFirstTimeLeaf = false; - try { - // try-with-resources statement ensures that each resource is closed at the end of the statement otherwise stream is left open and directory can not be deleted! - try (Stream files = Files.list(getValue()).filter(Files::isDirectory)) { - isLeaf = files.count() == 0; - } + @Override + public boolean isLeaf() { + if (isFirstTimeLeaf) { + isFirstTimeLeaf = false; + try { + // try-with-resources statement ensures that each resource is closed at the end of the + // statement otherwise stream is left open and directory can not be deleted! + try (Stream files = Files.list(getValue()).filter(Files::isDirectory)) { + isLeaf = files.count() == 0; + } - } catch (IOException e) { - log.error(e); - } - } - return isLeaf; - } + } catch (IOException e) { + log.error(e); + } + } + return isLeaf; + } - /** - * Returning a collection of type ObservableList containing TreeItems, which represent all children of this TreeITem. - * - * - * @return an ObservableList> containing TreeItems, which represent all children available in this TreeItem. If the handed TreeItem is a leaf, an empty list is returned. - */ - private ObservableList> buildChildren() { - if (Files.isDirectory(getValue())) { - try { - return Files.list(getValue()) - .filter(Files::isDirectory) - .map(OverlayTreeItem::new) - .collect(Collectors.toCollection(() -> FXCollections.observableArrayList())); + /** + * Returning a collection of type ObservableList containing TreeItems, which represent all + * children of this TreeITem. + * + * @return an ObservableList> containing TreeItems, which represent all children + * available in this TreeItem. If the handed TreeItem is a leaf, an empty list is returned. + */ + private ObservableList> buildChildren() { + if (Files.isDirectory(getValue())) { + try { + return Files.list(getValue()) + .filter(Files::isDirectory) + .map(OverlayTreeItem::new) + .collect(Collectors.toCollection(() -> FXCollections.observableArrayList())); - } catch (IOException e) { - e.printStackTrace(); - return FXCollections.emptyObservableList(); - } - } + } catch (IOException e) { + e.printStackTrace(); + return FXCollections.emptyObservableList(); + } + } - return FXCollections.emptyObservableList(); - } -} \ No newline at end of file + return FXCollections.emptyObservableList(); + } +} diff --git a/src/main/java/net/rptools/tokentool/model/PdfModel.java b/src/main/java/net/rptools/tokentool/model/PdfModel.java new file mode 100644 index 0000000..8df54d8 --- /dev/null +++ b/src/main/java/net/rptools/tokentool/model/PdfModel.java @@ -0,0 +1,112 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * TokenTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.tokentool.model; + +import com.twelvemonkeys.io.FileUtil; +import java.awt.Toolkit; +import java.awt.image.BufferedImage; +import java.io.EOFException; +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import javafx.embed.swing.SwingFXUtils; +import javafx.scene.control.ToggleButton; +import javafx.scene.image.Image; +import javafx.scene.image.WritableImage; +import net.rptools.tokentool.controller.TokenTool_Controller; +import net.rptools.tokentool.util.ExtractImagesFromPDF; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.rendering.PDFRenderer; + +public class PdfModel { + private static final Logger log = LogManager.getLogger(PdfModel.class); + + private PDDocument document; + private PDFRenderer renderer; + private ExtractImagesFromPDF imageExtractor; + + // Not sure if this gives proper DPI on multiple monitor setup? For instance, it returns 96 for me + // when the specs for Dell U3415W says 109? + // Was rendering a little blurry at 96 so lets give it a bump, sacrificing memory for + // resolution... + private double DPI = Math.max(Toolkit.getDefaultToolkit().getScreenResolution() * 1.5, 100); + + private Map pageCache = new HashMap(); + + public PdfModel(File pdfFile, TokenTool_Controller tokenTool_Controller) throws IOException { + try { + // document = PDDocument.load(pdfFile, MemoryUsageSetting.setupTempFileOnly()); + document = PDDocument.load(pdfFile); + + renderer = new PDFRenderer(document); + imageExtractor = + new ExtractImagesFromPDF(document, FileUtil.getBasename(pdfFile), tokenTool_Controller); + } catch (IOException ex) { + throw new UncheckedIOException( + "PDDocument throws IOException file=" + pdfFile.getAbsolutePath(), ex); + } + + log.info("Rendering at " + DPI + " DPI"); + } + + public int numPages() { + return document.getPages().getCount(); + } + + public Image getImage(int pageNumber) { + if (pageCache.containsKey(pageNumber)) return pageCache.get(pageNumber); + + Image pageImage = new WritableImage(1, 1); + + try { + BufferedImage pageBufferedImage = renderer.renderImageWithDPI(pageNumber, (float) DPI); + pageImage = SwingFXUtils.toFXImage(pageBufferedImage, null); + pageCache.put(pageNumber, pageImage); + } catch (EOFException eof) { + log.warn("PDFBox encountered an error: ", eof); + } catch (IOException ex) { + throw new UncheckedIOException("PDFRenderer throws IOException", ex); + } + + return pageImage; + } + + public void close() { + try { + document.close(); + } catch (IOException e) { + log.error("Error closing PDF Document.", e); + } + } + + public ArrayList extractImages(int currentPageIndex) { + try { + // imageExtractor.interrupt(); + return imageExtractor.addImages(currentPageIndex); + } catch (IOException e) { + log.error("Error extracting images from PDF...", e); + return null; + } + } + + public void interrupt() { + imageExtractor.interrupt(); + } +} diff --git a/src/main/java/net/rptools/tokentool/model/Window_Preferences.java b/src/main/java/net/rptools/tokentool/model/Window_Preferences.java new file mode 100644 index 0000000..56cd553 --- /dev/null +++ b/src/main/java/net/rptools/tokentool/model/Window_Preferences.java @@ -0,0 +1,87 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * TokenTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.tokentool.model; + +import com.google.gson.Gson; +import javafx.stage.Stage; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/* + * Store and return needed Stage (window) attributes as a JSON for easy storage in user preferences + */ +public class Window_Preferences { + private static final Logger log = LogManager.getLogger(Window_Preferences.class); + + double windowX = 0, windowY = 0; + double windowWidth, windowHeight; + + public Window_Preferences(Stage stage) { + windowX = stage.getX(); + windowY = stage.getY(); + windowWidth = stage.getWidth(); + windowHeight = stage.getHeight(); + } + + public Window_Preferences(double windowWidth, double windowHeight) { + this.windowWidth = windowWidth; + this.windowHeight = windowHeight; + } + + public void setWindow(Stage stage) { + stage.setX(windowX); + stage.setY(windowY); + if (windowWidth > 10) stage.setWidth(windowWidth); + if (windowHeight > 10) stage.setHeight(windowHeight); + } + + public double getWindowX() { + return windowX; + } + + public void setWindowX(double windowX) { + this.windowX = windowX; + } + + public double getWindowY() { + return windowY; + } + + public void setWindowY(double windowY) { + this.windowY = windowY; + } + + public double getWindowWidth() { + return windowWidth; + } + + public void setWindowWidth(double windowWidth) { + this.windowWidth = windowWidth; + } + + public double getWindowHeight() { + return windowHeight; + } + + public void setWindowHeight(double windowHeight) { + this.windowHeight = windowHeight; + } + + public String toJson() { + String json = new Gson().toJson(this).toString(); + log.debug("JSON output: " + json); + return json; + } +} diff --git a/src/main/java/net/rptools/tokentool/util/ExtractImagesFromPDF.java b/src/main/java/net/rptools/tokentool/util/ExtractImagesFromPDF.java new file mode 100644 index 0000000..2181c0b --- /dev/null +++ b/src/main/java/net/rptools/tokentool/util/ExtractImagesFromPDF.java @@ -0,0 +1,262 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * TokenTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.tokentool.util; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import javafx.embed.swing.SwingFXUtils; +import javafx.event.ActionEvent; +import javafx.scene.control.ToggleButton; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.input.ClipboardContent; +import javafx.scene.input.Dragboard; +import javafx.scene.input.MouseButton; +import javafx.scene.input.MouseEvent; +import javafx.scene.input.TransferMode; +import javax.imageio.ImageIO; +import net.rptools.tokentool.controller.TokenTool_Controller; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.cos.COSStream; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDResources; +import org.apache.pdfbox.pdmodel.graphics.PDXObject; +import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; +import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceEntry; + +/** + * Extract all images from a PDF using Apache's PdfBox 2.0 This will also walk through all + * annotations and extract those images as well which is key, some interactive PDF's, such as from + * Paizo, store different versions of maps as button icons, which will not normally extract using + * other methods. + * + * @author Jamz + */ +public final class ExtractImagesFromPDF { + private static final Logger log = LogManager.getLogger(ExtractImagesFromPDF.class); + + private final PDDocument document; + + private final Set imageTracker = new HashSet(); + + private static final int imageViewSize = 175; + private static final int imageButtonSize = 200; + + private TokenTool_Controller tokenTool_Controller; + private ArrayList imageButtons = new ArrayList(); + private int currentPageNumber; + private String pdfName; + + private FileSaveUtil fileSaveUtil = new FileSaveUtil(); + + private boolean isRunning; + private boolean interrupt; + + public ExtractImagesFromPDF( + PDDocument document, String pdfName, TokenTool_Controller tokenTool_Controller) { + this.tokenTool_Controller = tokenTool_Controller; + this.document = document; + this.pdfName = pdfName; + } + + public ArrayList addImages(int pageNumber) throws IOException { + isRunning = true; + imageTracker.clear(); + imageButtons.clear(); + this.currentPageNumber = pageNumber; + + extractAnnotationImages(document.getPage(pageNumber)); + getImagesFromResources(document.getPage(pageNumber).getResources()); + + isRunning = false; + interrupt = false; + return imageButtons; + } + + private void getImagesFromResources(PDResources resources) throws IOException { + // Testing various Pathfinder PDF's, various page elements like borders and backgrounds + // generally come first... + // ...so lets sort them to the bottom and get the images we really want to the top of the + // TilePane! + ArrayList xObjectNamesReversed = new ArrayList<>(); + + for (COSName xObjectName : resources.getXObjectNames()) { + xObjectNamesReversed.add(xObjectName); + } + + Collections.reverse(xObjectNamesReversed); + + for (COSName xObjectName : xObjectNamesReversed) { + if (interrupt) return; + + PDXObject xObject = resources.getXObject(xObjectName); + + if (xObject instanceof PDFormXObject) { + getImagesFromResources(((PDFormXObject) xObject).getResources()); + } else if (xObject instanceof PDImageXObject) { + if (!imageTracker.contains(xObject.getCOSObject())) { + imageTracker.add(xObject.getCOSObject()); + String name = pdfName + " - pg " + currentPageNumber + " - " + xObjectName.getName(); + log.debug("Extracting image... " + name); + + addTileButton(SwingFXUtils.toFXImage(((PDImageXObject) xObject).getImage(), null), name); + } + } + } + } + + /* + * Jamz: A note on what we are doing here... + * + * Paizo's Interactive PDF's (amongst others) are sneaky and put map images in the PDF as a "button" with an image resource. So we need to walk through all the forms to find the buttons, then walk + * through all the button resources for the images. Also, a 'Button Down' may hold the 'Grid' version of the map and 'Button Up' may hold the 'Non-Grid' version. There may also be Player vs GM + * versions of each for a total of up to 4 images per button! + * + * This is the REAL beauty of this function as currently no other tools outside of Full Acrobat extracts these raw images! + * + */ + private void extractAnnotationImages(PDPage page) throws IOException { + for (PDAnnotation annotation : page.getAnnotations()) { + extractAnnotationImages(annotation); + } + } + + private void extractAnnotationImages(PDAnnotation annotation) throws IOException { + PDAppearanceDictionary appearance = annotation.getAppearance(); + + if (appearance == null) return; + + extractAnnotationImages(appearance.getDownAppearance()); + extractAnnotationImages(appearance.getNormalAppearance()); + extractAnnotationImages(appearance.getRolloverAppearance()); + } + + private void extractAnnotationImages(PDAppearanceEntry appearance) throws IOException { + if (interrupt) return; + + PDResources resources = appearance.getAppearanceStream().getResources(); + if (resources == null) return; + + for (COSName cosname : resources.getXObjectNames()) { + PDXObject xObject = resources.getXObject(cosname); + + if (xObject instanceof PDFormXObject) extractAnnotationImages((PDFormXObject) xObject); + else if (xObject instanceof PDImageXObject) extractAnnotationImages((PDImageXObject) xObject); + } + } + + private void extractAnnotationImages(PDFormXObject form) throws IOException { + PDResources resources = form.getResources(); + if (resources == null) return; + + for (COSName cosname : resources.getXObjectNames()) { + PDXObject xObject = resources.getXObject(cosname); + + if (xObject instanceof PDFormXObject) extractAnnotationImages((PDFormXObject) xObject); + else if (xObject instanceof PDImageXObject) extractAnnotationImages((PDImageXObject) xObject); + } + } + + private void extractAnnotationImages(PDImageXObject xObject) throws IOException { + if (!imageTracker.contains(xObject.getCOSObject())) { + + String name = pdfName + " - pg " + currentPageNumber + " - img " + imageTracker.size(); + + log.debug("Extracting Annotation, eg button image... " + name); + + imageTracker.add(xObject.getCOSObject()); + addTileButton(SwingFXUtils.toFXImage(xObject.getImage(), null), name); + } + } + + private void addTileButton(Image buttonImage, String imageName) { + ToggleButton imageButton = new ToggleButton(); + ImageView imageViewNode = new ImageView(buttonImage); + imageViewNode.setFitWidth(imageViewSize); + imageViewNode.setFitHeight(imageViewSize); + imageButton.setPrefWidth(imageButtonSize); + imageButton.setPrefHeight(imageButtonSize); + imageViewNode.setPreserveRatio(true); + + imageButton.getStyleClass().add("overlay-toggle-button"); + imageButton.setGraphic(imageViewNode); + + // Can also drag image to TokenTool pane OR any other place, like MapTool! + imageButton.setOnDragDetected( + event -> { + Dragboard db = imageButton.startDragAndDrop(TransferMode.ANY); + ClipboardContent content = new ClipboardContent(); + + try { + File tempImageFile; + tempImageFile = fileSaveUtil.getTempFileName(imageName); + + ImageIO.write(SwingFXUtils.fromFXImage(buttonImage, null), "png", tempImageFile); + content.putFiles(java.util.Collections.singletonList(tempImageFile)); + tempImageFile.deleteOnExit(); + } catch (IOException e) { + log.error("Unable to write token to file: " + imageName, e); + } catch (Exception e) { + log.error(e); + } finally { + content.putImage(buttonImage); + db.setContent(content); + event.consume(); + } + event.consume(); + }); + + // Right click sets background vs portrait... + // Drag will consume the event first so image doesn't reset... + imageButton.addEventHandler( + MouseEvent.MOUSE_RELEASED, + event -> { + imageButton.setSelected(true); + tokenTool_Controller.updateImage( + imageViewNode.getImage(), imageName, event.getButton().equals(MouseButton.SECONDARY)); + event.consume(); + }); + + // capture other actions like touch, focus+spacebar, etc + imageButton.addEventHandler( + ActionEvent.ACTION, + event -> { + imageButton.setSelected(true); + tokenTool_Controller.updateImage(imageViewNode.getImage(), imageName); + event.consume(); + }); + + if (interrupt) log.info("I REALLY SHOULD STOP!"); + else log.info("Free to go..."); + + imageButtons.add(imageButton); + } + + public void interrupt() { + log.info("isRunning? " + isRunning); + + if (isRunning) interrupt = true; + } +} diff --git a/src/main/java/net/rptools/tokentool/util/FileSaveUtil.java b/src/main/java/net/rptools/tokentool/util/FileSaveUtil.java index 3c09a79..aed9c79 100644 --- a/src/main/java/net/rptools/tokentool/util/FileSaveUtil.java +++ b/src/main/java/net/rptools/tokentool/util/FileSaveUtil.java @@ -1,138 +1,174 @@ /* - * This software Copyright by the RPTools.net development team, and licensed under the Affero GPL Version 3 or, at your option, any later version. + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. * - * TokenTool Source Code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * TokenTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * - * You should have received a copy of the GNU Affero General Public License * along with this source Code. If not, please visit and specifically the Affero license text - * at . + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . */ package net.rptools.tokentool.util; import java.io.File; import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; import java.util.regex.Matcher; import java.util.regex.Pattern; - +import javafx.scene.control.TextField; +import net.rptools.tokentool.AppConstants; import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import javafx.scene.control.TextField; -import net.rptools.tokentool.AppConstants; - public class FileSaveUtil { - private static final Logger log = LogManager.getLogger(FileSaveUtil.class); - private static File lastFile = null; - - public File getTempFileName(boolean asToken, boolean useNumbering, String tempFileName, - TextField fileNameSuffix) throws IOException { - - return new File(System.getProperty("java.io.tmpdir"), getFileName(asToken, useNumbering, tempFileName, fileNameSuffix).getName()); - } - - public File getFileName(boolean asToken, boolean useNumbering, String tempFileName, TextField fileNameSuffix) - throws IOException { - final String _extension; - - _extension = AppConstants.DEFAULT_IMAGE_EXTENSION; - - if (useNumbering) { - int dragCounter; - try { - dragCounter = Integer.parseInt(fileNameSuffix.getText()); - } catch (NumberFormatException e) { - dragCounter = 0; - } - - String leadingZeroes = "%0" + fileNameSuffix.getLength() + "d"; - - fileNameSuffix.setText(String.format(leadingZeroes, dragCounter + 1)); - - if (tempFileName.isEmpty()) - tempFileName = AppConstants.DEFAULT_TOKEN_NAME; - - if (lastFile != null) { - return new File(lastFile.getParent(), String.format("%s_" + leadingZeroes + _extension, tempFileName, dragCounter)); - } else { - return new File(String.format("%s_" + leadingZeroes + _extension, tempFileName, dragCounter)); - } - } else { - if (lastFile != null) - if (tempFileName.isEmpty()) - tempFileName = AppConstants.DEFAULT_TOKEN_NAME + _extension; - - if (!tempFileName.endsWith(_extension)) - tempFileName += _extension; - - if (lastFile != null) - lastFile = new File(lastFile.getParent(), tempFileName); - else - lastFile = new File(tempFileName); - - return lastFile; - } - } - - public static String cleanFileName(String fileName) { - return fileName.replaceAll(AppConstants.VALID_FILE_NAME_REPLACEMENT_PATTERN, AppConstants.VALID_FILE_NAME_REPLACEMENT_CHARACTER); - } - - /* - * Attempt to find filename buried inside the URL e.g. https://vignette.wikia.nocookie.net/glass-cannon/images/b/bd/Barron.jpg/revision/latest/scale-to-width-down/552?cb=20170511190014 - */ - public static String searchURL(String urlString) { - String imageTypes = ""; - - for (String type : ImageUtil.SUPPORTED_FILE_FILTER_ARRAY) { - if (imageTypes.isEmpty()) - imageTypes = type; - else - imageTypes += "|" + type; - } - Pattern p = Pattern.compile("([\\w-]+)\\.(?i:" + imageTypes.replace(".", "") + ")"); - Matcher m = p.matcher(urlString); - - if (m.find()) - return m.group(1); - else - return FilenameUtils.getBaseName(urlString); - } - - public static File getLastFile() { - return lastFile; - } - - public static void setLastFile(File file) { - lastFile = file; - } - - public static void setLastFile(String filePath) { - if (filePath != null) - lastFile = new File(filePath); - } - - public static void copyFile(File srcFile, File destDir) { - try { - FileUtils.copyFile(srcFile, new File(destDir, srcFile.getName())); - } catch (Exception e) { - log.error("Could not copy " + srcFile, e); - } - } - - public static boolean makeDir(String dirName, File destDir) { - if (dirName.isEmpty()) - return false; - - File newDir; - newDir = new File(destDir, dirName); - if (newDir.mkdir()) { - log.info("Created directory: " + newDir.getAbsolutePath()); - return true; - } else { - log.error("Could not create directory: " + newDir.getAbsolutePath()); - } - - return false; - } + private static final Logger log = LogManager.getLogger(FileSaveUtil.class); + private static File lastFile = null; + + public File getTempFileName( + boolean asToken, boolean useNumbering, String tempFileName, TextField fileNameSuffix) + throws IOException { + return getTempFileName(asToken, useNumbering, tempFileName, fileNameSuffix, true); + } + + public File getTempFileName( + boolean asToken, + boolean useNumbering, + String tempFileName, + TextField fileNameSuffix, + boolean advanceFileNameSuffix) + throws IOException { + return new File( + System.getProperty("java.io.tmpdir"), + getFileName(asToken, useNumbering, tempFileName, fileNameSuffix, advanceFileNameSuffix) + .getName()); + } + + public File getTempFileName(String tempFileName) throws IOException { + final String _extension = AppConstants.DEFAULT_IMAGE_EXTENSION; + + if (!tempFileName.endsWith(_extension)) tempFileName += _extension; + + return new File(System.getProperty("java.io.tmpdir"), tempFileName); + } + + public File getFileName( + boolean asToken, + boolean useNumbering, + String tempFileName, + TextField fileNameSuffix, + boolean advanceFileNameSuffix) + throws IOException { + final String _extension = AppConstants.DEFAULT_IMAGE_EXTENSION; + + if (useNumbering) { + int dragCounter; + try { + dragCounter = Integer.parseInt(fileNameSuffix.getText()); + } catch (NumberFormatException e) { + dragCounter = 0; + } + + String leadingZeroes = "%0" + fileNameSuffix.getLength() + "d"; + + if (advanceFileNameSuffix) + fileNameSuffix.setText(String.format(leadingZeroes, dragCounter + 1)); + + if (tempFileName.isEmpty()) tempFileName = AppConstants.DEFAULT_TOKEN_NAME; + + if (lastFile != null) { + return new File( + lastFile.getParent(), + String.format("%s_" + leadingZeroes + _extension, tempFileName, dragCounter)); + } else { + return new File( + String.format("%s_" + leadingZeroes + _extension, tempFileName, dragCounter)); + } + } else { + if (lastFile != null) + if (tempFileName.isEmpty()) tempFileName = AppConstants.DEFAULT_TOKEN_NAME + _extension; + + if (!tempFileName.endsWith(_extension)) tempFileName += _extension; + + if (lastFile != null) lastFile = new File(lastFile.getParent(), tempFileName); + else lastFile = new File(tempFileName); + + return lastFile; + } + } + + public static String cleanFileName(String fileName) { + String decodedFileName = fileName; + + try { + decodedFileName = URLDecoder.decode(decodedFileName, "UTF-8").toString(); + } catch (UnsupportedEncodingException e) { + log.error("Issue decoding file name: " + fileName, e); + } finally { + decodedFileName = + decodedFileName.replaceAll( + AppConstants.VALID_FILE_NAME_REPLACEMENT_PATTERN, + AppConstants.VALID_FILE_NAME_REPLACEMENT_CHARACTER); + } + + return decodedFileName; + } + + /* + * Attempt to find filename buried inside the URL e.g. https://vignette.wikia.nocookie.net/glass-cannon/images/b/bd/Barron.jpg/revision/latest/scale-to-width-down/552?cb=20170511190014 + */ + public static String searchURL(String urlString) { + String imageTypes = ""; + + for (String type : ImageUtil.SUPPORTED_FILE_FILTER_ARRAY) { + if (imageTypes.isEmpty()) imageTypes = type; + else imageTypes += "|" + type; + } + Pattern p = Pattern.compile("([\\w-]+)\\.(?i:" + imageTypes.replace(".", "") + ")"); + Matcher m = p.matcher(urlString); + + if (m.find()) return m.group(1); + else return FilenameUtils.getBaseName(urlString); + } + + public static File getLastFile() { + return lastFile; + } + + public static void setLastFile(File file) { + lastFile = file; + } + + public static void setLastFile(String filePath) { + if (filePath != null) lastFile = new File(filePath); + } + + public static void copyFile(File srcFile, File destDir) { + try { + FileUtils.copyFile(srcFile, new File(destDir, srcFile.getName())); + } catch (Exception e) { + log.error("Could not copy " + srcFile, e); + } + } + + public static boolean makeDir(String dirName, File destDir) { + if (dirName.isEmpty()) return false; + + File newDir; + newDir = new File(destDir, dirName); + if (newDir.mkdir()) { + log.info("Created directory: " + newDir.getAbsolutePath()); + return true; + } else { + log.error("Could not create directory: " + newDir.getAbsolutePath()); + } + + return false; + } } diff --git a/src/main/java/net/rptools/tokentool/util/I18N.java b/src/main/java/net/rptools/tokentool/util/I18N.java index 9ef60dd..fe33896 100644 --- a/src/main/java/net/rptools/tokentool/util/I18N.java +++ b/src/main/java/net/rptools/tokentool/util/I18N.java @@ -1,10 +1,16 @@ /* - * This software Copyright by the RPTools.net development team, and licensed under the Affero GPL Version 3 or, at your option, any later version. + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. * - * TokenTool Source Code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * TokenTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * - * You should have received a copy of the GNU Affero General Public License * along with this source Code. If not, please visit and specifically the Affero license text - * at . + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . */ package net.rptools.tokentool.util; @@ -12,18 +18,17 @@ import java.util.ResourceBundle; public class I18N { - private static final String BUNDLE_NAME = "net.rptools.tokentool.i18n.TokenTool"; + private static final String BUNDLE_NAME = "net.rptools.tokentool.i18n.TokenTool"; - private static final ResourceBundle RESOURCE_BUNDLE = ResourceBundle.getBundle(BUNDLE_NAME); + private static final ResourceBundle RESOURCE_BUNDLE = ResourceBundle.getBundle(BUNDLE_NAME); - private I18N() { - } + private I18N() {} - public static String getString(String key) { - try { - return RESOURCE_BUNDLE.getString(key); - } catch (MissingResourceException e) { - return '!' + key + '!'; - } - } + public static String getString(String key) { + try { + return RESOURCE_BUNDLE.getString(key); + } catch (MissingResourceException e) { + return '!' + key + '!'; + } + } } diff --git a/src/main/java/net/rptools/tokentool/util/ImageUtil.java b/src/main/java/net/rptools/tokentool/util/ImageUtil.java index c0a4244..db309c8 100644 --- a/src/main/java/net/rptools/tokentool/util/ImageUtil.java +++ b/src/main/java/net/rptools/tokentool/util/ImageUtil.java @@ -1,14 +1,23 @@ /* - * This software Copyright by the RPTools.net development team, and licensed under the Affero GPL Version 3 or, at your option, any later version. + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. * - * TokenTool Source Code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * TokenTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * - * You should have received a copy of the GNU Affero General Public License * along with this source Code. If not, please visit and specifically the Affero license text - * at . + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . */ package net.rptools.tokentool.util; +import com.twelvemonkeys.imageio.plugins.psd.PSDImageReader; +import com.twelvemonkeys.imageio.plugins.psd.PSDMetadata; import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FilenameFilter; import java.io.IOException; @@ -17,23 +26,6 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; - -import javax.imageio.ImageIO; -import javax.imageio.ImageReader; -import javax.imageio.metadata.IIOMetadata; -import javax.imageio.metadata.IIOMetadataNode; -import javax.imageio.stream.ImageInputStream; - -import org.apache.commons.io.FilenameUtils; -import org.apache.commons.io.filefilter.IOFileFilter; -import org.apache.commons.io.filefilter.SuffixFileFilter; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.w3c.dom.NodeList; - -import com.twelvemonkeys.imageio.plugins.psd.PSDImageReader; -import com.twelvemonkeys.imageio.plugins.psd.PSDMetadata; - import javafx.embed.swing.SwingFXUtils; import javafx.geometry.Rectangle2D; import javafx.scene.Group; @@ -47,381 +39,454 @@ import javafx.scene.layout.StackPane; import javafx.scene.paint.Color; import javafx.stage.FileChooser.ExtensionFilter; +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.metadata.IIOMetadataNode; +import javax.imageio.stream.ImageInputStream; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.io.filefilter.IOFileFilter; +import org.apache.commons.io.filefilter.SuffixFileFilter; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.w3c.dom.NodeList; public class ImageUtil { - private static final Logger log = LogManager.getLogger(ImageUtil.class); - - private static final int THUMB_SIZE = 100; - private static final int COLOR_THRESHOLD = 1; - - public static ImageView getOverlayThumb(ImageView thumbView, Path filePath) throws IOException { - return getImage(thumbView, filePath, true, THUMB_SIZE); - } - - public static ImageView getOverlayImage(ImageView thumbView, Path overlayFileURI) throws IOException { - return getImage(thumbView, overlayFileURI, true, 0); - } - - public static ImageView getMaskImage(ImageView thumbView, Path overlayFileURI) throws IOException { - return getImage(thumbView, overlayFileURI, false, 0); - } - - private static ImageView getImage(ImageView thumbView, final Path filePath, final boolean overlayWanted, final int THUMB_SIZE) throws IOException { - Image thumb = null; - String fileURL = filePath.toUri().toURL().toString(); - - if (ImageUtil.SUPPORTED_IMAGE_FILE_FILTER.accept(null, fileURL)) { - if (THUMB_SIZE <= 0) - thumb = processMagenta(new Image(fileURL), COLOR_THRESHOLD, overlayWanted); - else - thumb = processMagenta(new Image(fileURL, THUMB_SIZE, THUMB_SIZE, true, true), COLOR_THRESHOLD, overlayWanted); - } else if (ImageUtil.PSD_FILE_FILTER.accept(null, fileURL)) { - ImageInputStream is = null; - PSDImageReader reader = null; - int imageIndex = 1; - - // Mask layer should always be layer 1 and overlay image on layer 2. Note, layer 0 will be a combined layer composite - if (overlayWanted) - imageIndex = 2; - - File file = filePath.toFile(); - - try { - is = ImageIO.createImageInputStream(file); - if (is == null || is.length() == 0) { - log.info("Image from file " + file.getAbsolutePath() + " is null"); - } - - Iterator iterator = ImageIO.getImageReaders(is); - if (iterator == null || !iterator.hasNext()) { - throw new IOException("Image file format not supported by ImageIO: " + filePath); - } - - reader = (PSDImageReader) iterator.next(); - reader.setInput(is); - BufferedImage thumbBI; - thumbBI = reader.read(imageIndex); - - if (thumbBI != null) { - int layerIndex = 0; - if (overlayWanted) - layerIndex = 1; - - IIOMetadata metadata = reader.getImageMetadata(0); - IIOMetadataNode root = (IIOMetadataNode) metadata.getAsTree(PSDMetadata.NATIVE_METADATA_FORMAT_NAME); - NodeList layerInfos = root.getElementsByTagName("LayerInfo"); - - // Layer index corresponds to imageIndex - 1 in the reader - IIOMetadataNode layerInfo = (IIOMetadataNode) layerInfos.item(layerIndex); - - // Get the width & height of the Mask layer so we can create the overlay the same size - int width = reader.getWidth(0); - int height = reader.getHeight(0); - - // Get layer offsets, PhotoShop PSD layers can have different widths/heights and all images start at 0,0 with a layer offset applied - int x = Math.max(Integer.parseInt(layerInfo.getAttribute("left")), 0); - int y = Math.max(Integer.parseInt(layerInfo.getAttribute("top")), 0); - - // Lets pad the overlay with transparency to make it the same size as the PSD canvas size - thumb = resizeCanvas(SwingFXUtils.toFXImage(thumbBI, null), width, height, x, y); - - // Finally set ImageView to thumbnail size - if (THUMB_SIZE > 0) { - thumbView.setFitWidth(THUMB_SIZE); - thumbView.setPreserveRatio(true); - } - } - } catch (Exception e) { - log.error("Processing: " + file.getAbsolutePath(), e); - } finally { - // Dispose reader in finally block to avoid memory leaks - reader.dispose(); - is.close(); - } - } - - thumbView.setImage(thumb); - - return thumbView; - } - - public static Image resizeCanvas(Image imageSource, double newWidth, double newHeight) { - int offsetX = (int) ((newWidth - imageSource.getWidth()) / 2); - int offsetY = (int) ((newHeight - imageSource.getHeight()) / 2); - - return resizeCanvas(imageSource, (int) newWidth, (int) newHeight, offsetX, offsetY); - } - - /* - * Resize the overall image width/height without scaling the actual image, eg resize the canvas - */ - public static Image resizeCanvas(Image imageSource, int newWidth, int newHeight, int offsetX, int offsetY) { - int sourceWidth = (int) imageSource.getWidth(); - int sourceHeight = (int) imageSource.getHeight(); - - // No work needed here... - if (sourceWidth == newWidth && sourceHeight == newHeight) - return imageSource; - - WritableImage outputImage = new WritableImage(newWidth, newHeight); - PixelReader pixelReader = imageSource.getPixelReader(); - PixelWriter pixelWriter = outputImage.getPixelWriter(); - WritablePixelFormat format = WritablePixelFormat.getIntArgbInstance(); - - int[] buffer = new int[sourceWidth * sourceHeight]; - pixelReader.getPixels(0, 0, sourceWidth, sourceHeight, format, buffer, 0, sourceWidth); - pixelWriter.setPixels(offsetX, offsetY, sourceWidth, sourceHeight, format, buffer, 0, sourceWidth); - - return outputImage; - } - - /* - * Resize the overall image width/height scaled to the target width/height - */ - public static Image scaleImage(Image source, double targetWidth, double targetHeight, boolean preserveRatio) { - ImageView imageView = new ImageView(source); - imageView.setPreserveRatio(preserveRatio); - imageView.setFitWidth(targetWidth); - imageView.setFitHeight(targetHeight); - return imageView.snapshot(null, null); - } - - /* - * Return the intersection between the source image and the mask. Note, the mask does not need to be magenta anymore, any non-transparent pixel is considering a mask - */ - private static Image clipImageWithMask(Image imageSource, Image imageMask) { - int imageWidth = (int) imageMask.getWidth(); - int imageHeight = (int) imageMask.getHeight(); - - WritableImage outputImage = new WritableImage(imageWidth, imageHeight); - PixelReader pixelReader_Mask = imageMask.getPixelReader(); - PixelReader pixelReader_Source = imageSource.getPixelReader(); - PixelWriter pixelWriter = outputImage.getPixelWriter(); - - for (int readY = 0; readY < imageHeight; readY++) { - for (int readX = 0; readX < imageWidth; readX++) { - Color pixelColor = pixelReader_Mask.getColor(readX, readY); - - if (pixelColor.equals(Color.TRANSPARENT)) - pixelWriter.setColor(readX, readY, pixelReader_Source.getColor(readX, readY)); - } - } - - return outputImage; - } - - /* - * Crop image to smallest width/height based on transparency - */ - private static Image autoCropImage(Image imageSource) { - ImageView croppedImageView = new ImageView(imageSource); - PixelReader pixelReader = imageSource.getPixelReader(); - - int imageWidth = (int) imageSource.getWidth(); - int imageHeight = (int) imageSource.getHeight(); - int minX = imageWidth, minY = imageHeight, maxX = 0, maxY = 0; - - // Find the first and last pixels that are not transparent to create a bounding viewport - for (int readY = 0; readY < imageHeight; readY++) { - for (int readX = 0; readX < imageWidth; readX++) { - Color pixelColor = pixelReader.getColor(readX, readY); - - if (!pixelColor.equals(Color.TRANSPARENT)) { - if (readX < minX) - minX = readX; - if (readX > maxX) - maxX = readX; - - if (readY < minY) - minY = readY; - if (readY > maxY) - maxY = readY; - } - } - } - - if (maxX - minX <= 0 || maxY - minY <= 0) - return new WritableImage(1, 1); - - // Create a viewport to clip the image using snapshot - Rectangle2D viewPort = new Rectangle2D(minX, minY, maxX - minX, maxY - minY); - SnapshotParameters parameter = new SnapshotParameters(); - parameter.setViewport(viewPort); - parameter.setFill(Color.TRANSPARENT); - - return croppedImageView.snapshot(parameter, null); - } - - public static Image composePreview(StackPane compositeTokenPane, Color bgColor, ImageView portraitImageView, ImageView maskImageView, ImageView overlayImageView, boolean useAsBase, - boolean clipImage) { - // Process layout as maskImage may have changed size if the overlay was changed - compositeTokenPane.layout(); - SnapshotParameters parameter = new SnapshotParameters(); - Image finalImage = null; - Group blend; - - if (clipImage) { - // We need to clip the portrait image first then blend the overlay image over it - // We will first get a snapshot of the portrait equal to the mask overlay image width/height - double x, y, width, height; - - x = maskImageView.getParent().getLayoutX(); - y = maskImageView.getParent().getLayoutY(); - width = maskImageView.getFitWidth(); - height = maskImageView.getFitHeight(); - - Rectangle2D viewPort = new Rectangle2D(x, y, width, height); - Rectangle2D maskViewPort = new Rectangle2D(1, 1, width, height); - WritableImage newImage = new WritableImage((int) width, (int) height); - WritableImage newMaskImage = new WritableImage((int) width, (int) height); - - ImageView overlayCopyImageView = new ImageView(); - ImageView clippedImageView = new ImageView(); - - parameter.setViewport(viewPort); - parameter.setFill(bgColor); - portraitImageView.snapshot(parameter, newImage); - - parameter.setViewport(maskViewPort); - parameter.setFill(Color.TRANSPARENT); - maskImageView.setVisible(true); - maskImageView.snapshot(parameter, newMaskImage); - maskImageView.setVisible(false); - - clippedImageView.setFitWidth(width); - clippedImageView.setFitHeight(height); - clippedImageView.setImage(clipImageWithMask(newImage, newMaskImage)); - - // Our masked portrait image is now stored in clippedImageView, lets now blend the overlay image over it - // We'll create a temporary group to hold our temporary ImageViews's and blend them and take a snapshot - overlayCopyImageView.setImage(overlayImageView.getImage()); - overlayCopyImageView.setFitWidth(overlayImageView.getFitWidth()); - overlayCopyImageView.setFitHeight(overlayImageView.getFitHeight()); - overlayCopyImageView.setOpacity(overlayImageView.getOpacity()); - - if (useAsBase) { - blend = new Group(overlayCopyImageView, clippedImageView); - } else { - blend = new Group(clippedImageView, overlayCopyImageView); - } - - // Last, we'll clean up any excess transparent edges by cropping it - finalImage = autoCropImage(blend.snapshot(parameter, null)); - } else { - parameter.setFill(Color.TRANSPARENT); - finalImage = autoCropImage(compositeTokenPane.snapshot(parameter, null)); - } - - return finalImage; - } - - public static double getScaleXRatio(ImageView imageView) { - return imageView.getBoundsInParent().getWidth() / imageView.getImage().getWidth(); - } - - public static double getScaleYRatio(ImageView imageView) { - return imageView.getBoundsInParent().getHeight() / imageView.getImage().getHeight(); - } - - /* - * This is for Legacy support but can cause magenta bleed on edges if there is transparency overlap. The preferred overlay storage is now PhotoShop PSD format with layer 1 containing the mask and - * layer 2 containing the image - */ - private static Image processMagenta(Image inputImage, int colorThreshold, boolean overlayWanted) { - int imageWidth = (int) inputImage.getWidth(); - int imageHeight = (int) inputImage.getHeight(); - - WritableImage outputImage = new WritableImage(imageWidth, imageHeight); - PixelReader pixelReader = inputImage.getPixelReader(); - PixelWriter pixelWriter = outputImage.getPixelWriter(); - - for (int readY = 0; readY < imageHeight; readY++) { - for (int readX = 0; readX < imageWidth; readX++) { - Color pixelColor = pixelReader.getColor(readX, readY); - - if (isMagenta(pixelColor, COLOR_THRESHOLD) == overlayWanted) - pixelWriter.setColor(readX, readY, Color.TRANSPARENT); - else - pixelWriter.setColor(readX, readY, pixelColor); - - } - } - - return outputImage; - } - - // Using some fudge factor... - private static boolean isMagenta(Color color, int fudge) { - if (color.equals(Color.MAGENTA)) - return true; - - double r = color.getRed(); - double g = color.getGreen(); - double b = color.getBlue(); - - if (Math.abs(r - b) > fudge) - return false; - - if (g > r - fudge || g > b - fudge) - return false; - - return true; - } - - public static String getFileType(File imageFile) { - if (FilenameUtils.getExtension(imageFile.getName()).toLowerCase().equals("psd")) { - return "Adobe Photoshop Image"; - } else { - return FilenameUtils.getExtension(imageFile.getName()).toUpperCase() + " File"; - } - } - - /* - * These are the file types supported by TokenTool - */ - public static final String[] SUPPORTED_FILE_FILTER_ARRAY = new String[] { ".psd", ".png", ".gif", ".jpg", ".jpeg", ".bmp" }; - public static final IOFileFilter SUPPORTED_FILE_FILTER = new SuffixFileFilter(SUPPORTED_FILE_FILTER_ARRAY); - - public static final List GET_EXTENSION_FILTERS() { - List extensionFilters = new ArrayList(); - extensionFilters.add(new ExtensionFilter("All Images", "*.psd", "*.png", "*.gif", "*.jpg", "*.jpeg", "*.bmp")); - extensionFilters.add(new ExtensionFilter("PSD Files", "*.psd")); - extensionFilters.add(new ExtensionFilter("PNG Files", "*.png")); - extensionFilters.add(new ExtensionFilter("JPG Files", "*.jpg")); - extensionFilters.add(new ExtensionFilter("JPEG Files", "*.jpeg")); - extensionFilters.add(new ExtensionFilter("BMP Files", "*.bmp")); - - return extensionFilters; - } - - /* - * These are the supported image types used in the new Image class - */ - public static final FilenameFilter SUPPORTED_IMAGE_FILE_FILTER = new FilenameFilter() { - public boolean accept(File dir, String name) { - name = name.toLowerCase(); - - return name.endsWith(".png") || name.endsWith(".gif") || name.endsWith(".jpg") || name.endsWith(".jpeg") || name.endsWith(".bmp"); - } - }; - - /* - * PSD Support using com.twelvemonkeys.imageio - */ - public static final FilenameFilter PSD_FILE_FILTER = new FilenameFilter() { - public boolean accept(File dir, String name) { - return name.toLowerCase().endsWith(".psd"); - } - }; - - /* - * These are the supported types used in the new Image class - */ - public static final FilenameFilter SUPPORTED_FILENAME_FILTER = new FilenameFilter() { - public boolean accept(File dir, String name) { - name = name.toLowerCase(); - - return name.endsWith(".psd") || name.endsWith(".png") || name.endsWith(".gif") || name.endsWith(".jpg") || name.endsWith(".jpeg") || name.endsWith(".bmp"); - } - }; + private static final Logger log = LogManager.getLogger(ImageUtil.class); + + private static final int THUMB_SIZE = 100; + private static final int COLOR_THRESHOLD = 1; + + public static ImageView getOverlayThumb(ImageView thumbView, Path filePath) throws IOException { + return getImage(thumbView, filePath, true, THUMB_SIZE); + } + + public static ImageView getOverlayImage(ImageView thumbView, Path overlayFileURI) + throws IOException { + return getImage(thumbView, overlayFileURI, true, 0); + } + + public static ImageView getMaskImage(ImageView thumbView, Path overlayFileURI) + throws IOException { + return getImage(thumbView, overlayFileURI, false, 0); + } + + private static ImageView getImage( + ImageView thumbView, final Path filePath, final boolean overlayWanted, final int THUMB_SIZE) + throws IOException { + Image thumb = null; + String fileURL = filePath.toUri().toURL().toString(); + + if (THUMB_SIZE > 0) { + thumbView.setFitWidth(THUMB_SIZE); + thumbView.setPreserveRatio(true); + } + + if (ImageUtil.SUPPORTED_IMAGE_FILE_FILTER.accept(null, fileURL)) { + thumb = processMagenta(new Image(fileURL), COLOR_THRESHOLD, overlayWanted); + } else if (ImageUtil.PSD_FILE_FILTER.accept(null, fileURL)) { + ImageInputStream is = null; + PSDImageReader reader = null; + int imageIndex = 1; + + // Mask layer should always be layer 1 and overlay image on layer 2. Note, layer 0 will be a + // combined layer composite + if (overlayWanted) imageIndex = 2; + + File file = filePath.toFile(); + + try { + is = ImageIO.createImageInputStream(file); + if (is == null || is.length() == 0) { + log.info("Image from file " + file.getAbsolutePath() + " is null"); + } + + Iterator iterator = ImageIO.getImageReaders(is); + if (iterator == null || !iterator.hasNext()) { + throw new IOException("Image file format not supported by ImageIO: " + filePath); + } + + reader = (PSDImageReader) iterator.next(); + reader.setInput(is); + BufferedImage thumbBI; + thumbBI = reader.read(imageIndex); + + if (thumbBI != null) { + int layerIndex = 0; + if (overlayWanted) layerIndex = 1; + + IIOMetadata metadata = reader.getImageMetadata(0); + IIOMetadataNode root = + (IIOMetadataNode) metadata.getAsTree(PSDMetadata.NATIVE_METADATA_FORMAT_NAME); + NodeList layerInfos = root.getElementsByTagName("LayerInfo"); + + // Layer index corresponds to imageIndex - 1 in the reader + IIOMetadataNode layerInfo = (IIOMetadataNode) layerInfos.item(layerIndex); + + // Get the width & height of the Mask layer so we can create the overlay the same size + int width = reader.getWidth(0); + int height = reader.getHeight(0); + + // Get layer offsets, PhotoShop PSD layers can have different widths/heights and all + // images start at 0,0 with a layer offset applied + int x = Math.max(Integer.parseInt(layerInfo.getAttribute("left")), 0); + int y = Math.max(Integer.parseInt(layerInfo.getAttribute("top")), 0); + + // Lets pad the overlay with transparency to make it the same size as the PSD canvas size + thumb = resizeCanvas(SwingFXUtils.toFXImage(thumbBI, null), width, height, x, y); + } + } catch (Exception e) { + log.error("Processing: " + file.getAbsolutePath(), e); + } finally { + // Dispose reader in finally block to avoid memory leaks + reader.dispose(); + is.close(); + } + } + + thumbView.setImage(thumb); + + return thumbView; + } + + public static Image resizeCanvas(Image imageSource, double newWidth, double newHeight) { + int offsetX = (int) ((newWidth - imageSource.getWidth()) / 2); + int offsetY = (int) ((newHeight - imageSource.getHeight()) / 2); + + return resizeCanvas(imageSource, (int) newWidth, (int) newHeight, offsetX, offsetY); + } + + /* + * Resize the overall image width/height without scaling the actual image, eg resize the canvas + */ + public static Image resizeCanvas( + Image imageSource, int newWidth, int newHeight, int offsetX, int offsetY) { + int sourceWidth = (int) imageSource.getWidth(); + int sourceHeight = (int) imageSource.getHeight(); + + // No work needed here... + if (sourceWidth == newWidth && sourceHeight == newHeight) return imageSource; + + WritableImage outputImage = new WritableImage(newWidth, newHeight); + PixelReader pixelReader = imageSource.getPixelReader(); + PixelWriter pixelWriter = outputImage.getPixelWriter(); + WritablePixelFormat format = WritablePixelFormat.getIntArgbInstance(); + + int[] buffer = new int[sourceWidth * sourceHeight]; + pixelReader.getPixels(0, 0, sourceWidth, sourceHeight, format, buffer, 0, sourceWidth); + pixelWriter.setPixels( + offsetX, offsetY, sourceWidth, sourceHeight, format, buffer, 0, sourceWidth); + + return outputImage; + } + + /* + * Resize the overall image width/height scaled to the target width/height + */ + public static Image scaleImage( + Image source, double targetWidth, double targetHeight, boolean preserveRatio) { + ImageView imageView = new ImageView(source); + imageView.setPreserveRatio(preserveRatio); + imageView.setFitWidth(targetWidth); + imageView.setFitHeight(targetHeight); + return imageView.snapshot(null, null); + } + + /* + * Return the intersection between the source image and the mask. Note, the mask does not need to be magenta anymore, any non-transparent pixel is considering a mask + */ + private static Image clipImageWithMask(Image imageSource, Image imageMask) { + int imageWidth = (int) imageMask.getWidth(); + int imageHeight = (int) imageMask.getHeight(); + + WritableImage outputImage = new WritableImage(imageWidth, imageHeight); + PixelReader pixelReader_Mask = imageMask.getPixelReader(); + PixelReader pixelReader_Source = imageSource.getPixelReader(); + PixelWriter pixelWriter = outputImage.getPixelWriter(); + + for (int readY = 0; readY < imageHeight; readY++) { + for (int readX = 0; readX < imageWidth; readX++) { + Color pixelColor = pixelReader_Mask.getColor(readX, readY); + + if (pixelColor.equals(Color.TRANSPARENT)) + pixelWriter.setColor(readX, readY, pixelReader_Source.getColor(readX, readY)); + } + } + + return outputImage; + } + + /* + * Crop image to smallest width/height based on transparency + */ + private static Image autoCropImage(Image imageSource) { + return autoCropImage(imageSource, Color.TRANSPARENT, null); + } + + public static Image autoCropImage( + Image imageSource, Color backgroundColor, Image backgroundImage) { + ImageView croppedImageView = new ImageView(imageSource); + PixelReader pixelReader = imageSource.getPixelReader(); + + int imageWidth = (int) imageSource.getWidth(); + int imageHeight = (int) imageSource.getHeight(); + int minX = imageWidth, minY = imageHeight, maxX = 0, maxY = 0; + + // Find the first and last pixels that are not transparent to create a bounding viewport + for (int readY = 0; readY < imageHeight; readY++) { + for (int readX = 0; readX < imageWidth; readX++) { + Color pixelColor = pixelReader.getColor(readX, readY); + + if (!pixelColor.equals(Color.TRANSPARENT)) { + if (readX < minX) minX = readX; + if (readX > maxX) maxX = readX; + + if (readY < minY) minY = readY; + if (readY > maxY) maxY = readY; + } + } + } + + if (maxX - minX <= 0 || maxY - minY <= 0) return new WritableImage(1, 1); + + // Create a viewport to clip the image using snapshot + Rectangle2D viewPort = new Rectangle2D(minX, minY, maxX - minX, maxY - minY); + SnapshotParameters parameter = new SnapshotParameters(); + parameter.setViewport(viewPort); + parameter.setFill(backgroundColor); + + if (backgroundImage != null) { + return new Group(new ImageView(backgroundImage), croppedImageView).snapshot(parameter, null); + } else { + return croppedImageView.snapshot(parameter, null); + } + } + + public static Image composePreview( + StackPane compositeTokenPane, + ImageView backgroundImageView, + Color bgColor, + ImageView portraitImageView, + ImageView maskImageView, + ImageView overlayImageView, + boolean useAsBase, + boolean clipImage) { + + // Process layout as maskImage may have changed size if the overlay was changed + compositeTokenPane.layout(); + SnapshotParameters parameter = new SnapshotParameters(); + Image finalImage = null; + Group blend; + + // check if there is a mask image + if (maskImageView.getFitWidth() <= 0 || maskImageView.getFitHeight() <= 0) clipImage = false; + + if (clipImage) { + // We need to clip the portrait image first then blend the overlay image over it + // We will first get a snapshot of the portrait equal to the mask overlay image width/height + // We will then get a snapshot of the background image, if any. + double x, y, width, height; + + x = maskImageView.getParent().getLayoutX(); + y = maskImageView.getParent().getLayoutY(); + width = maskImageView.getFitWidth(); + height = maskImageView.getFitHeight(); + + Rectangle2D viewPort = new Rectangle2D(x, y, width, height); + Rectangle2D maskViewPort = new Rectangle2D(1, 1, width, height); + WritableImage newBackgroundImage = new WritableImage((int) width, (int) height); + WritableImage newImage = new WritableImage((int) width, (int) height); + WritableImage newMaskImage = new WritableImage((int) width, (int) height); + + ImageView newBackgroundImageView = new ImageView(); + ImageView overlayCopyImageView = new ImageView(); + ImageView clippedImageView = new ImageView(); + + parameter.setViewport(viewPort); + parameter.setFill(bgColor); + backgroundImageView.snapshot(parameter, newBackgroundImage); + + parameter.setFill(Color.TRANSPARENT); + portraitImageView.snapshot(parameter, newImage); + + parameter.setViewport(maskViewPort); + maskImageView.setVisible(true); + maskImageView.snapshot(parameter, newMaskImage); + maskImageView.setVisible(false); + + clippedImageView.setFitWidth(width); + clippedImageView.setFitHeight(height); + clippedImageView.setImage(clipImageWithMask(newImage, newMaskImage)); + newBackgroundImageView.setImage(clipImageWithMask(newBackgroundImage, newMaskImage)); + + // Our masked portrait image is now stored in clippedImageView, lets now blend the overlay + // image over it + // We'll create a temporary group to hold our temporary ImageViews's and blend them and take a + // snapshot + overlayCopyImageView.setImage(overlayImageView.getImage()); + overlayCopyImageView.setFitWidth(overlayImageView.getFitWidth()); + overlayCopyImageView.setFitHeight(overlayImageView.getFitHeight()); + overlayCopyImageView.setOpacity(overlayImageView.getOpacity()); + + if (useAsBase) { + blend = new Group(newBackgroundImageView, overlayCopyImageView, clippedImageView); + } else { + blend = new Group(newBackgroundImageView, clippedImageView, overlayCopyImageView); + } + + // Last, we'll clean up any excess transparent edges by cropping it + finalImage = autoCropImage(blend.snapshot(parameter, null)); + } else { + parameter.setFill(Color.TRANSPARENT); + finalImage = autoCropImage(compositeTokenPane.snapshot(parameter, null)); + } + + return finalImage; + } + + public static double getScaleXRatio(ImageView imageView) { + return imageView.getBoundsInParent().getWidth() / imageView.getImage().getWidth(); + } + + public static double getScaleYRatio(ImageView imageView) { + return imageView.getBoundsInParent().getHeight() / imageView.getImage().getHeight(); + } + + /* + * This is for Legacy support but can cause magenta bleed on edges if there is transparency overlap. The preferred overlay storage is now PhotoShop PSD format with layer 1 containing the mask and + * layer 2 containing the image + */ + private static Image processMagenta(Image inputImage, int colorThreshold, boolean overlayWanted) { + int imageWidth = (int) inputImage.getWidth(); + int imageHeight = (int) inputImage.getHeight(); + + WritableImage outputImage = new WritableImage(imageWidth, imageHeight); + PixelReader pixelReader = inputImage.getPixelReader(); + PixelWriter pixelWriter = outputImage.getPixelWriter(); + + for (int readY = 0; readY < imageHeight; readY++) { + for (int readX = 0; readX < imageWidth; readX++) { + Color pixelColor = pixelReader.getColor(readX, readY); + + if (isMagenta(pixelColor, COLOR_THRESHOLD) == overlayWanted) + pixelWriter.setColor(readX, readY, Color.TRANSPARENT); + else pixelWriter.setColor(readX, readY, pixelColor); + } + } + + return outputImage; + } + + // Using some fudge factor... + private static boolean isMagenta(Color color, int fudge) { + if (color.equals(Color.MAGENTA)) return true; + + double r = color.getRed(); + double g = color.getGreen(); + double b = color.getBlue(); + + if (Math.abs(r - b) > fudge) return false; + + if (g > r - fudge || g > b - fudge) return false; + + return true; + } + + public static String getFileType(File imageFile) { + if (FilenameUtils.getExtension(imageFile.getName()).toLowerCase().equals("psd")) { + return "Adobe Photoshop " + I18N.getString("imageUtil.filetype.label.image"); + } else { + return FilenameUtils.getExtension(imageFile.getName()).toUpperCase() + + I18N.getString("imageUtil.filetype.label.extension"); + } + } + + public static byte[] imageToBytes(BufferedImage image) throws IOException { + return imageToBytes(image, "png"); + } + + public static byte[] imageToBytes(BufferedImage image, String format) throws IOException { + ByteArrayOutputStream outStream = new ByteArrayOutputStream(10000); + ImageIO.write(image, format, outStream); + + return outStream.toByteArray(); + } + + /* + * These are the file types supported by TokenTool + */ + public static final String[] SUPPORTED_FILE_FILTER_ARRAY = + new String[] {".psd", ".png", ".gif", ".jpg", ".jpeg", ".bmp"}; + public static final IOFileFilter SUPPORTED_FILE_FILTER = + new SuffixFileFilter(SUPPORTED_FILE_FILTER_ARRAY); + public static final ExtensionFilter SUPPORTED_PDF_EXTENSION_FILTER = + new ExtensionFilter("PDF Files", "*.pdf"); + + public static final List GET_EXTENSION_FILTERS() { + List extensionFilters = new ArrayList(); + extensionFilters.add( + new ExtensionFilter( + I18N.getString("imageUtil.filetype.label.all_images"), + "*.psd", + "*.png", + "*.gif", + "*.jpg", + "*.jpeg", + "*.bmp")); + extensionFilters.add( + new ExtensionFilter("PSD" + I18N.getString("imageUtil.filetype.label.files"), "*.psd")); + extensionFilters.add( + new ExtensionFilter("PNG" + I18N.getString("imageUtil.filetype.label.files"), "*.png")); + extensionFilters.add( + new ExtensionFilter("JPG" + I18N.getString("imageUtil.filetype.label.files"), "*.jpg")); + extensionFilters.add( + new ExtensionFilter("JPEG" + I18N.getString("imageUtil.filetype.label.files"), "*.jpeg")); + extensionFilters.add( + new ExtensionFilter("BMP" + I18N.getString("imageUtil.filetype.label.files"), "*.bmp")); + + return extensionFilters; + } + + /* + * These are the supported image types used in the new Image class + */ + public static final FilenameFilter SUPPORTED_IMAGE_FILE_FILTER = + new FilenameFilter() { + public boolean accept(File dir, String name) { + name = name.toLowerCase(); + + return name.endsWith(".png") + || name.endsWith(".gif") + || name.endsWith(".jpg") + || name.endsWith(".jpeg") + || name.endsWith(".bmp"); + } + }; + + /* + * PSD Support using com.twelvemonkeys.imageio + */ + public static final FilenameFilter PSD_FILE_FILTER = + new FilenameFilter() { + public boolean accept(File dir, String name) { + return name.toLowerCase().endsWith(".psd"); + } + }; + + /* + * These are the supported types used in the new Image class + */ + public static final FilenameFilter SUPPORTED_FILENAME_FILTER = + new FilenameFilter() { + public boolean accept(File dir, String name) { + name = name.toLowerCase(); + + return name.endsWith(".psd") + || name.endsWith(".png") + || name.endsWith(".gif") + || name.endsWith(".jpg") + || name.endsWith(".jpeg") + || name.endsWith(".bmp"); + } + }; } diff --git a/src/main/java/net/rptools/tokentool/util/StageResizeMoveUtil.java b/src/main/java/net/rptools/tokentool/util/StageResizeMoveUtil.java index 613afd0..d8491e6 100644 --- a/src/main/java/net/rptools/tokentool/util/StageResizeMoveUtil.java +++ b/src/main/java/net/rptools/tokentool/util/StageResizeMoveUtil.java @@ -1,10 +1,16 @@ /* - * This software Copyright by the RPTools.net development team, and licensed under the Affero GPL Version 3 or, at your option, any later version. + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. * - * TokenTool Source Code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * TokenTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * - * You should have received a copy of the GNU Affero General Public License * along with this source Code. If not, please visit and specifically the Affero license text - * at . + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . */ package net.rptools.tokentool.util; @@ -15,175 +21,190 @@ import javafx.scene.Node; import javafx.scene.Parent; import javafx.scene.Scene; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; import javafx.scene.input.MouseEvent; import javafx.stage.Stage; public class StageResizeMoveUtil { - public static void addResizeListener(Stage stage) { - addResizeListener(stage, 0, 0, Double.MAX_VALUE, Double.MAX_VALUE); - } - - public static void addResizeListener(Stage stage, double minWidth, double minHeight, double maxWidth, double maxHeight) { - ResizeListener resizeListener = new ResizeListener(stage); - stage.getScene().addEventHandler(MouseEvent.MOUSE_MOVED, resizeListener); - stage.getScene().addEventHandler(MouseEvent.MOUSE_PRESSED, resizeListener); - stage.getScene().addEventHandler(MouseEvent.MOUSE_DRAGGED, resizeListener); - stage.getScene().addEventHandler(MouseEvent.MOUSE_EXITED, resizeListener); - stage.getScene().addEventHandler(MouseEvent.MOUSE_EXITED_TARGET, resizeListener); - - resizeListener.setMinWidth(minWidth); - resizeListener.setMinHeight(minHeight); - resizeListener.setMaxWidth(maxWidth); - resizeListener.setMaxHeight(maxHeight); - - ObservableList children = stage.getScene().getRoot().getChildrenUnmodifiable(); - for (Node child : children) { - addListenerDeeply(child, resizeListener); - } - } - - private static void addListenerDeeply(Node node, EventHandler listener) { - node.addEventHandler(MouseEvent.MOUSE_MOVED, listener); - node.addEventHandler(MouseEvent.MOUSE_PRESSED, listener); - node.addEventHandler(MouseEvent.MOUSE_DRAGGED, listener); - node.addEventHandler(MouseEvent.MOUSE_EXITED, listener); - node.addEventHandler(MouseEvent.MOUSE_EXITED_TARGET, listener); - if (node instanceof Parent) { - Parent parent = (Parent) node; - ObservableList children = parent.getChildrenUnmodifiable(); - for (Node child : children) { - addListenerDeeply(child, listener); - } - } - } - - static class ResizeListener implements EventHandler { - private Stage stage; - private Cursor cursorEvent = Cursor.DEFAULT; - private int border = 10; - private double startX = 0; - private double startY = 0; - - // Max and min sizes for controlled stage - private double minWidth; - private double maxWidth; - private double minHeight; - private double maxHeight; - - // For stage movement - private double xOffset; - private double yOffset; - - public ResizeListener(Stage stage) { - this.stage = stage; - } - - public void setMinWidth(double minWidth) { - this.minWidth = minWidth; - } - - public void setMaxWidth(double maxWidth) { - this.maxWidth = maxWidth; - } - - public void setMinHeight(double minHeight) { - this.minHeight = minHeight; - } - - public void setMaxHeight(double maxHeight) { - this.maxHeight = maxHeight; - } - - @Override - public void handle(MouseEvent mouseEvent) { - EventType mouseEventType = mouseEvent.getEventType(); - Scene scene = stage.getScene(); - - double mouseEventX = mouseEvent.getSceneX(), - mouseEventY = mouseEvent.getSceneY(), - sceneWidth = scene.getWidth(), - sceneHeight = scene.getHeight(); - - if (MouseEvent.MOUSE_MOVED.equals(mouseEventType)) { - if (mouseEventX < border && mouseEventY < border) { - cursorEvent = Cursor.NW_RESIZE; - } else if (mouseEventX < border && mouseEventY > sceneHeight - border) { - cursorEvent = Cursor.SW_RESIZE; - } else if (mouseEventX > sceneWidth - border && mouseEventY < border) { - cursorEvent = Cursor.NE_RESIZE; - } else if (mouseEventX > sceneWidth - border && mouseEventY > sceneHeight - border) { - cursorEvent = Cursor.SE_RESIZE; - } else if (mouseEventX < border) { - cursorEvent = Cursor.W_RESIZE; - } else if (mouseEventX > sceneWidth - border) { - cursorEvent = Cursor.E_RESIZE; - } else if (mouseEventY < border) { - cursorEvent = Cursor.N_RESIZE; - } else if (mouseEventY > sceneHeight - border) { - cursorEvent = Cursor.S_RESIZE; - } else { - cursorEvent = Cursor.MOVE; - } - scene.setCursor(cursorEvent); - } else if (MouseEvent.MOUSE_EXITED.equals(mouseEventType) || MouseEvent.MOUSE_EXITED_TARGET.equals(mouseEventType)) { - scene.setCursor(Cursor.DEFAULT); - } else if (MouseEvent.MOUSE_PRESSED.equals(mouseEventType)) { - startX = stage.getWidth() - mouseEventX; - startY = stage.getHeight() - mouseEventY; - - if (Cursor.MOVE.equals(cursorEvent)) { - xOffset = stage.getX() - mouseEvent.getScreenX(); - yOffset = stage.getY() - mouseEvent.getScreenY(); - } - } else if (MouseEvent.MOUSE_DRAGGED.equals(mouseEventType)) { - if (!Cursor.DEFAULT.equals(cursorEvent) && !Cursor.MOVE.equals(cursorEvent)) { - if (!Cursor.W_RESIZE.equals(cursorEvent) && !Cursor.E_RESIZE.equals(cursorEvent)) { - double minHeight = stage.getMinHeight() > (border * 2) ? stage.getMinHeight() : (border * 2); - if (Cursor.NW_RESIZE.equals(cursorEvent) || Cursor.N_RESIZE.equals(cursorEvent) - || Cursor.NE_RESIZE.equals(cursorEvent)) { - if (stage.getHeight() > minHeight || mouseEventY < 0) { - setStageHeight(stage.getY() - mouseEvent.getScreenY() + stage.getHeight()); - stage.setY(mouseEvent.getScreenY()); - } - } else { - if (stage.getHeight() > minHeight || mouseEventY + startY - stage.getHeight() > 0) { - setStageHeight(mouseEventY + startY); - } - } - } - - if (!Cursor.N_RESIZE.equals(cursorEvent) && !Cursor.S_RESIZE.equals(cursorEvent)) { - double minWidth = stage.getMinWidth() > (border * 2) ? stage.getMinWidth() : (border * 2); - if (Cursor.NW_RESIZE.equals(cursorEvent) || Cursor.W_RESIZE.equals(cursorEvent) - || Cursor.SW_RESIZE.equals(cursorEvent)) { - if (stage.getWidth() > minWidth || mouseEventX < 0) { - setStageWidth(stage.getX() - mouseEvent.getScreenX() + stage.getWidth()); - stage.setX(mouseEvent.getScreenX()); - } - } else { - if (stage.getWidth() > minWidth || mouseEventX + startX - stage.getWidth() > 0) { - setStageWidth(mouseEventX + startX); - } - } - } - } else if (Cursor.MOVE.equals(cursorEvent)) { - stage.setX(mouseEvent.getScreenX() + xOffset); - stage.setY(mouseEvent.getScreenY() + yOffset); - } - } - } - - private void setStageWidth(double width) { - width = Math.min(width, maxWidth); - width = Math.max(width, minWidth); - stage.setWidth(width); - } - - private void setStageHeight(double height) { - height = Math.min(height, maxHeight); - height = Math.max(height, minHeight); - stage.setHeight(height); - } - - } + public static void addResizeListener(Stage stage) { + addResizeListener(stage, 0, 0, Double.MAX_VALUE, Double.MAX_VALUE); + + stage.addEventHandler( + KeyEvent.KEY_RELEASED, + (KeyEvent event) -> { + if (event.getCode() == KeyCode.ESCAPE) { + stage.close(); + } + }); + } + + public static void addResizeListener( + Stage stage, double minWidth, double minHeight, double maxWidth, double maxHeight) { + ResizeListener resizeListener = new ResizeListener(stage); + stage.getScene().addEventHandler(MouseEvent.MOUSE_MOVED, resizeListener); + stage.getScene().addEventHandler(MouseEvent.MOUSE_PRESSED, resizeListener); + stage.getScene().addEventHandler(MouseEvent.MOUSE_DRAGGED, resizeListener); + stage.getScene().addEventHandler(MouseEvent.MOUSE_EXITED, resizeListener); + stage.getScene().addEventHandler(MouseEvent.MOUSE_EXITED_TARGET, resizeListener); + + resizeListener.setMinWidth(minWidth); + resizeListener.setMinHeight(minHeight); + resizeListener.setMaxWidth(maxWidth); + resizeListener.setMaxHeight(maxHeight); + + ObservableList children = stage.getScene().getRoot().getChildrenUnmodifiable(); + for (Node child : children) { + addListenerDeeply(child, resizeListener); + } + } + + private static void addListenerDeeply(Node node, EventHandler listener) { + node.addEventHandler(MouseEvent.MOUSE_MOVED, listener); + node.addEventHandler(MouseEvent.MOUSE_PRESSED, listener); + node.addEventHandler(MouseEvent.MOUSE_DRAGGED, listener); + node.addEventHandler(MouseEvent.MOUSE_EXITED, listener); + node.addEventHandler(MouseEvent.MOUSE_EXITED_TARGET, listener); + if (node instanceof Parent) { + Parent parent = (Parent) node; + ObservableList children = parent.getChildrenUnmodifiable(); + for (Node child : children) { + addListenerDeeply(child, listener); + } + } + } + + static class ResizeListener implements EventHandler { + private Stage stage; + private Cursor cursorEvent = Cursor.DEFAULT; + private int border = 10; + private double startX = 0; + private double startY = 0; + + // Max and min sizes for controlled stage + private double minWidth; + private double maxWidth; + private double minHeight; + private double maxHeight; + + // For stage movement + private double xOffset; + private double yOffset; + + public ResizeListener(Stage stage) { + this.stage = stage; + } + + public void setMinWidth(double minWidth) { + this.minWidth = minWidth; + } + + public void setMaxWidth(double maxWidth) { + this.maxWidth = maxWidth; + } + + public void setMinHeight(double minHeight) { + this.minHeight = minHeight; + } + + public void setMaxHeight(double maxHeight) { + this.maxHeight = maxHeight; + } + + @Override + public void handle(MouseEvent mouseEvent) { + EventType mouseEventType = mouseEvent.getEventType(); + Scene scene = stage.getScene(); + + double mouseEventX = mouseEvent.getSceneX(), + mouseEventY = mouseEvent.getSceneY(), + sceneWidth = scene.getWidth(), + sceneHeight = scene.getHeight(); + + if (MouseEvent.MOUSE_MOVED.equals(mouseEventType)) { + if (mouseEventX < border && mouseEventY < border) { + cursorEvent = Cursor.NW_RESIZE; + } else if (mouseEventX < border && mouseEventY > sceneHeight - border) { + cursorEvent = Cursor.SW_RESIZE; + } else if (mouseEventX > sceneWidth - border && mouseEventY < border) { + cursorEvent = Cursor.NE_RESIZE; + } else if (mouseEventX > sceneWidth - border && mouseEventY > sceneHeight - border) { + cursorEvent = Cursor.SE_RESIZE; + } else if (mouseEventX < border) { + cursorEvent = Cursor.W_RESIZE; + } else if (mouseEventX > sceneWidth - border) { + cursorEvent = Cursor.E_RESIZE; + } else if (mouseEventY < border) { + cursorEvent = Cursor.N_RESIZE; + } else if (mouseEventY > sceneHeight - border) { + cursorEvent = Cursor.S_RESIZE; + } else { + cursorEvent = Cursor.MOVE; + } + scene.setCursor(cursorEvent); + } else if (MouseEvent.MOUSE_EXITED.equals(mouseEventType) + || MouseEvent.MOUSE_EXITED_TARGET.equals(mouseEventType)) { + scene.setCursor(Cursor.DEFAULT); + } else if (MouseEvent.MOUSE_PRESSED.equals(mouseEventType)) { + startX = stage.getWidth() - mouseEventX; + startY = stage.getHeight() - mouseEventY; + + if (Cursor.MOVE.equals(cursorEvent)) { + xOffset = stage.getX() - mouseEvent.getScreenX(); + yOffset = stage.getY() - mouseEvent.getScreenY(); + } + } else if (MouseEvent.MOUSE_DRAGGED.equals(mouseEventType)) { + if (!Cursor.DEFAULT.equals(cursorEvent) && !Cursor.MOVE.equals(cursorEvent)) { + if (!Cursor.W_RESIZE.equals(cursorEvent) && !Cursor.E_RESIZE.equals(cursorEvent)) { + double minHeight = + stage.getMinHeight() > (border * 2) ? stage.getMinHeight() : (border * 2); + if (Cursor.NW_RESIZE.equals(cursorEvent) + || Cursor.N_RESIZE.equals(cursorEvent) + || Cursor.NE_RESIZE.equals(cursorEvent)) { + if (stage.getHeight() > minHeight || mouseEventY < 0) { + setStageHeight(stage.getY() - mouseEvent.getScreenY() + stage.getHeight()); + stage.setY(mouseEvent.getScreenY()); + } + } else { + if (stage.getHeight() > minHeight || mouseEventY + startY - stage.getHeight() > 0) { + setStageHeight(mouseEventY + startY); + } + } + } + + if (!Cursor.N_RESIZE.equals(cursorEvent) && !Cursor.S_RESIZE.equals(cursorEvent)) { + double minWidth = + stage.getMinWidth() > (border * 2) ? stage.getMinWidth() : (border * 2); + if (Cursor.NW_RESIZE.equals(cursorEvent) + || Cursor.W_RESIZE.equals(cursorEvent) + || Cursor.SW_RESIZE.equals(cursorEvent)) { + if (stage.getWidth() > minWidth || mouseEventX < 0) { + setStageWidth(stage.getX() - mouseEvent.getScreenX() + stage.getWidth()); + stage.setX(mouseEvent.getScreenX()); + } + } else { + if (stage.getWidth() > minWidth || mouseEventX + startX - stage.getWidth() > 0) { + setStageWidth(mouseEventX + startX); + } + } + } + } else if (Cursor.MOVE.equals(cursorEvent)) { + stage.setX(mouseEvent.getScreenX() + xOffset); + stage.setY(mouseEvent.getScreenY() + yOffset); + } + } + } + + private void setStageWidth(double width) { + width = Math.min(width, maxWidth); + width = Math.max(width, minWidth); + stage.setWidth(width); + } + + private void setStageHeight(double height) { + height = Math.min(height, maxHeight); + height = Math.max(height, minHeight); + stage.setHeight(height); + } + } } diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml index f082a63..d2ff28a 100644 --- a/src/main/resources/log4j2.xml +++ b/src/main/resources/log4j2.xml @@ -7,13 +7,13 @@ - + - - + + - + \ No newline at end of file diff --git a/src/main/resources/net/rptools/tokentool/css/TokenTool.css b/src/main/resources/net/rptools/tokentool/css/TokenTool.css index df2fe8c..d23e2df 100644 --- a/src/main/resources/net/rptools/tokentool/css/TokenTool.css +++ b/src/main/resources/net/rptools/tokentool/css/TokenTool.css @@ -1,3 +1,8 @@ +/* For reference... + https://docs.oracle.com/javase/10/docs/api/javafx/scene/doc-files/cssref.html + http://www.oracle.com/technetwork/articles/javafx/javafx-css-159889.html +*/ + /* Add custom font */ @font-face { font-family: Horta; @@ -6,7 +11,7 @@ .hortaFont { -fx-font-family: "Horta"; - -fx-font-size: 20; + -fx-font-size: 20.0; } /* leaf is a PsuedoClass added via the controller to child-only nodes, e.g. overlays... */ @@ -96,33 +101,66 @@ } .add-folder-button { - -fx-min-width: 26px; - -fx-min-height: 24px; + -fx-min-width: 26.0px; + -fx-min-height: 24.0px; -fx-background-image: url("../image/folder.png"); - -fx-background-size: 20px 20px; + -fx-background-size: 20.0px 20.0px; -fx-background-repeat: no-repeat; -fx-background-position: center; } .add-overlay-button { - -fx-min-width: 26px; - -fx-min-height: 24px; + -fx-min-width: 26.0px; + -fx-min-height: 24.0px; -fx-background-image: url("../image/add.png"); - -fx-background-size: 20px 20px; + -fx-background-size: 20.0px 20.0px; -fx-background-repeat: no-repeat; -fx-background-position: center; } .delete-button { - -fx-min-width: 26px; - -fx-min-height: 24px; + -fx-min-width: 26.0px; + -fx-min-height: 24.0px; -fx-background-image: url("../image/trash.png"); - -fx-background-size: 20px 20px; + -fx-background-size: 20.0px 20.0px; -fx-background-repeat: no-repeat; -fx-background-position: center; } + /* CSS For Credits window */ .text-area, .text-area .viewport, .text-area .content { -fx-background-color: transparent ; +} + + +/* + * For PDF pagination + * https://stackoverflow.com/questions/46563798/javafx-pagination-button-sizes-and-style + */ +.pagination { + -fx-base: black; + -fx-arrow-button-gap: 5.0px; +} +.pagination .toggle-button, .mypage { + -fx-background-radius:15.0px; + -fx-padding: 0.0px; +} +.pagination .button{ + -fx-padding: 5.0 15.0 5.0 15.0px; +} +.pagination .label { + -fx-text-fill: white; +} +.pagination > .pagination-control > .control-box { + -fx-spacing: 10.0px; + -fx-font-size: 12.0; +} +.pagination > .pagination-control { + -fx-font-size: 14.0; +} +.pagination > .pagination-control > .control-box > .left-arrow-button, +.pagination > .pagination-control > .control-box > .right-arrow-button { + -fx-min-width: 40.0; + -fx-background-radius:15.0px; } \ No newline at end of file diff --git a/src/main/resources/net/rptools/tokentool/i18n/TokenTool.properties b/src/main/resources/net/rptools/tokentool/i18n/TokenTool.properties index 369304b..279be2d 100644 --- a/src/main/resources/net/rptools/tokentool/i18n/TokenTool.properties +++ b/src/main/resources/net/rptools/tokentool/i18n/TokenTool.properties @@ -1,10 +1,15 @@ #Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/) -# http://translatr.varunmalhotra.xyz/?ref=producthunt +# http://translatr.varunmalhotra.xyz + +AppSetup.dialog.install.overlays.confirmation = New overlays were installed. +AppSetup.dialog.install.overlays.confirmation.title = New Overlays Available! Credits.label.Contributors = Contributors Credits.label.Credits = Credits Credits.stage.title = About TokenTool +ImageGallery.stage.title = PDF Images + ManageOverlays.button.Restore_Default_Overlays = Restore Default Overlays ManageOverlays.dialog.delete.confirmation = Are you sure you want to delete ManageOverlays.dialog.delete.confirmation.overlays = \ overlays? @@ -26,22 +31,48 @@ ManageOverlays.label.Image_Type_Description = Image Type Description ManageOverlays.label.Overlays = Overlays ManageOverlays.stage.title = Manage Overlays +PdvViewer.stage.title = Select PDF + RegionSelector.button.Capture = Capture -TokenTool.save.filechooser.title = Save as Image -TokenTool.stage.title = TokenTool -TokenTool.treeview.caching = Caching +TokenTool.dialog.confirmation.header = Confirmation +TokenTool.dialog.reset.confirmation.text = This will reset all saved UI settings back to default, are you sure? +TokenTool.dialog.reset.confirmation.title = Reset Settings? +TokenTool.openBackgroundImage.filechooser.title = Select Image +TokenTool.openPDF.filechooser.title = Select PDF +TokenTool.openPortraitImage.filechooser.title = Select Image +TokenTool.save.filechooser.title = Save as Image +TokenTool.stage.title = TokenTool +TokenTool.treeview.caching = Caching + +controls.base.text = Use as a token base +controls.dragAsTokenCheckbox.text = Drag as .rptok Token +controls.dragAsTokenCheckbox.tooltip = If selected, dragging the token image will create a MapTool compatible .rptok token using base image as a Portrait image. +controls.filenameSuffixLabel.text = Filename Suffix # +controls.filenameSuffixLabel.tooltip = Append a number to the file name that auto increments for each save of the token. e.g. Orc-1, Orc-2, Orc-3. +controls.layers.menu.item.background = Background +controls.layers.menu.item.overlay = Overlay +controls.layers.menu.item.portrait = Portrait +controls.layers.menu.layer.text = \ Layer +controls.layers.menu.text = Portrait Layer +controls.overlayHeightLabel.text = Height +controls.overlayWidthLabel.text = Width +controls.portrait.filenameLabel.text = Portrait Filename +controls.portraitNameSuffixLabel.text = Portrait Suffix +controls.portraitNameSuffixLabel.tooltip = Use the Token's filename appended with the supplied text, eg. Orc [Portrait] +controls.save_portrait.text = Save Portrait on Drag & Drop +controls.save_portrait.tooltip = When saving token via Drag and Drop, a copy of the Portrait image used will also be saved. The .png format will be used if the image has transparency otherwise .jpg format will be used. +controls.token.filenameLabel.text = Token Filename +controls.tokenResolution.text = 256 x 256 +controls.useFileNumberingCheckbox.text = Use File Numbering +controls.useTokenNameCheckbox.text = Use Token Name + +controls.use_background.text = Use Background Options +controls.use_background.tooltip = Save Portrait using Background Image and Background Color is they are set. This will force the Portrait to be saved as a .jpg image. -controls.base.text = Use as a token base -controls.dragAsTokenCheckbox.text = Drag as .rptok Token -controls.dragAsTokenCheckbox.tooltip = If selected, dragging the token image will create a MapTool compatible .rptok token using base image as a Portrait image. -controls.filenameLabel.text = Filename -controls.filenameSuffixLabel.text = Filename Suffix # -controls.filenameSuffixLabel.tooltip = Append a number to the file name that auto increments for each save of the token. e.g. Orc-1, Orc-2, Orc-3. -controls.overlayHeightLabel.text = Height -controls.overlayWidthLabel.text = Width -controls.tokenResolution.text = 256 x 256 -controls.useFileNumberingCheckbox.text = Use File Numbering +imageUtil.filetype.label.all_images = All Images +imageUtil.filetype.label.extension = \ File +imageUtil.filetype.label.files = \ Files +imageUtil.filetype.label.image = Image menu.title.edit = _Edit menu.title.edit.capture.screen = Capture _Screen @@ -50,24 +81,32 @@ menu.title.edit.paste.image = Paste I_mage menu.title.file = _File menu.title.file.exit = E_xit menu.title.file.manage.overlays = _Manage Overlays +menu.title.file.open.pdf = _Open PDF menu.title.file.save.as = Save _As menu.title.help = _Help menu.title.help.about = _About TokenTool +menu.title.help.reset = _Reset Settings +options.pane.background.title = Background Options options.pane.effects = Overlay Options -options.pane.naming = Naming Options +options.pane.naming = Save Options options.pane.overlay = Overlay Options options.pane.overlay.checkbox.clip_portrait = Clip Portrait options.pane.overlay.checkbox.use_as_base = Send to Back options.pane.overlay.slider.Opacity = Opacity options.pane.overlay.tooltip.aspect = Keep the aspect ratio of the overlay -options.pane.portrait = Portrait Options -options.pane.portrait.button.Remove_Background_Color = Remove Background Color +options.pane.portrait.button.add_Background_Image = Change Background Image +options.pane.portrait.button.add_Portrait_Image = Change Portrait Image +options.pane.portrait.button.remove_Background_Color = Remove Background Color +options.pane.portrait.button.remove_Background_Image = Remove Background Image +options.pane.portrait.button.remove_Portrait_Image = Remove Portrait Image options.pane.portrait.color.prompt = Choose color to use behind portrait image options.pane.portrait.label.Background_Color = Background Color options.pane.portrait.label.Gaussian_Blur = Gaussian Blur options.pane.portrait.label.Glow = Glow options.pane.portrait.label.Opacity = Opacity +options.pane.portrait.label.effects = Portrait Effects +options.pane.portrait.title = Portrait Options pane.left.title = Drag or paste image here... diff --git a/src/main/resources/net/rptools/tokentool/i18n/TokenTool_da.properties b/src/main/resources/net/rptools/tokentool/i18n/TokenTool_da.properties index 411eb98..7ddac60 100644 --- a/src/main/resources/net/rptools/tokentool/i18n/TokenTool_da.properties +++ b/src/main/resources/net/rptools/tokentool/i18n/TokenTool_da.properties @@ -1,67 +1,114 @@ #Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/) # http://translatr.varunmalhotra.xyz/?ref=producthunt -Credits.label.Contributors=Bidragsydere -Credits.label.Credits=Anerkendelse + +AppSetup.dialog.install.overlays.confirmation = Nye overlejringer blev installeret. +AppSetup.dialog.install.overlays.confirmation.title = Nye overlejringer tilg\u00E6ngelige! + +Credits.label.Contributors = Bidragsydere +Credits.label.Credits = Anerkendelse Credits.stage.title = Om TokenTool -ManageOverlays.button.Restore_Default_Overlays=Gendan standard overlejringer -ManageOverlays.dialog.delete.confirmation=Er du sikker p\u00E5 at du vil slette -ManageOverlays.dialog.delete.confirmation.overlays=\ overlejringer? -ManageOverlays.dialog.delete.confirmation.these=disse -ManageOverlays.dialog.delete.dir.confirmation=Er du sikker p\u00E5 at du \u00F8nsker at slette -ManageOverlays.dialog.delete.dir.confirmation.directory=\ mappen? -ManageOverlays.dialog.delete.dir.directory_containing=\ mappen indeholdende \ - -ManageOverlays.dialog.delete.dir.overlays=\ overlejringer? -ManageOverlays.dialog.delete.dir.title=Slet mappe -ManageOverlays.dialog.delete.title=Slet overlejringer -ManageOverlays.dialog.restore.overlays.content_text=Er du sikker p\u00E5 at du \u00F8nsker at gendanne alle standard overlejringerne? -ManageOverlays.dialog.restore.overlays.title=Gendan overlejringer -ManageOverlays.filechooser.folder.content_text=Mappens navn: -ManageOverlays.filechooser.folder.title=Opret ny mappe -ManageOverlays.filechooser.overlay.title=V\u00E6lg billede filer -ManageOverlays.label.Details=Detaljer -ManageOverlays.label.Directory=Mappe -ManageOverlays.label.Image_Type_Description=Beskrivelse af billede type -ManageOverlays.label.Overlays=Overlejringer -ManageOverlays.stage.title=H\u00E5ndt\u00E9r overlejringer -RegionSelector.button.Capture=Indfang -TokenTool.save.filechooser.title=Gem som billede -TokenTool.stage.title = TokenTool -TokenTool.treeview.caching=Cacher -controls.base.text=Anvend som polet base -controls.dragAsTokenCheckbox.text=Tr\u00E6k som .rptok polet -controls.dragAsTokenCheckbox.tooltip=Hvis valgt, vil det at tr\u00E6kke billedet over i MapTool oprette en kompatibel .rptok polet med billedet som portr\u00E6t billede. -controls.filenameLabel.text=Filnavn -controls.filenameSuffixLabel.text=Filnavn endelse # -controls.filenameSuffixLabel.tooltip=F\u00F8j et fortl\u00F8bende nummer til filnavnet hver gang filen gemmes. F.eks. Ork-1, Ork-2, Ork-3. -controls.overlayHeightLabel.text=H\u00F8jde -controls.overlayWidthLabel.text=Bredde -controls.tokenResolution.text = 256 x 256 -controls.useFileNumberingCheckbox.text=Anvend fil nummerering -menu.title.edit=R_ediger -menu.title.edit.capture.screen=Indfang _sk\u00E6rm -menu.title.edit.copy.image=_Kopi\u00E9r billede -menu.title.edit.paste.image=Inds\u00E6t _billede -menu.title.file=_Filer -menu.title.file.exit=Af_slut -menu.title.file.manage.overlays=_H\u00E5ndt\u00E9r overlejringer -menu.title.file.save.as=Gem so_m -menu.title.help=_Hj\u00E6lp -menu.title.help.about=_Om TokenTool -options.pane.effects=Overlejring indstillinger -options.pane.naming=Navngivningsindstillinger -options.pane.overlay=Overlejringsindstillinger -options.pane.overlay.checkbox.clip_portrait=Trim portr\u00E6t -options.pane.overlay.checkbox.use_as_base=Send til baggrund -options.pane.overlay.slider.Opacity=Gennemsigtighed -options.pane.overlay.tooltip.aspect=Behold aspektforhold for overlejringen -options.pane.portrait=Portr\u00E6t indstillinger -options.pane.portrait.button.Remove_Background_Color=Fjern baggrundsfarve -options.pane.portrait.color.prompt=V\u00E6lg farve bag portr\u00E6t billede -options.pane.portrait.label.Background_Color=Baggrundsfarve -options.pane.portrait.label.Gaussian_Blur=Gaussisk sl\u00F8ring -options.pane.portrait.label.Glow=Gl\u00F8d -options.pane.portrait.label.Opacity=Gennemsigtighed -pane.left.title=Tr\u00E6k eller inds\u00E6t billede her... -splash.cache.label=Cacher overlejringer, dette tager kun et \u00F8jeblik... + +ImageGallery.stage.title = PDF billeder + +ManageOverlays.button.Restore_Default_Overlays = Gendan standard overlejringer +ManageOverlays.dialog.delete.confirmation = Er du sikker p\u00E5 at du vil slette +ManageOverlays.dialog.delete.confirmation.overlays = \ overlejringer? +ManageOverlays.dialog.delete.confirmation.these = disse +ManageOverlays.dialog.delete.dir.confirmation = Er du sikker p\u00E5 at du \u00F8nsker at slette +ManageOverlays.dialog.delete.dir.confirmation.directory = \ mappen? +ManageOverlays.dialog.delete.dir.directory_containing = \ mappen indeholdende +ManageOverlays.dialog.delete.dir.overlays = \ overlejringer? +ManageOverlays.dialog.delete.dir.title = Slet mappe +ManageOverlays.dialog.delete.title = Slet overlejringer +ManageOverlays.dialog.restore.overlays.content_text = Er du sikker p\u00E5 at du \u00F8nsker at gendanne alle standard overlejringerne? +ManageOverlays.dialog.restore.overlays.title = Gendan overlejringer +ManageOverlays.filechooser.folder.content_text = Mappens navn: +ManageOverlays.filechooser.folder.title = Opret ny mappe +ManageOverlays.filechooser.overlay.title = V\u00E6lg billede filer +ManageOverlays.label.Details = Detaljer +ManageOverlays.label.Directory = Mappe +ManageOverlays.label.Image_Type_Description = Beskrivelse af billede type +ManageOverlays.label.Overlays = Overlejringer +ManageOverlays.stage.title = H\u00E5ndt\u00E9r overlejringer + +PdvViewer.stage.title = V\u00E6lg PDF + +RegionSelector.button.Capture = Indfang + +TokenTool.dialog.confirmation.header = Bekr\u00E6ftelse +TokenTool.dialog.reset.confirmation.text = Dette vil gendanne alle standard UI indstillinger, er du sikker? +TokenTool.dialog.reset.confirmation.title = Nulstil indstillinger? +TokenTool.openBackgroundImage.filechooser.title = V\u00E6lg billede +TokenTool.openPDF.filechooser.title = V\u00E6lg PDF +TokenTool.openPortraitImage.filechooser.title = V\u00E6lg billede +TokenTool.save.filechooser.title = Gem som billede +TokenTool.stage.title = TokenTool +TokenTool.treeview.caching = Cacher + +controls.base.text = Anvend som polet base +controls.dragAsTokenCheckbox.text = Tr\u00E6k som .rptok polet +controls.dragAsTokenCheckbox.tooltip = Hvis valgt, vil det at tr\u00E6kke billedet over i MapTool oprette en kompatibel .rptok polet med billedet som portr\u00E6t billede. +controls.filenameSuffixLabel.text = Filnavn endelse # +controls.filenameSuffixLabel.tooltip = F\u00F8j et fortl\u00F8bende nummer til filnavnet hver gang filen gemmes. F.eks. Ork-1, Ork-2, Ork-3. +controls.layers.menu.item.background = Baggrund +controls.layers.menu.item.overlay = Overlejring +controls.layers.menu.item.portrait = Portr\u00E6t +controls.layers.menu.layer.text = \ lag +controls.layers.menu.text = Portr\u00E6t lag +controls.overlayHeightLabel.text = H\u00F8jde +controls.overlayWidthLabel.text = Bredde +controls.portrait.filenameLabel.text = Portr\u00E6t filnavn +controls.portraitNameSuffixLabel.text = Portr\u00E6t endelse +controls.portraitNameSuffixLabel.tooltip = Anvend polettens filnavn med den angivne tekst som endelse, f.eks. Ork [Portr\u00E6t] +controls.save_portrait.text = Gem portr\u00E6t ved tr\u00E6k og slip +controls.save_portrait.tooltip = N\u00E5r en polet gemmes via tr\u00E6k og slip, vil en kopi af portr\u00E6t billedet ogs\u00E5 blive gemt. .Png formattet vil blive brugt hvis billedet har transparens, ellers vil .jpg blive anvendt. +controls.token.filenameLabel.text = Polet filnavn +controls.tokenResolution.text = 256 x 256 +controls.useFileNumberingCheckbox.text = Anvend fil nummerering +controls.useTokenNameCheckbox.text = Anvend polet navn + +controls.use_background.text = Anvend baggrundsindstillinger +controls.use_background.tooltip = Gem portr\u00E6t med baggundsbillede og farve hvis disse er angivet. Dette vil tvinge portr\u00E6ttet til at blive gemt som et .jpg billede. + +imageUtil.filetype.label.all_images = Alle billeder +imageUtil.filetype.label.extension = \ fil +imageUtil.filetype.label.files = \ filer +imageUtil.filetype.label.image = billede + +menu.title.edit = R_ediger +menu.title.edit.capture.screen = Indfang _sk\u00E6rm +menu.title.edit.copy.image = _Kopi\u00E9r billede +menu.title.edit.paste.image = Inds\u00E6t _billede +menu.title.file = _Filer +menu.title.file.exit = Af_slut +menu.title.file.manage.overlays = _H\u00E5ndt\u00E9r overlejringer +menu.title.file.open.pdf = _\u00C5ben PDF +menu.title.file.save.as = Gem so_m +menu.title.help = _Hj\u00E6lp +menu.title.help.about = _Om TokenTool +menu.title.help.reset = _Gendan indstillinger + +options.pane.background.title = Baggrundsindstillinger +options.pane.effects = Overlejring indstillinger +options.pane.naming = Navngivningsindstillinger +options.pane.overlay = Overlejringsindstillinger +options.pane.overlay.checkbox.clip_portrait = Trim portr\u00E6t +options.pane.overlay.checkbox.use_as_base = Send til baggrund +options.pane.overlay.slider.Opacity = Gennemsigtighed +options.pane.overlay.tooltip.aspect = Behold aspektforhold for overlejringen +options.pane.portrait.button.add_Background_Image = Skift baggrundsbillede +options.pane.portrait.button.add_Portrait_Image = Skift portr\u00E6t billede +options.pane.portrait.button.remove_Background_Color = Fjern baggrundsfarve +options.pane.portrait.button.remove_Background_Image = Fjern baggrundsbillede +options.pane.portrait.button.remove_Portrait_Image = Fjern portr\u00E6tbillede +options.pane.portrait.color.prompt = V\u00E6lg farve bag portr\u00E6t billede +options.pane.portrait.label.Background_Color = Baggrundsfarve +options.pane.portrait.label.Gaussian_Blur = Gaussisk sl\u00F8ring +options.pane.portrait.label.Glow = Gl\u00F8d +options.pane.portrait.label.Opacity = Gennemsigtighed +options.pane.portrait.label.effects = Portr\u00E6t effekter +options.pane.portrait.title = Portr\u00E6t indstillinger + +pane.left.title = Tr\u00E6k eller inds\u00E6t billede her... + +splash.cache.label = Cacher overlejringer, dette tager kun et \u00F8jeblik... splash.version.label = Version diff --git a/src/main/resources/net/rptools/tokentool/i18n/TokenTool_de.properties b/src/main/resources/net/rptools/tokentool/i18n/TokenTool_de.properties index bd84818..69381f2 100644 --- a/src/main/resources/net/rptools/tokentool/i18n/TokenTool_de.properties +++ b/src/main/resources/net/rptools/tokentool/i18n/TokenTool_de.properties @@ -1,8 +1,13 @@ +AppSetup.dialog.install.overlays.confirmation = Neue Overlays wurden installiert. +AppSetup.dialog.install.overlays.confirmation.title = Neue \u00DCberlagerungen verf\u00FCgbar! + Credits.label.Contributors = Mitwirkende Credits.label.Credits = Credits Credits.stage.title = \u00DCber TokenTool +ImageGallery.stage.title = PDF Bilder + ManageOverlays.button.Restore_Default_Overlays = Standard-Overlays wiederherstellen ManageOverlays.dialog.delete.confirmation = Sind Sie sicher, dass Sie l\u00F6schen m\u00F6chten ManageOverlays.dialog.delete.confirmation.overlays = \ \u00DCberlagerungen? @@ -24,34 +29,63 @@ ManageOverlays.label.Image_Type_Description = Bildtyp Beschreibung ManageOverlays.label.Overlays = Overlays ManageOverlays.stage.title = Overlays verwalten +PdvViewer.stage.title = W\u00E4hlen Sie PDF + RegionSelector.button.Capture = Erfassung -TokenTool.save.filechooser.title = Als Bild speichern -TokenTool.stage.title = TokenTool -TokenTool.treeview.caching = Zwischenspeichern +TokenTool.dialog.confirmation.header = Best\u00E4tigung +TokenTool.dialog.reset.confirmation.text = Dadurch werden alle gespeicherten UI-Einstellungen auf die Standardwerte zur\u00FCckgesetzt. Sind Sie sicher? +TokenTool.dialog.reset.confirmation.title = Einstellungen zur\u00FCcksetzen? +TokenTool.openBackgroundImage.filechooser.title = Bild ausw\u00E4hlen +TokenTool.openPDF.filechooser.title = W\u00E4hlen Sie PDF +TokenTool.openPortraitImage.filechooser.title = Bild ausw\u00E4hlen +TokenTool.save.filechooser.title = Als Bild speichern +TokenTool.stage.title = TokenTool +TokenTool.treeview.caching = Zwischenspeichern + +controls.base.text = Als Token-Basis verwenden +controls.dragAsTokenCheckbox.text = Ziehe als .rptok Token +controls.dragAsTokenCheckbox.tooltip = Wenn ausgew\u00E4hlt, zieht das Ziehen des Token-Bildes ein MapTool-kompatibles .rptok-Token mit dem Basisbild als Portraitbild. +controls.filenameSuffixLabel.text = Dateiname Suffix +controls.filenameSuffixLabel.tooltip = Anh\u00E4ngen einer Nummer an den Dateinamen, der automatisch f\u00FCr jedes Speichern des Tokens inkrementiert wird. z.B. Orc-1, Orc-2, Orc-3. +controls.layers.menu.item.background = Hintergrund +controls.layers.menu.item.overlay = \u00DCberlagerung +controls.layers.menu.item.portrait = Portrait +controls.layers.menu.layer.text = \ Schicht +controls.layers.menu.text = Portr\u00E4tschicht +controls.overlayHeightLabel.text = H\u00F6he +controls.overlayWidthLabel.text = Breite +controls.portrait.filenameLabel.text = Portrait Filename +controls.portraitNameSuffixLabel.text = Portrait Suffix +controls.portraitNameSuffixLabel.tooltip = Verwenden Sie den Dateinamen des Tokens, dem der angegebene Text angeh\u00E4ngt ist, z. Ork [Portr\u00E4t] +controls.save_portrait.text = Portrait per Drag & Drop speichern +controls.save_portrait.tooltip = Beim Speichern von Token per Drag & Drop wird auch eine Kopie des verwendeten Portrait-Bildes gespeichert. Das .png-Format wird verwendet, wenn das Bild transparent ist, ansonsten wird das JPG-Format verwendet. +controls.token.filenameLabel.text = Dateiname +controls.tokenResolution.text = 256 x 256 +controls.useFileNumberingCheckbox.text = Verwenden Sie die Dateinummerierung +controls.useTokenNameCheckbox.text = Verwenden Sie den Token-Namen + +controls.use_background.text = Verwenden Sie Hintergrundoptionen +controls.use_background.tooltip = Save Portrait mit Hintergrundbild und Hintergrundfarbe sind sie eingestellt. Dadurch wird das Portrait als JPG-Bild gespeichert. -controls.base.text = Als Token-Basis verwenden -controls.dragAsTokenCheckbox.text = Ziehe als .rptok Token -controls.dragAsTokenCheckbox.tooltip = Wenn ausgew\u00E4hlt, zieht das Ziehen des Token-Bildes ein MapTool-kompatibles .rptok-Token mit dem Basisbild als Portraitbild. -controls.filenameLabel.text = Dateiname -controls.filenameSuffixLabel.text = Dateiname Suffix -controls.filenameSuffixLabel.tooltip = Anh\u00E4ngen einer Nummer an den Dateinamen, der automatisch f\u00FCr jedes Speichern des Tokens inkrementiert wird. z.B. Orc-1, Orc-2, Orc-3. -controls.overlayHeightLabel.text = H\u00F6he -controls.overlayWidthLabel.text = Breite -controls.tokenResolution.text = 256 x 256 -controls.useFileNumberingCheckbox.text = Verwenden Sie die Dateinummerierung +imageUtil.filetype.label.all_images = Alle Bilder +imageUtil.filetype.label.extension = \ Datei +imageUtil.filetype.label.files = \ Dateien +imageUtil.filetype.label.image = Bild -menu.title.edit = Bearbeiten -menu.title.edit.capture.screen = Bildschirmaufnahme anfertigen +menu.title.edit = _Bearbeiten +menu.title.edit.capture.screen = _Bildschirmaufnahme anfertigen menu.title.edit.copy.image = _Kopie Token -menu.title.edit.paste.image = Bild einf\u00FCgen -menu.title.file = Datei -menu.title.file.exit = Beenden -menu.title.file.manage.overlays = Overlays verwalten -menu.title.file.save.as = Als Bild speichern -menu.title.help = Hilfe -menu.title.help.about = \u00DCber TokenTool +menu.title.edit.paste.image = Bild _einf\u00FCgen +menu.title.file = _Datei +menu.title.file.exit = _Beenden +menu.title.file.manage.overlays = Overlays _verwalten +menu.title.file.open.pdf = PDF _\u00F6ffnen +menu.title.file.save.as = _Als Bild speichern +menu.title.help = _Hilfe +menu.title.help.about = _\u00DCber TokenTool +menu.title.help.reset = _Einstellungen zur\u00FCcksetzen +options.pane.background.title = Hintergrundoptionen options.pane.effects = Effekte anwenden options.pane.naming = Namensoptionen options.pane.overlay = Overlay-Optionen @@ -59,13 +93,18 @@ options.pane.overlay.checkbox.clip_portrait = Portr\u00E4tclip options.pane.overlay.checkbox.use_as_base = Nach hinten senden options.pane.overlay.slider.Opacity = Opazit\u00E4t options.pane.overlay.tooltip.aspect = Behalten Sie das Seitenverh\u00E4ltnis des Overlays bei -options.pane.portrait = Hintergrundoptionen -options.pane.portrait.button.Remove_Background_Color = Hintergrundfarbe entfernen +options.pane.portrait.button.add_Background_Image = \u00C4ndern Sie das Hintergrundbild +options.pane.portrait.button.add_Portrait_Image = \u00C4ndern Sie das Portr\u00E4tbild +options.pane.portrait.button.remove_Background_Color = Hintergrundfarbe entfernen +options.pane.portrait.button.remove_Background_Image = Supprimer l'image de fond +options.pane.portrait.button.remove_Portrait_Image = Entfernen Sie das Portr\u00E4tbild options.pane.portrait.color.prompt = W\u00E4hlen Sie die Farbe aus, die hinter dem Portr\u00E4tbild verwendet werden soll options.pane.portrait.label.Background_Color = Hintergrundfarbe options.pane.portrait.label.Gaussian_Blur = Gau\u00DFscher Weichzeichner options.pane.portrait.label.Glow = Gl\u00FChen options.pane.portrait.label.Opacity = Opazit\u00E4t +options.pane.portrait.label.effects = Portrait Effekte +options.pane.portrait.title = Hintergrundoptionen pane.left.title = Bild zuschneiden oder einf\u00FCgen ... diff --git a/src/main/resources/net/rptools/tokentool/i18n/TokenTool_en.properties b/src/main/resources/net/rptools/tokentool/i18n/TokenTool_en.properties index 1d22027..01ba614 100644 --- a/src/main/resources/net/rptools/tokentool/i18n/TokenTool_en.properties +++ b/src/main/resources/net/rptools/tokentool/i18n/TokenTool_en.properties @@ -1,9 +1,14 @@ #Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/) +AppSetup.dialog.install.overlays.confirmation = New overlays were installed. +AppSetup.dialog.install.overlays.confirmation.title = New Overlays Available! + Credits.label.Contributors = Contributors Credits.label.Credits = Credits Credits.stage.title = About TokenTool +ImageGallery.stage.title = PDF Images + ManageOverlays.button.Restore_Default_Overlays = Restore Default Overlays ManageOverlays.dialog.delete.confirmation = Are you sure you want to delete ManageOverlays.dialog.delete.confirmation.overlays = \ overlays? @@ -25,23 +30,49 @@ ManageOverlays.label.Image_Type_Description = Image Type Description ManageOverlays.label.Overlays = Overlays ManageOverlays.stage.title = Manage Overlays +PdvViewer.stage.title = Select PDF + RegionSelector.button.Capture = Capture -TokenTool.save.filechooser.title = Save as Image -TokenTool.stage.title = TokenTool -TokenTool.treeview.caching = Caching +TokenTool.dialog.confirmation.header = Confirmation +TokenTool.dialog.reset.confirmation.text = This will reset all saved UI settings back to default, are you sure? +TokenTool.dialog.reset.confirmation.title = Reset Settings? +TokenTool.openBackgroundImage.filechooser.title = Select Image +TokenTool.openPDF.filechooser.title = Select PDF +TokenTool.openPortraitImage.filechooser.title = Select Image +TokenTool.save.filechooser.title = Save as Image +TokenTool.stage.title = TokenTool +TokenTool.treeview.caching = Caching -controls.base.text = Use as a token base +controls.base.text = Use as a token base # Text and tool-tips for all the UI controls -controls.dragAsTokenCheckbox.text = Drag as .rptok Token -controls.dragAsTokenCheckbox.tooltip = If selected, dragging the token image will create a MapTool compatible .rptok token using base image as a Portrait image. -controls.filenameLabel.text = Filename -controls.filenameSuffixLabel.text = Filename Suffix # -controls.filenameSuffixLabel.tooltip = Append a number to the file name that auto increments for each save of the token. e.g. Orc-1, Orc-2, Orc-3. -controls.overlayHeightLabel.text = Height -controls.overlayWidthLabel.text = Width -controls.tokenResolution.text = 256 x 256 -controls.useFileNumberingCheckbox.text = Use File Numbering +controls.dragAsTokenCheckbox.text = Drag as .rptok Token +controls.dragAsTokenCheckbox.tooltip = If selected, dragging the token image will create a MapTool compatible .rptok token using base image as a Portrait image. +controls.filenameSuffixLabel.text = Filename Suffix # +controls.filenameSuffixLabel.tooltip = Append a number to the file name that auto increments for each save of the token. e.g. Orc-1, Orc-2, Orc-3. +controls.layers.menu.item.background = Background +controls.layers.menu.item.overlay = Overlay +controls.layers.menu.item.portrait = Portrait +controls.layers.menu.layer.text = \ Layer +controls.layers.menu.text = Portrait Layer +controls.overlayHeightLabel.text = Height +controls.overlayWidthLabel.text = Width +controls.portrait.filenameLabel.text = Portrait Filename +controls.portraitNameSuffixLabel.text = Portrait Suffix +controls.portraitNameSuffixLabel.tooltip = Use the Token's filename appended with the supplied text, eg. Orc [Portrait] +controls.save_portrait.text = Save Portrait on Drag & Drop +controls.save_portrait.tooltip = When saving token via Drag and Drop, a copy of the Portrait image used will also be saved. The .png format will be used if the image has transparency otherwise .jpg format will be used. +controls.token.filenameLabel.text = Token Filename +controls.tokenResolution.text = 256 x 256 +controls.useFileNumberingCheckbox.text = Use File Numbering +controls.useTokenNameCheckbox.text = Use Token Name + +controls.use_background.text = Use Background Options +controls.use_background.tooltip = Save Portrait using Background Image and Background Color is they are set. This will force the Portrait to be saved as a .jpg image. + +imageUtil.filetype.label.all_images = All Images +imageUtil.filetype.label.extension = \ File +imageUtil.filetype.label.files = \ Files +imageUtil.filetype.label.image = Image #For all the menu text... menu.title.edit = _Edit @@ -51,24 +82,32 @@ menu.title.edit.paste.image = Paste I_mage menu.title.file = _File menu.title.file.exit = E_xit menu.title.file.manage.overlays = _Manage Overlays +menu.title.file.open.pdf = _Open PDF menu.title.file.save.as = Save _As menu.title.help = _Help menu.title.help.about = _About TokenTool +menu.title.help.reset = _Reset Settings +options.pane.background.title = Background Options options.pane.effects = Overlay Options -options.pane.naming = Naming Options +options.pane.naming = Save Options options.pane.overlay = Overlay Options options.pane.overlay.checkbox.clip_portrait = Clip Portrait options.pane.overlay.checkbox.use_as_base = Send to Back options.pane.overlay.slider.Opacity = Opacity options.pane.overlay.tooltip.aspect = Keep the aspect ratio of the overlay -options.pane.portrait = Portrait Options -options.pane.portrait.button.Remove_Background_Color = Remove Background Color +options.pane.portrait.button.add_Background_Image = Change Background Image +options.pane.portrait.button.add_Portrait_Image = Change Portrait Image +options.pane.portrait.button.remove_Background_Color = Remove Background Color +options.pane.portrait.button.remove_Background_Image = Remove Background Image +options.pane.portrait.button.remove_Portrait_Image = Remove Portrait Image options.pane.portrait.color.prompt = Choose color to use behind portrait image options.pane.portrait.label.Background_Color = Background Color options.pane.portrait.label.Gaussian_Blur = Gaussian Blur options.pane.portrait.label.Glow = Glow options.pane.portrait.label.Opacity = Opacity +options.pane.portrait.label.effects = Portrait Effects +options.pane.portrait.title = Portrait Options pane.left.title = Drag or paste image here... diff --git a/src/main/resources/net/rptools/tokentool/i18n/TokenTool_fr.properties b/src/main/resources/net/rptools/tokentool/i18n/TokenTool_fr.properties index a478e6e..cc8d79c 100644 --- a/src/main/resources/net/rptools/tokentool/i18n/TokenTool_fr.properties +++ b/src/main/resources/net/rptools/tokentool/i18n/TokenTool_fr.properties @@ -1,8 +1,13 @@ +AppSetup.dialog.install.overlays.confirmation = De nouvelles superpositions ont \u00E9t\u00E9 install\u00E9es. +AppSetup.dialog.install.overlays.confirmation.title = De nouvelles superpositions disponibles! + Credits.label.Contributors = Contributeurs Credits.label.Credits = Cr\u00E9dits Credits.stage.title = A propos de TokenTool +ImageGallery.stage.title = PDF Images + ManageOverlays.button.Restore_Default_Overlays = Restaurer les superpositions par d\u00E9faut ManageOverlays.dialog.delete.confirmation = Etes-vous s\u00FBr que vous voulez supprimer ManageOverlays.dialog.delete.confirmation.overlays = \ superpositions? @@ -24,34 +29,63 @@ ManageOverlays.label.Image_Type_Description = Image Type Description ManageOverlays.label.Overlays = Superpositions ManageOverlays.stage.title = G\u00E9rer les surimpressions +PdvViewer.stage.title = S\u00E9lectionnez PDF + RegionSelector.button.Capture = Capture -TokenTool.save.filechooser.title = Save as Image -TokenTool.stage.title = TokenTool -TokenTool.treeview.caching = Mise en cache +TokenTool.dialog.confirmation.header = Confirmation +TokenTool.dialog.reset.confirmation.text = Cela r\u00E9initialisera tous les param\u00E8tres de l'interface utilisateur sauvegard\u00E9s \u00E0 la valeur par d\u00E9faut, \u00EAtes-vous s\u00FBr? +TokenTool.dialog.reset.confirmation.title = R\u00E9initialiser les options? +TokenTool.openBackgroundImage.filechooser.title = S\u00E9lectionner une image +TokenTool.openPDF.filechooser.title = S\u00E9lectionnez PDF +TokenTool.openPortraitImage.filechooser.title = S\u00E9lectionner une image +TokenTool.save.filechooser.title = Save as Image +TokenTool.stage.title = TokenTool +TokenTool.treeview.caching = Mise en cache + +controls.base.text = Utilisation comme base symbolique +controls.dragAsTokenCheckbox.text = Faites glisser le jeton .rptok +controls.dragAsTokenCheckbox.tooltip = Si s\u00E9lectionn\u00E9, le glissement de l'image de jeton cr\u00E9era un jeton .rptok compatible MapTool utilisant l'image de base sous forme d'image Portrait. +controls.filenameSuffixLabel.text = Suffixe du nom de fichier +controls.filenameSuffixLabel.tooltip = Ajoutez un nombre au nom du fichier qui augmente automatiquement pour chaque sauvegarde du jeton. par exemple. Orc-1, Orc-2, Orc-3. +controls.layers.menu.item.background = Contexte +controls.layers.menu.item.overlay = Recouvrir +controls.layers.menu.item.portrait = Portrait +controls.layers.menu.layer.text = \ Layer +controls.layers.menu.text = Portrait Layer +controls.overlayHeightLabel.text = la taille +controls.overlayWidthLabel.text = Largeur +controls.portrait.filenameLabel.text = Portrait Filename +controls.portraitNameSuffixLabel.text = Portrait Suffix +controls.portraitNameSuffixLabel.tooltip = Utilisez le nom de fichier du jeton ajout\u00E9 au texte fourni, par exemple. Orc [Portrait] +controls.save_portrait.text = Enregistrer un portrait par glisser-d\u00E9poser +controls.save_portrait.tooltip = Lors de l'enregistrement du jeton par glisser-d\u00E9poser, une copie de l'image Portrait utilis\u00E9e sera \u00E9galement enregistr\u00E9e. Le format .png sera utilis\u00E9 si l'image est transparente, sinon le format .jpg sera utilis\u00E9. +controls.token.filenameLabel.text = Nom de fichier +controls.tokenResolution.text = 256 x 256 +controls.useFileNumberingCheckbox.text = Utiliser la num\u00E9rotation des fichiers +controls.useTokenNameCheckbox.text = Utiliser le nom du jeton + +controls.use_background.text = Utiliser les options d'arri\u00E8re-plan +controls.use_background.tooltip = Enregistrer un portrait \u00E0 l'aide de l'image d'arri\u00E8re-plan et de la couleur d'arri\u00E8re-plan sont d\u00E9finies. Cela forcera le portrait \u00E0 \u00EAtre enregistr\u00E9 sous la forme d'une image .jpg. -controls.base.text = Utilisation comme base symbolique -controls.dragAsTokenCheckbox.text = Faites glisser le jeton .rptok -controls.dragAsTokenCheckbox.tooltip = Si s\u00E9lectionn\u00E9, le glissement de l'image de jeton cr\u00E9era un jeton .rptok compatible MapTool utilisant l'image de base sous forme d'image Portrait. -controls.filenameLabel.text = Nom de fichier -controls.filenameSuffixLabel.text = Suffixe du nom de fichier -controls.filenameSuffixLabel.tooltip = Ajoutez un nombre au nom du fichier qui augmente automatiquement pour chaque sauvegarde du jeton. par exemple. Orc-1, Orc-2, Orc-3. -controls.overlayHeightLabel.text = la taille -controls.overlayWidthLabel.text = Largeur -controls.tokenResolution.text = 256 x 256 -controls.useFileNumberingCheckbox.text = Utiliser la num\u00E9rotation des fichiers +imageUtil.filetype.label.all_images = All Images +imageUtil.filetype.label.extension = \ Fichier +imageUtil.filetype.label.files = \ Des dossiers +imageUtil.filetype.label.image = Image -menu.title.edit = Edition -menu.title.edit.capture.screen = Capture d'\u00E9cran -menu.title.edit.copy.image = _Copie Token -menu.title.edit.paste.image = Coller l'image -menu.title.file = Fichier -menu.title.file.exit = Sortie -menu.title.file.manage.overlays = G\u00E9rer superpositions -menu.title.file.save.as = Enregistrer sous image -menu.title.help = Aide -menu.title.help.about = A propos de TokenTool +menu.title.edit = _Edition +menu.title.edit.capture.screen = _Capture d'\u00E9cran +menu.title.edit.copy.image = _Copie Image +menu.title.edit.paste.image = Coller I_mage +menu.title.file = _Fichier +menu.title.file.exit = S_ortie +menu.title.file.manage.overlays = _G\u00E9rer superpositions +menu.title.file.open.pdf = _Ouvrir le PDF +menu.title.file.save.as = Enregistrer _sous image +menu.title.help = _Aide +menu.title.help.about = _A propos de TokenTool +menu.title.help.reset = _R\u00E9initialiser les options +options.pane.background.title = Options d'arri\u00E8re-plan options.pane.effects = Appliquer des effets options.pane.naming = Options de nommage options.pane.overlay = Options de superposition @@ -59,13 +93,18 @@ options.pane.overlay.checkbox.clip_portrait = Portrait de clip options.pane.overlay.checkbox.use_as_base = Envoyer au fond options.pane.overlay.slider.Opacity = Opacit\u00E9 options.pane.overlay.tooltip.aspect = Conserver les proportions de la superposition -options.pane.portrait = Options de fond -options.pane.portrait.button.Remove_Background_Color = Supprimer la couleur de fond +options.pane.portrait.button.add_Background_Image = Changer l'image de fond +options.pane.portrait.button.add_Portrait_Image = Change Portrait Image +options.pane.portrait.button.remove_Background_Color = Supprimer la couleur de fond +options.pane.portrait.button.remove_Background_Image = Entfernen Sie das Hintergrundbild +options.pane.portrait.button.remove_Portrait_Image = Remove Portrait Image options.pane.portrait.color.prompt = Choisissez la couleur \u00E0 utiliser derri\u00E8re l'image de portrait options.pane.portrait.label.Background_Color = Couleur de fond options.pane.portrait.label.Gaussian_Blur = Flou gaussien options.pane.portrait.label.Glow = lueur options.pane.portrait.label.Opacity = Opacit\u00E9 +options.pane.portrait.label.effects = Portrait Effects +options.pane.portrait.title = Options de fond pane.left.title = Faites glisser ou collez l'image ici ... diff --git a/src/main/resources/net/rptools/tokentool/i18n/TokenTool_it.properties b/src/main/resources/net/rptools/tokentool/i18n/TokenTool_it.properties index 3c1badd..a6b7f3d 100644 --- a/src/main/resources/net/rptools/tokentool/i18n/TokenTool_it.properties +++ b/src/main/resources/net/rptools/tokentool/i18n/TokenTool_it.properties @@ -1,8 +1,13 @@ +AppSetup.dialog.install.overlays.confirmation = Sono state installate nuove sovrapposizioni. +AppSetup.dialog.install.overlays.confirmation.title = Nuove sovrapposizioni disponibili! + Credits.label.Contributors = Contributori Credits.label.Credits = Crediti Credits.stage.title = Informazioni su TokenTool +ImageGallery.stage.title = Immagini PDF + ManageOverlays.button.Restore_Default_Overlays = Ripristina sovrapposizioni predefinite ManageOverlays.dialog.delete.confirmation = Sei sicuro di voler eliminare ManageOverlays.dialog.delete.confirmation.overlays = \ sovrapposizioni? @@ -24,34 +29,63 @@ ManageOverlays.label.Image_Type_Description = Tipo di immagine Descr ManageOverlays.label.Overlays = Sovrapposizioni ManageOverlays.stage.title = Gestione sovrapposizioni +PdvViewer.stage.title = Seleziona PDF + RegionSelector.button.Capture = Catturare -TokenTool.save.filechooser.title = Salva come immagine -TokenTool.stage.title = TokenTool -TokenTool.treeview.caching = caching +TokenTool.dialog.confirmation.header = Conferma +TokenTool.dialog.reset.confirmation.text = Ci\u00F2 ripristiner\u00E0 tutte le impostazioni dell'interfaccia utente salvate sui valori predefiniti, sei sicuro? +TokenTool.dialog.reset.confirmation.title = Ripristina le impostazioni? +TokenTool.openBackgroundImage.filechooser.title = Seleziona immagine +TokenTool.openPDF.filechooser.title = Seleziona PDF +TokenTool.openPortraitImage.filechooser.title = Seleziona immagine +TokenTool.save.filechooser.title = Salva come immagine +TokenTool.stage.title = TokenTool +TokenTool.treeview.caching = caching + +controls.base.text = Utilizzo come base di riferimento +controls.dragAsTokenCheckbox.text = Trascina come .rptok Token +controls.dragAsTokenCheckbox.tooltip = Se selezionato, trascinando l'immagine del token creer\u00E0 un token rtp compatibile con MapTool utilizzando l'immagine di base come immagine ritratta. +controls.filenameSuffixLabel.text = Suffisso del nome del file +controls.filenameSuffixLabel.tooltip = Aggiungere un numero al nome del file che incrementa automaticamente per ogni salvataggio del token. per esempio. Orc-1, Orc-2, Orc-3. +controls.layers.menu.item.background = sfondo +controls.layers.menu.item.overlay = Copertura +controls.layers.menu.item.portrait = Ritratto +controls.layers.menu.layer.text = \ Strato +controls.layers.menu.text = Livello di ritratto +controls.overlayHeightLabel.text = Altezza +controls.overlayWidthLabel.text = Larghezza +controls.portrait.filenameLabel.text = Filename ritratto +controls.portraitNameSuffixLabel.text = Suffisso di ritratto +controls.portraitNameSuffixLabel.tooltip = Utilizzare il nome file del token aggiunto al testo fornito, ad es. Orco [Ritratto] +controls.save_portrait.text = Salva ritratto su trascina e rilascia +controls.save_portrait.tooltip = Quando si salva token tramite Drag and Drop, verr\u00E0 salvata anche una copia dell'immagine Portrait utilizzata. Il formato .png verr\u00E0 utilizzato se l'immagine \u00E8 trasparente altrimenti verr\u00E0 utilizzato il formato .jpg. +controls.token.filenameLabel.text = Nome del file +controls.tokenResolution.text = 256 x 256 +controls.useFileNumberingCheckbox.text = Utilizzare la numerazione dei file +controls.useTokenNameCheckbox.text = Usa nome token + +controls.use_background.text = Usa le opzioni di sfondo +controls.use_background.tooltip = Salva ritratto utilizzando l'immagine di sfondo e il colore di sfondo \u00E8 sono impostati. Ci\u00F2 costringer\u00E0 il ritratto a essere salvato come immagine .jpg. -controls.base.text = Utilizzo come base di riferimento -controls.dragAsTokenCheckbox.text = Trascina come .rptok Token -controls.dragAsTokenCheckbox.tooltip = Se selezionato, trascinando l'immagine del token creer\u00E0 un token rtp compatibile con MapTool utilizzando l'immagine di base come immagine ritratta. -controls.filenameLabel.text = Nome del file -controls.filenameSuffixLabel.text = Suffisso del nome del file -controls.filenameSuffixLabel.tooltip = Aggiungere un numero al nome del file che incrementa automaticamente per ogni salvataggio del token. per esempio. Orc-1, Orc-2, Orc-3. -controls.overlayHeightLabel.text = Altezza -controls.overlayWidthLabel.text = Larghezza -controls.tokenResolution.text = 256 x 256 -controls.useFileNumberingCheckbox.text = Utilizzare la numerazione dei file +imageUtil.filetype.label.all_images = Tutte le immagini +imageUtil.filetype.label.extension = \ File +imageUtil.filetype.label.files = \ Files +imageUtil.filetype.label.image = Immagine -menu.title.edit = Modifica -menu.title.edit.capture.screen = _Screen Capture +menu.title.edit = _Modifica +menu.title.edit.capture.screen = _Cattura schermo menu.title.edit.copy.image = _Copia provvisorie -menu.title.edit.paste.image = Incolla l'immagine -menu.title.file = File -menu.title.file.exit = Esci -menu.title.file.manage.overlays = Gestione sovrapposizioni -menu.title.file.save.as = Salva come immagine -menu.title.help = Aiuto -menu.title.help.about = Chi TokenTool +menu.title.edit.paste.image = Incolla I_mmagine +menu.title.file = _File +menu.title.file.exit = _Esci +menu.title.file.manage.overlays = _Gestione sovrapposizioni +menu.title.file.open.pdf = _Apri PDF +menu.title.file.save.as = Salva _come immagine +menu.title.help = _Aiuto +menu.title.help.about = _Chi TokenTool +menu.title.help.reset = _Ripristina le impostazioni +options.pane.background.title = Opzioni di sfondo options.pane.effects = Applica gli Effetti options.pane.naming = Opzioni di denominazione options.pane.overlay = Opzioni di sovrapposizione @@ -59,13 +93,18 @@ options.pane.overlay.checkbox.clip_portrait = Clip di ritratto options.pane.overlay.checkbox.use_as_base = Mandare indietro options.pane.overlay.slider.Opacity = Opacit\u00E0 options.pane.overlay.tooltip.aspect = Mantenere il rapporto di aspetto dell'overlay -options.pane.portrait = Opzioni di sfondo -options.pane.portrait.button.Remove_Background_Color = Rimuovi colore di sfondo +options.pane.portrait.button.add_Background_Image = Cambia l'immagine di sfondo +options.pane.portrait.button.add_Portrait_Image = Cambia immagine verticale +options.pane.portrait.button.remove_Background_Color = Rimuovi colore di sfondo +options.pane.portrait.button.remove_Background_Image = Rimuovi l'immagine di sfondo +options.pane.portrait.button.remove_Portrait_Image = Rimuovi l'immagine verticale options.pane.portrait.color.prompt = Scegli il colore da utilizzare dietro l'immagine ritratta options.pane.portrait.label.Background_Color = Colore di sfondo options.pane.portrait.label.Gaussian_Blur = Sfocatura gaussiana options.pane.portrait.label.Glow = splendore options.pane.portrait.label.Opacity = Opacit\u00E0 +options.pane.portrait.label.effects = Effetti di ritratto +options.pane.portrait.title = Opzioni di sfondo pane.left.title = Trascina o incolla l'immagine qui ... diff --git a/src/main/resources/net/rptools/tokentool/i18n/TokenTool_ja.properties b/src/main/resources/net/rptools/tokentool/i18n/TokenTool_ja.properties index bda13c0..c7555a8 100644 --- a/src/main/resources/net/rptools/tokentool/i18n/TokenTool_ja.properties +++ b/src/main/resources/net/rptools/tokentool/i18n/TokenTool_ja.properties @@ -1,8 +1,13 @@ +AppSetup.dialog.install.overlays.confirmation = \u65B0\u3057\u3044\u30AA\u30FC\u30D0\u30FC\u30EC\u30A4\u304C\u30A4\u30F3\u30B9\u30C8\u30FC\u30EB\u3055\u308C\u307E\u3057\u305F\u3002 +AppSetup.dialog.install.overlays.confirmation.title = \u65B0\u3057\u3044\u30AA\u30FC\u30D0\u30FC\u30EC\u30A4\u304C\u5229\u7528\u53EF\u80FD\u3067\u3059\uFF01 + Credits.label.Contributors = \u8CA2\u732E\u8005 Credits.label.Credits = \u30AF\u30EC\u30B8\u30C3\u30C8 Credits.stage.title = TokenTool\u306B\u3064\u3044\u3066 +ImageGallery.stage.title = PDF\u753B\u50CF + ManageOverlays.button.Restore_Default_Overlays = \u30C7\u30D5\u30A9\u30EB\u30C8\u306E\u30AA\u30FC\u30D0\u30FC\u30EC\u30A4\u3092\u5FA9\u5143\u3059\u308B ManageOverlays.dialog.delete.confirmation = \u6D88\u53BB\u3057\u3066\u3082\u3088\u308D\u3057\u3044\u3067\u3059\u304B ManageOverlays.dialog.delete.confirmation.overlays = \ \u30AA\u30FC\u30D0\u30FC\u30EC\u30A4\uFF1F @@ -24,22 +29,48 @@ ManageOverlays.label.Image_Type_Description = \u753B\u50CF\u30BF\u30 ManageOverlays.label.Overlays = \u30AA\u30FC\u30D0\u30FC\u30EC\u30A4 ManageOverlays.stage.title = \u30AA\u30FC\u30D0\u30FC\u30EC\u30A4\u3092\u7BA1\u7406\u3059\u308B +PdvViewer.stage.title = PDF\u3092\u9078\u629E + RegionSelector.button.Capture = \u30AD\u30E3\u30D7\u30C1\u30E3\u30FC -TokenTool.save.filechooser.title = \u753B\u50CF\u3068\u3057\u3066\u4FDD\u5B58 -TokenTool.stage.title = TokenTool -TokenTool.treeview.caching = \u30AD\u30E3\u30C3\u30B7\u30F3\u30B0 +TokenTool.dialog.confirmation.header = \u78BA\u8A8D +TokenTool.dialog.reset.confirmation.text = \u4FDD\u5B58\u3055\u308C\u305F\u3059\u3079\u3066\u306EUI\u8A2D\u5B9A\u304C\u30C7\u30D5\u30A9\u30EB\u30C8\u306B\u623B\u3055\u308C\u307E\u3059\u3002\u672C\u5F53\u3067\u3059\u304B\uFF1F +TokenTool.dialog.reset.confirmation.title = \u8A2D\u5B9A\u3092\u30EA\u30BB\u30C3\u30C8\uFF1F +TokenTool.openBackgroundImage.filechooser.title = \u753B\u50CF\u3092\u9078\u629E +TokenTool.openPDF.filechooser.title = PDF\u3092\u9078\u629E +TokenTool.openPortraitImage.filechooser.title = \u753B\u50CF\u3092\u9078\u629E +TokenTool.save.filechooser.title = \u753B\u50CF\u3068\u3057\u3066\u4FDD\u5B58 +TokenTool.stage.title = TokenTool +TokenTool.treeview.caching = \u30AD\u30E3\u30C3\u30B7\u30F3\u30B0 + +controls.base.text = \u30C8\u30FC\u30AF\u30F3\u30D9\u30FC\u30B9\u3068\u3057\u3066\u4F7F\u7528\u3059\u308B +controls.dragAsTokenCheckbox.text = .rptok\u30C8\u30FC\u30AF\u30F3\u3068\u3057\u3066\u30C9\u30E9\u30C3\u30B0 +controls.dragAsTokenCheckbox.tooltip = \u9078\u629E\u3059\u308B\u3068\u3001\u30C8\u30FC\u30AF\u30F3\u30A4\u30E1\u30FC\u30B8\u3092\u30C9\u30E9\u30C3\u30B0\u3059\u308B\u3068\u3001\u30D9\u30FC\u30B9\u30A4\u30E1\u30FC\u30B8\u3092\u30DD\u30FC\u30C8\u30EC\u30FC\u30C8\u30A4\u30E1\u30FC\u30B8\u3068\u3057\u3066\u4F7F\u7528\u3057\u3066MapTool\u4E92\u63DB\u306E.rptok\u30C8\u30FC\u30AF\u30F3\u304C\u4F5C\u6210\u3055\u308C\u307E\u3059\u3002 +controls.filenameSuffixLabel.text = \u30D5\u30A1\u30A4\u30EB\u540D\u63A5\u5C3E\u8F9E +controls.filenameSuffixLabel.tooltip = \u30C8\u30FC\u30AF\u30F3\u306E\u4FDD\u5B58\u3054\u3068\u306B\u81EA\u52D5\u30A4\u30F3\u30AF\u30EA\u30E1\u30F3\u30C8\u3059\u308B\u30D5\u30A1\u30A4\u30EB\u540D\u306B\u6570\u5B57\u3092\u8FFD\u52A0\u3057\u307E\u3059\u3002\u4F8B\u3048\u3070Orc-1\u3001Orc-2\u3001Orc-3\u3002 +controls.layers.menu.item.background = \u30D0\u30C3\u30AF\u30B0\u30E9\u30A6\u30F3\u30C9 +controls.layers.menu.item.overlay = \u30AA\u30FC\u30D0\u30FC\u30EC\u30A4 +controls.layers.menu.item.portrait = \u30DD\u30FC\u30C8\u30EC\u30FC\u30C8 +controls.layers.menu.layer.text = \ \u5C64 +controls.layers.menu.text = \u30DD\u30FC\u30C8\u30EC\u30FC\u30C8\u30EC\u30A4\u30E4\u30FC +controls.overlayHeightLabel.text = \u9AD8\u3055 +controls.overlayWidthLabel.text = \u5E45 +controls.portrait.filenameLabel.text = \u30DD\u30FC\u30C8\u30EC\u30FC\u30C8\u30D5\u30A1\u30A4\u30EB\u540D +controls.portraitNameSuffixLabel.text = \u30DD\u30FC\u30C8\u30EC\u30FC\u30C8\u63A5\u5C3E\u8F9E +controls.portraitNameSuffixLabel.tooltip = \u6307\u5B9A\u3055\u308C\u305F\u30C6\u30AD\u30B9\u30C8\u304C\u4ED8\u3044\u305F\u30C8\u30FC\u30AF\u30F3\u306E\u30D5\u30A1\u30A4\u30EB\u540D\u3092\u4F7F\u7528\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u30AA\u30EB\u30AF[\u30DD\u30FC\u30C8\u30EC\u30FC\u30C8] +controls.save_portrait.text = \u30C9\u30E9\u30C3\u30B0\uFF06\u30C9\u30ED\u30C3\u30D7\u3067\u30DD\u30FC\u30C8\u30EC\u30FC\u30C8\u3092\u4FDD\u5B58 +controls.save_portrait.tooltip = \u30C9\u30E9\u30C3\u30B0\u30A2\u30F3\u30C9\u30C9\u30ED\u30C3\u30D7\u3067\u30C8\u30FC\u30AF\u30F3\u3092\u4FDD\u5B58\u3059\u308B\u3068\u3001\u4F7F\u7528\u3055\u308C\u305F\u30DD\u30FC\u30C8\u30EC\u30FC\u30C8\u753B\u50CF\u306E\u30B3\u30D4\u30FC\u3082\u4FDD\u5B58\u3055\u308C\u307E\u3059\u3002\u30A4\u30E1\u30FC\u30B8\u306B\u900F\u660E\u6027\u304C\u3042\u308B\u5834\u5408\u306F.png\u5F62\u5F0F\u304C\u4F7F\u7528\u3055\u308C\u3001\u305D\u3046\u3067\u306A\u3044\u5834\u5408\u306F.jpg\u5F62\u5F0F\u304C\u4F7F\u7528\u3055\u308C\u307E\u3059\u3002 +controls.token.filenameLabel.text = \u30D5\u30A1\u30A4\u30EB\u540D +controls.tokenResolution.text = 256 x 256 +controls.useFileNumberingCheckbox.text = \u30D5\u30A1\u30A4\u30EB\u756A\u53F7\u3092\u4F7F\u7528\u3059\u308B +controls.useTokenNameCheckbox.text = \u30C8\u30FC\u30AF\u30F3\u540D\u3092\u4F7F\u7528\u3059\u308B +controls.use_background.text = \u80CC\u666F\u30AA\u30D7\u30B7\u30E7\u30F3\u3092\u4F7F\u7528\u3059\u308B +controls.use_background.tooltip = \u80CC\u666F\u753B\u50CF\u3068\u80CC\u666F\u8272\u3092\u4F7F\u7528\u3057\u3066\u30DD\u30FC\u30C8\u30EC\u30FC\u30C8\u3092\u4FDD\u5B58\u3059\u308B\u3068\u3001\u305D\u308C\u3089\u304C\u8A2D\u5B9A\u3055\u308C\u307E\u3059\u3002\u3053\u308C\u306B\u3088\u308A\u3001\u30DD\u30FC\u30C8\u30EC\u30FC\u30C8\u306F.jpg\u753B\u50CF\u3068\u3057\u3066\u4FDD\u5B58\u3055\u308C\u307E\u3059\u3002 -controls.base.text = \u30C8\u30FC\u30AF\u30F3\u30D9\u30FC\u30B9\u3068\u3057\u3066\u4F7F\u7528\u3059\u308B -controls.dragAsTokenCheckbox.text = .rptok\u30C8\u30FC\u30AF\u30F3\u3068\u3057\u3066\u30C9\u30E9\u30C3\u30B0 -controls.dragAsTokenCheckbox.tooltip = \u9078\u629E\u3059\u308B\u3068\u3001\u30C8\u30FC\u30AF\u30F3\u30A4\u30E1\u30FC\u30B8\u3092\u30C9\u30E9\u30C3\u30B0\u3059\u308B\u3068\u3001\u30D9\u30FC\u30B9\u30A4\u30E1\u30FC\u30B8\u3092\u30DD\u30FC\u30C8\u30EC\u30FC\u30C8\u30A4\u30E1\u30FC\u30B8\u3068\u3057\u3066\u4F7F\u7528\u3057\u3066MapTool\u4E92\u63DB\u306E.rptok\u30C8\u30FC\u30AF\u30F3\u304C\u4F5C\u6210\u3055\u308C\u307E\u3059\u3002 -controls.filenameLabel.text = \u30D5\u30A1\u30A4\u30EB\u540D -controls.filenameSuffixLabel.text = \u30D5\u30A1\u30A4\u30EB\u540D\u63A5\u5C3E\u8F9E -controls.filenameSuffixLabel.tooltip = \u30C8\u30FC\u30AF\u30F3\u306E\u4FDD\u5B58\u3054\u3068\u306B\u81EA\u52D5\u30A4\u30F3\u30AF\u30EA\u30E1\u30F3\u30C8\u3059\u308B\u30D5\u30A1\u30A4\u30EB\u540D\u306B\u6570\u5B57\u3092\u8FFD\u52A0\u3057\u307E\u3059\u3002\u4F8B\u3048\u3070Orc-1\u3001Orc-2\u3001Orc-3\u3002 -controls.overlayHeightLabel.text = \u9AD8\u3055 -controls.overlayWidthLabel.text = \u5E45 -controls.tokenResolution.text = 256 x 256 -controls.useFileNumberingCheckbox.text = \u30D5\u30A1\u30A4\u30EB\u756A\u53F7\u3092\u4F7F\u7528\u3059\u308B +imageUtil.filetype.label.all_images = \u3059\u3079\u3066\u306E\u753B\u50CF +imageUtil.filetype.label.extension = \ \u30D5\u30A1\u30A4\u30EB +imageUtil.filetype.label.files = \ \u30D5\u30A1\u30A4\u30EB +imageUtil.filetype.label.image = \u753B\u50CF menu.title.edit = \u7DE8\u96C6 menu.title.edit.capture.screen = \u30B9\u30AF\u30EA\u30FC\u30F3\u30AD\u30E3\u30D7\u30C1\u30E3 @@ -48,10 +79,13 @@ menu.title.edit.paste.image = \u753B\u50CF\u3092\u8CBC\u308A\u4ED8\u3051\u30 menu.title.file = \u30D5\u30A1\u30A4\u30EB menu.title.file.exit = \u7D42\u4E86 menu.title.file.manage.overlays = \u7BA1\u7406\u30AA\u30FC\u30D0\u30FC\u30EC\u30A4 +menu.title.file.open.pdf = PDF\u3092\u958B\u304F menu.title.file.save.as = \u540D\u524D\u3092\u4ED8\u3051\u3066\u4FDD\u5B58\u753B\u50CF \u30C8\u30FC\u30AF\u30F3\u3068\u3057\u3066 menu.title.help = \u30D8\u30EB\u30D7 menu.title.help.about = TokenTool\u306B\u3064\u3044\u3066 +menu.title.help.reset = \u8A2D\u5B9A\u3092\u30EA\u30BB\u30C3\u30C8 +options.pane.background.title = \u80CC\u666F\u30AA\u30D7\u30B7\u30E7\u30F3 options.pane.effects = \u30A8\u30D5\u30A7\u30AF\u30C8\u3092\u9069\u7528\u3059\u308B options.pane.naming = \u547D\u540D\u30AA\u30D7\u30B7\u30E7\u30F3 options.pane.overlay = \u30AA\u30FC\u30D0\u30FC\u30EC\u30A4\u30AA\u30D7\u30B7\u30E7\u30F3 @@ -59,13 +93,18 @@ options.pane.overlay.checkbox.clip_portrait = \u30DD\u30FC\u30C8\u30EC\ options.pane.overlay.checkbox.use_as_base = \u8FD4\u4FE1\u3059\u308B options.pane.overlay.slider.Opacity = \u4E0D\u900F\u660E\u5EA6 options.pane.overlay.tooltip.aspect = \u30AA\u30FC\u30D0\u30FC\u30EC\u30A4\u306E\u7E26\u6A2A\u6BD4\u3092\u7DAD\u6301\u3059\u308B -options.pane.portrait = \u80CC\u666F\u30AA\u30D7\u30B7\u30E7\u30F3 -options.pane.portrait.button.Remove_Background_Color = \u80CC\u666F\u8272\u3092\u524A\u9664\u3059\u308B +options.pane.portrait.button.add_Background_Image = \u80CC\u666F\u753B\u50CF\u3092\u5909\u66F4\u3059\u308B +options.pane.portrait.button.add_Portrait_Image = \u30DD\u30FC\u30C8\u30EC\u30FC\u30C8\u753B\u50CF\u3092\u5909\u66F4\u3059\u308B +options.pane.portrait.button.remove_Background_Color = \u80CC\u666F\u8272\u3092\u524A\u9664\u3059\u308B +options.pane.portrait.button.remove_Background_Image = \u80CC\u666F\u753B\u50CF\u3092\u524A\u9664\u3059\u308B +options.pane.portrait.button.remove_Portrait_Image = \u30DD\u30FC\u30C8\u30EC\u30FC\u30C8\u753B\u50CF\u3092\u524A\u9664\u3059\u308B options.pane.portrait.color.prompt = \u30DD\u30FC\u30C8\u30EC\u30FC\u30C8\u753B\u50CF\u306E\u80CC\u5F8C\u3067\u4F7F\u7528\u3059\u308B\u8272\u3092\u9078\u629E\u3059\u308B options.pane.portrait.label.Background_Color = \u80CC\u666F\u8272 options.pane.portrait.label.Gaussian_Blur = \u30AC\u30A6\u30B9\u307C\u304B\u3057 options.pane.portrait.label.Glow = \u30B0\u30ED\u30FC options.pane.portrait.label.Opacity = \u4E0D\u900F\u660E\u5EA6 +options.pane.portrait.label.effects = \u30DD\u30FC\u30C8\u30EC\u30FC\u30C8\u30A8\u30D5\u30A7\u30AF\u30C8 +options.pane.portrait.title = \u80CC\u666F\u30AA\u30D7\u30B7\u30E7\u30F3 pane.left.title = \u3053\u3053\u306B\u753B\u50CF\u3092\u30C9\u30E9\u30C3\u30B0\u307E\u305F\u306F\u30DA\u30FC\u30B9\u30C8\u3059\u308B... diff --git a/src/main/resources/net/rptools/tokentool/image/corner-drag-35px.png b/src/main/resources/net/rptools/tokentool/image/corner-drag-35px.png index 5b2fe43..901ec5e 100644 Binary files a/src/main/resources/net/rptools/tokentool/image/corner-drag-35px.png and b/src/main/resources/net/rptools/tokentool/image/corner-drag-35px.png differ diff --git a/src/main/resources/net/rptools/tokentool/image/creative_commons_portrait.png b/src/main/resources/net/rptools/tokentool/image/creative_commons_portrait.png index 41dfcc1..4c52198 100644 Binary files a/src/main/resources/net/rptools/tokentool/image/creative_commons_portrait.png and b/src/main/resources/net/rptools/tokentool/image/creative_commons_portrait.png differ diff --git a/src/main/resources/net/rptools/tokentool/image/side-drag-35px.png b/src/main/resources/net/rptools/tokentool/image/side-drag-35px.png index fe9b511..0a83fa2 100644 Binary files a/src/main/resources/net/rptools/tokentool/image/side-drag-35px.png and b/src/main/resources/net/rptools/tokentool/image/side-drag-35px.png differ diff --git a/src/main/resources/net/rptools/tokentool/overlays/v2/Create/Blank.psd b/src/main/resources/net/rptools/tokentool/overlays/v2/Create/Blank.psd new file mode 100644 index 0000000..f72bef5 Binary files /dev/null and b/src/main/resources/net/rptools/tokentool/overlays/v2/Create/Blank.psd differ diff --git a/src/main/resources/net/rptools/tokentool/overlays/v2_1/Cards/European.png b/src/main/resources/net/rptools/tokentool/overlays/v2_1/Cards/European.png new file mode 100644 index 0000000..b25797c Binary files /dev/null and b/src/main/resources/net/rptools/tokentool/overlays/v2_1/Cards/European.png differ diff --git a/src/main/resources/net/rptools/tokentool/overlays/v2_1/Cards/Gear.jpg b/src/main/resources/net/rptools/tokentool/overlays/v2_1/Cards/Gear.jpg new file mode 100644 index 0000000..5b5a942 Binary files /dev/null and b/src/main/resources/net/rptools/tokentool/overlays/v2_1/Cards/Gear.jpg differ diff --git a/src/main/resources/net/rptools/tokentool/overlays/v2_1/Cards/Magic Armor.jpg b/src/main/resources/net/rptools/tokentool/overlays/v2_1/Cards/Magic Armor.jpg new file mode 100644 index 0000000..36b138e Binary files /dev/null and b/src/main/resources/net/rptools/tokentool/overlays/v2_1/Cards/Magic Armor.jpg differ diff --git a/src/main/resources/net/rptools/tokentool/overlays/v2_1/Cards/Magic Item.png b/src/main/resources/net/rptools/tokentool/overlays/v2_1/Cards/Magic Item.png new file mode 100644 index 0000000..0b91b3b Binary files /dev/null and b/src/main/resources/net/rptools/tokentool/overlays/v2_1/Cards/Magic Item.png differ diff --git a/src/main/resources/net/rptools/tokentool/overlays/v2_1/Cards/Magic Weapon.png b/src/main/resources/net/rptools/tokentool/overlays/v2_1/Cards/Magic Weapon.png new file mode 100644 index 0000000..fde9ebc Binary files /dev/null and b/src/main/resources/net/rptools/tokentool/overlays/v2_1/Cards/Magic Weapon.png differ diff --git a/src/main/resources/net/rptools/tokentool/overlays/v2_1/Cards/Mutation.jpg b/src/main/resources/net/rptools/tokentool/overlays/v2_1/Cards/Mutation.jpg new file mode 100644 index 0000000..a8875fc Binary files /dev/null and b/src/main/resources/net/rptools/tokentool/overlays/v2_1/Cards/Mutation.jpg differ diff --git a/src/main/resources/net/rptools/tokentool/overlays/v2_1/Cards/Proxy.png b/src/main/resources/net/rptools/tokentool/overlays/v2_1/Cards/Proxy.png new file mode 100644 index 0000000..f12694e Binary files /dev/null and b/src/main/resources/net/rptools/tokentool/overlays/v2_1/Cards/Proxy.png differ diff --git a/src/main/resources/net/rptools/tokentool/overlays/v2_1/Cards/Threat.jpg b/src/main/resources/net/rptools/tokentool/overlays/v2_1/Cards/Threat.jpg new file mode 100644 index 0000000..f0d694c Binary files /dev/null and b/src/main/resources/net/rptools/tokentool/overlays/v2_1/Cards/Threat.jpg differ diff --git a/src/main/resources/net/rptools/tokentool/overlays/v2_1/Cards/Town.jpg b/src/main/resources/net/rptools/tokentool/overlays/v2_1/Cards/Town.jpg new file mode 100644 index 0000000..9493546 Binary files /dev/null and b/src/main/resources/net/rptools/tokentool/overlays/v2_1/Cards/Town.jpg differ diff --git a/src/main/resources/net/rptools/tokentool/overlays/v2_1/Cards/Wanted.jpg b/src/main/resources/net/rptools/tokentool/overlays/v2_1/Cards/Wanted.jpg new file mode 100644 index 0000000..1134a54 Binary files /dev/null and b/src/main/resources/net/rptools/tokentool/overlays/v2_1/Cards/Wanted.jpg differ diff --git a/src/main/resources/net/rptools/tokentool/overlays/v2_1/Round/Decorated/Leaves, Large.psd b/src/main/resources/net/rptools/tokentool/overlays/v2_1/Round/Decorated/Leaves, Large.psd new file mode 100644 index 0000000..cbe3acf Binary files /dev/null and b/src/main/resources/net/rptools/tokentool/overlays/v2_1/Round/Decorated/Leaves, Large.psd differ diff --git a/src/main/resources/net/rptools/tokentool/overlays/v2_1/Round/Decorated/Stargate.psd b/src/main/resources/net/rptools/tokentool/overlays/v2_1/Round/Decorated/Stargate.psd new file mode 100644 index 0000000..66d580f Binary files /dev/null and b/src/main/resources/net/rptools/tokentool/overlays/v2_1/Round/Decorated/Stargate.psd differ diff --git a/src/main/resources/net/rptools/tokentool/overlays/v2_1/Round/Runes/Silver and Blue with Vortex.psd b/src/main/resources/net/rptools/tokentool/overlays/v2_1/Round/Runes/Silver and Blue with Vortex.psd new file mode 100644 index 0000000..3789486 Binary files /dev/null and b/src/main/resources/net/rptools/tokentool/overlays/v2_1/Round/Runes/Silver and Blue with Vortex.psd differ diff --git a/src/main/resources/net/rptools/tokentool/overlays/v2_1/Round/Runes/Silver and Blue.psd b/src/main/resources/net/rptools/tokentool/overlays/v2_1/Round/Runes/Silver and Blue.psd new file mode 100644 index 0000000..80902f4 Binary files /dev/null and b/src/main/resources/net/rptools/tokentool/overlays/v2_1/Round/Runes/Silver and Blue.psd differ diff --git a/src/main/resources/net/rptools/tokentool/view/PdfView.fxml b/src/main/resources/net/rptools/tokentool/view/PdfView.fxml new file mode 100644 index 0000000..da739c2 --- /dev/null +++ b/src/main/resources/net/rptools/tokentool/view/PdfView.fxml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/net/rptools/tokentool/view/RegionSelector.fxml b/src/main/resources/net/rptools/tokentool/view/RegionSelector.fxml index 86d4b77..967546a 100644 --- a/src/main/resources/net/rptools/tokentool/view/RegionSelector.fxml +++ b/src/main/resources/net/rptools/tokentool/view/RegionSelector.fxml @@ -10,7 +10,7 @@ - + diff --git a/src/main/resources/net/rptools/tokentool/view/TokenTool.fxml b/src/main/resources/net/rptools/tokentool/view/TokenTool.fxml index 9badcc7..42ae763 100644 --- a/src/main/resources/net/rptools/tokentool/view/TokenTool.fxml +++ b/src/main/resources/net/rptools/tokentool/view/TokenTool.fxml @@ -10,15 +10,20 @@ + + + + + @@ -33,42 +38,58 @@ - +
- + - - - - - + + + + + + + + + + + + + + + - + - + + + + + - + - + - + - + - + - + + + @@ -77,15 +98,20 @@ - + - - - - - + + + + + + + + + + @@ -102,9 +128,38 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -131,43 +186,11 @@ - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -205,14 +362,20 @@ - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -304,4 +476,4 @@ - + \ No newline at end of file diff --git a/src/main/resources/sentry.properties b/src/main/resources/sentry.properties deleted file mode 100644 index 2a8a534..0000000 --- a/src/main/resources/sentry.properties +++ /dev/null @@ -1,3 +0,0 @@ -dsn=https://e5982b40bfb447a98ca70c00e7b071ef:a4cb102cd9da4b02bd914a1da674b524@sentry.io/230233 -environment=RPTools-Production -stacktrace.app.packages=net.rptools.tokentool