.
*/
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 extends Number> 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 extends Number> 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 extends Number> 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 extends Number> 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 extends Number> 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 extends Number> 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 extends Number> 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 extends Number> 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 extends MouseEvent> 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 extends MouseEvent> 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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+