package mara.mybox.controller; import java.awt.image.BufferedImage; import java.io.File; import java.text.MessageFormat; import java.util.Date; import java.util.List; import java.util.Optional; import java.util.Random; import java.util.Timer; import java.util.TimerTask; import javafx.application.Platform; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.concurrent.Task; import javafx.embed.swing.SwingFXUtils; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.Alert; import javafx.scene.control.Button; import javafx.scene.control.ButtonType; import javafx.scene.control.CheckBox; import javafx.scene.control.ComboBox; import javafx.scene.control.ContentDisplay; import javafx.scene.control.Control; import javafx.scene.control.Label; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; import javafx.scene.control.Tab; import javafx.scene.control.TabPane; import javafx.scene.control.TextField; import javafx.scene.control.TitledPane; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.KeyEvent; import javafx.scene.input.MouseEvent; import javafx.scene.layout.HBox; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; import javafx.scene.paint.Color; import javafx.scene.shape.Rectangle; import javafx.stage.FileChooser; import javafx.stage.Modality; import javafx.stage.Stage; import javafx.util.Callback; import mara.mybox.data.VisitHistory; import mara.mybox.db.TableImageHistory; import mara.mybox.fxml.FxmlColor; import mara.mybox.fxml.FxmlControl; import static mara.mybox.fxml.FxmlControl.badStyle; import static mara.mybox.fxml.FxmlControl.darkRedText; import mara.mybox.fxml.FxmlImageManufacture; import mara.mybox.fxml.FxmlStage; import mara.mybox.fxml.RecentVisitMenu; import mara.mybox.image.ImageClipboard; import mara.mybox.image.ImageFileInformation; import mara.mybox.image.ImageHistory; import mara.mybox.image.ImageInformation; import mara.mybox.image.ImageManufacture; import mara.mybox.image.ImageScope; import mara.mybox.image.file.ImageFileReaders; import mara.mybox.image.file.ImageFileWriters; import mara.mybox.tools.DateTools; import mara.mybox.tools.FileTools; import mara.mybox.value.AppVariables; import static mara.mybox.value.AppVariables.logger; import static mara.mybox.value.AppVariables.message; import mara.mybox.value.CommonValues; /** * @Author Mara * @CreateDate 2019-8-12 * @License Apache License Version 2.0 */ public class ImageManufactureController extends ImageViewerController { protected SimpleBooleanProperty imageLoaded, imageUpdated; protected SimpleIntegerProperty hisIndex; protected String imageHistoriesPath; protected boolean pickingColor, sychronizeZoom; protected int zoomStep, newWidth, newHeight; protected File refFile; protected ImageInformation refInformation; protected SimpleBooleanProperty editable; public static enum ImageOperation { Load, History, Saved, Recover, Clipboard, Paste, Arc, Color, Crop, Copy, Text, RichText, Effects, Enhancement, Shadow, Scale, Picture, Transform, Pen, Margins, Mosaic, Convolution } @FXML protected TitledPane newPane; @FXML protected VBox displayBox, imageBox, rightPaneBox; @FXML protected Label refLabel, imageTipsLabel; @FXML protected ImageView refView, scopeView; @FXML protected Rectangle bgRect; @FXML protected Button paletteButton; @FXML protected TextField newWidthInput, newHeightInput; @FXML protected ImageManufactureOperationController operationController; @FXML protected ImageManufacturePaneController currentImageController, hisImageController, refImageController; @FXML protected TabPane imageTabs; @FXML protected Tab currentImageTab, hisImageTab, refImageTab; @FXML protected Button imageManuHisAsCurrentButton, imageManuHisDeleteButton, imageManuHisClearButton, imageManuHisNextButton, imageManuHisPreviousButton; @FXML protected HBox commonBar, currentImageBar; @FXML protected ComboBox<ImageHistory> hisBox; public ImageManufactureController() { baseTitle = AppVariables.message("ImageManufacture"); TipsLabelKey = "ImageManufactureTips"; } @Override public void initValues() { super.initValues(); imageLoaded = new SimpleBooleanProperty(false); imageUpdated = new SimpleBooleanProperty(false); editable = new SimpleBooleanProperty(false); hisIndex = new SimpleIntegerProperty(-1); if (imageHistoriesPath == null) { imageHistoriesPath = AppVariables.getImageHisPath(); } zoomStep = 10; sychronizeZoom = AppVariables.getUserConfigBoolean("ImageZoomSychronize", true); } @Override public void initializeNext() { try { currentImageController.parent = this; hisImageController.parent = this; refImageController.parent = this; initLeftPane(); initRightPane(); initImageBox(); } catch (Exception e) { logger.error(e.toString()); } } @Override public void keyEventsHandler(KeyEvent event) { super.keyEventsHandler(event); // logger.debug(event.getCode() + " " + event.getText()); operationController.eventsHandler(event); // pass event to right pane } @Override public void controlHandler(KeyEvent event) { super.controlHandler(event); if (event.isControlDown()) { String key = event.getText(); if (key == null || key.isEmpty()) { return; } switch (key) { case "p": case "P": if (!popButton.isDisabled()) { popAction(); } break; default: break; } } } @Override public void altHandler(KeyEvent event) { super.altHandler(event); if (event.isAltDown()) { String key = event.getText(); if (key == null || key.isEmpty()) { return; } switch (key) { case "p": case "P": if (!popButton.isDisabled()) { popAction(); } break; default: break; } } } @FXML @Override public void okAction() { operationController.okAction(); } @FXML public void settingsAction() { SettingsController controller = (SettingsController) openStage(CommonValues.SettingsFxml); controller.setParentController(this); controller.setParentFxml(myFxml); controller.tabPane.getSelectionModel().select(controller.imageTab); } /* Left Pane */ protected void initLeftPane() { try { fileBox.disableProperty().bind(imageLoaded.not()); saveAsPane.disableProperty().bind(imageLoaded.not()); browsePane.disableProperty().bind(imageLoaded.not()); saveButton.disableProperty().bind(imageUpdated.not()); newPane.expandedProperty().addListener( (ObservableValue<? extends Boolean> ov, Boolean oldValue, Boolean newValue) -> { AppVariables.setUserConfigValue("ImageManufactureNewPane", newPane.isExpanded()); }); newPane.setExpanded(AppVariables.getUserConfigBoolean("ImageManufactureNewPane", true)); newWidthInput.textProperty().addListener( (ObservableValue<? extends String> ov, String oldValue, String newValue) -> { try { int v = Integer.parseInt(newValue); if (v > 0) { newWidth = v; newWidthInput.setStyle(null); } else { newWidthInput.setStyle(badStyle); } } catch (Exception e) { newWidthInput.setStyle(badStyle); } }); newHeightInput.textProperty().addListener( (ObservableValue<? extends String> ov, String oldValue, String newValue) -> { try { int v = Integer.parseInt(newValue); if (v > 0) { newHeight = v; newHeightInput.setStyle(null); } else { newHeightInput.setStyle(badStyle); } } catch (Exception e) { newHeightInput.setStyle(badStyle); } }); try { String c = AppVariables.getUserConfigValue("NewBackgroundColor", Color.TRANSPARENT.toString()); bgRect.setFill(Color.web(c)); } catch (Exception e) { bgRect.setFill(Color.TRANSPARENT); AppVariables.setUserConfigValue("NewBackgroundColor", Color.TRANSPARENT.toString()); } FxmlControl.setTooltip(bgRect, FxmlColor.colorNameDisplay((Color) bgRect.getFill())); newWidthInput.setText("500"); newHeightInput.setText("500"); saveConfirmCheck.selectedProperty().addListener(new ChangeListener<Boolean>() { @Override public void changed(ObservableValue ov, Boolean oldValue, Boolean newValue) { AppVariables.setUserConfigValue("ImageConfirmSave", saveConfirmCheck.isSelected()); } }); saveConfirmCheck.setSelected(AppVariables.getUserConfigBoolean("ImageConfirmSave", true)); createButton.disableProperty().bind( newWidthInput.styleProperty().isEqualTo(badStyle) .or(newHeightInput.styleProperty().isEqualTo(badStyle)) ); initSaveAsPane(); initBrowsePane(); initTipsPane(); } catch (Exception e) { logger.error(e.toString()); } } @Override public boolean setColor(Control control, Color color) { if (control == null || color == null) { return false; } if (paletteButton.equals(control)) { bgRect.setFill(color); FxmlControl.setTooltip(bgRect, FxmlColor.colorNameDisplay(color)); AppVariables.setUserConfigValue("NewBackgroundColor", color.toString()); } return true; } @FXML @Override public void showPalette(ActionEvent event) { showPalette(paletteButton, message("BackgroundColor"), false); } @FXML @Override public void createAction() { if (!checkBeforeNextAction()) { return; } Image newImage = FxmlImageManufacture.createImage(newWidth, newHeight, (Color) bgRect.getFill()); loadImage(newImage); if (operationController.myPane != operationController.marginsPane) { operationController.expandPane(operationController.marginsPane); } else { operationController.accordionPane.setExpandedPane(operationController.marginsPane); } } /* Right Pane */ protected void initRightPane() { try { rightPaneBox.disableProperty().bind(imageLoaded.not()); operationController.initPane(this); } catch (Exception e) { logger.error(e.toString()); } } public void operationChanged(ImageManufactureOperationController c) { operationController = c; // switch (operationController.operation) { // case Clipboard: // break; // case Crop: // imageTabs.getSelectionModel().select(currentImageTab); // break; // } } public void pickColor(boolean picking) { pickingColor = picking; if (picking) { imageLabel.setText(message("PickingColorNow")); imageLabel.setStyle(FxmlControl.darkRedText); } else { imageLabel.setText(""); imageLabel.setStyle(FxmlControl.blueText); } } /* Image Box */ protected void initImageBox() { try { imageBox.disableProperty().bind(imageLoaded.not()); leftPaneControl.visibleProperty().bind(imageLoaded); rightPaneControl.visibleProperty().bind(imageLoaded); editable.bind(imageTabs.getSelectionModel().selectedItemProperty().isEqualTo(currentImageTab)); imageUpdated.addListener(new ChangeListener<Boolean>() { @Override public void changed(ObservableValue<? extends Boolean> ov, Boolean oldVal, Boolean newVal) { if (sourceFile != null) { String title = getBaseTitle() + " " + sourceFile.getAbsolutePath(); if (imageInformation != null) { if (imageInformation.getImageFileInformation().getNumberOfImages() > 1) { title += " - " + message("Image") + " " + imageInformation.getIndex(); } if (imageInformation.isIsSampled()) { title += " - " + message("Sampled"); } } if (imageUpdated.get()) { title += " " + "*"; } getMyStage().setTitle(title); } } }); isPickingColor.addListener(new ChangeListener<Boolean>() { @Override public void changed(ObservableValue<? extends Boolean> ov, Boolean oldVal, Boolean newVal) { imageLabel.setStyle(darkRedText); imageLabel.setText(message("PickingColorsNow")); } }); currentImageController.init(this); hisImageController.init(this); refImageController.init(this); currentImageBar.disableProperty().bind(editable.not()); cropButton.disableProperty().bind(currentImageController.typeGroup.selectedToggleProperty().isNull()); redoButton.disableProperty().bind(hisIndex.lessThanOrEqualTo(0)); recoverButton.disableProperty().bind(imageUpdated.not()); imageTabs.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<Tab>() { @Override public void changed(ObservableValue ov, Tab oldValue, Tab newValue) { if (newValue.equals(currentImageTab)) { } else { if (currentImageController.getPaletteController() != null) { currentImageController.getPaletteController().closeStage(); currentImageController.setPaletteController(null); } } if (newValue.equals(hisImageTab)) { } else if (hisImageController.getPaletteController() != null) { hisImageController.getPaletteController().closeStage(); hisImageController.setPaletteController(null); } if (newValue.equals(refImageTab)) { } else if (refImageController.getPaletteController() != null) { refImageController.getPaletteController().closeStage(); refImageController.setPaletteController(null); } } }); initHisImageTab(); } catch (Exception e) { logger.error(e.toString()); } } @Override protected void checkRulerX() { currentImageController.drawMaskRulerX(); hisImageController.drawMaskRulerX(); refImageController.drawMaskRulerX(); } @Override protected void checkRulerY() { currentImageController.drawMaskRulerY(); hisImageController.drawMaskRulerY(); refImageController.drawMaskRulerY(); } @Override protected void checkCoordinate() { currentImageController.checkCoordinate(); hisImageController.checkCoordinate(); refImageController.checkCoordinate(); } @FXML @Override public void copyAction() { if (operationController.myPane == operationController.textPane || operationController.myPane == operationController.richTextPane) { return; } copy(false); } public void copy(boolean crop) { synchronized (this) { if (task != null) { return; } task = new SingletonTask<Void>() { private Image newImage; @Override protected boolean handle() { try { Image srcImage; ImageScope scope; if (imageTabs.getSelectionModel().getSelectedItem() == currentImageTab) { srcImage = currentImageController.image; scope = currentImageController.scope; } else if (imageTabs.getSelectionModel().getSelectedItem() == hisImageTab) { srcImage = hisImageController.image; scope = hisImageController.scope; } else if (imageTabs.getSelectionModel().getSelectedItem() == refImageTab) { srcImage = refImageController.image; scope = refImageController.scope; } else { return false; } if (scope == null || scope.getScopeType() == ImageScope.ScopeType.All || scope.getScopeType() == ImageScope.ScopeType.Operate) { newImage = srcImage; } else { switch (scope.getScopeType()) { case Matting: case Color: case RectangleColor: case CircleColor: case EllipseColor: case PolygonColor: ImageScope tmpScope = scope.cloneValues(); tmpScope.setColorExcluded(!scope.isColorExcluded()); newImage = FxmlImageManufacture.crop(srcImage, tmpScope, Color.TRANSPARENT, true); break; case Outline: case Rectangle: case Circle: case Ellipse: case Polygon: newImage = FxmlImageManufacture.crop(srcImage, scope, Color.TRANSPARENT, !scope.isAreaExcluded()); } } if (task == null || isCancelled()) { return false; } return ImageClipboard.add(newImage) != null; } catch (Exception e) { logger.debug(e.toString()); error = e.toString(); return false; } } @Override protected void whenSucceeded() { updateBottom(ImageOperation.Copy); popText(AppVariables.message(ImageOperation.Copy.name()) + " " + message("Successful"), AppVariables.getCommentsDelay(), "white", "1.5em", null); if (operationController.myPane == operationController.clipboardPane) { ((ImageManufactureClipboardController) operationController).loadClipboard(); } if (crop) { crop(); } } }; openHandlingStage(task, Modality.WINDOW_MODAL); Thread thread = new Thread(task); thread.setDaemon(true); thread.start(); } } /* Current Image */ @Override public void afterImageLoaded() { try { undoButton.setDisable(true); if (image == null) { return; } if (imageInformation == null) { setImageChanged(true); } else if (!imageInformation.isIsSampled()) { setImageChanged(imageInformation.isIsScaled()); } if (imageInformation != null && imageInformation.isIsSampled()) { sampledTips.setVisible(true); loadWidth = (int) image.getWidth(); loadSampledImage(); } else { sampledTips.setVisible(false); } if (imageInformation != null) { if (imageInformation.getImageType() == BufferedImage.TYPE_BYTE_INDEXED && imageInformation.getColorChannels() == 4) { Alert alert = new Alert(Alert.AlertType.WARNING); if (sourceFile != null) { alert.setTitle(sourceFile.getAbsolutePath()); } else { alert.setTitle(getBaseTitle()); } alert.setContentText(AppVariables.message("IndexedAlphaWarning")); alert.getDialogPane().setMinHeight(Region.USE_PREF_SIZE); ButtonType buttonConvert = new ButtonType(AppVariables.message("Convert")); ButtonType buttonISee = new ButtonType(AppVariables.message("ISee")); alert.getButtonTypes().setAll(buttonConvert, buttonISee); Stage stage = (Stage) alert.getDialogPane().getScene().getWindow(); stage.setAlwaysOnTop(true); stage.toFront(); Optional<ButtonType> result = alert.showAndWait(); if (result.get() == buttonConvert) { openStage(CommonValues.ImageConverterBatchFxml); return; } } } String title = getMyStage().getTitle(); currentImageController.init(sourceFile, image, title + " - " + message("CurrentImage")); hisImageController.init(sourceFile, image, title + " - " + message("HistoricalImage")); refImageController.init(sourceFile, image, title + " - " + message("ReferenceImage")); currentImageController.clearValues(); imageLoaded.set(true); imageUpdated.set(false); imageView = currentImageController.imageView; xZoomStep = (int) (image.getWidth() * zoomStep / 100); yZoomStep = (int) (image.getHeight() * zoomStep / 100); currentImageController.xZoomStep = xZoomStep; currentImageController.yZoomStep = yZoomStep; hisImageController.xZoomStep = xZoomStep; hisImageController.yZoomStep = yZoomStep; refImageController.xZoomStep = xZoomStep; refImageController.yZoomStep = yZoomStep; refInformation = imageInformation; operationController.expandPane(operationController.myPane); hisIndex.set(-1); loadImageHistories(); makeImageNevigator(); updateBottom(ImageOperation.Load); } catch (Exception e) { logger.debug(e.toString()); } } public ImageScope scope() { return currentImageController.scope; } @FXML @Override public void loadedSize() { if (sychronizeZoom) { currentImageController.loadedSize(); hisImageController.loadedSize(); refImageController.paneSize(); } else { if (currentImageTab.isSelected()) { currentImageController.loadedSize(); } else if (refImageTab.isSelected()) { refImageController.loadedSize(); } else if (hisImageTab.isSelected()) { hisImageController.loadedSize(); } } } @FXML @Override public void paneSize() { if (sychronizeZoom) { currentImageController.paneSize(); hisImageController.paneSize(); refImageController.paneSize(); } else { if (currentImageTab.isSelected()) { currentImageController.paneSize(); } else if (refImageTab.isSelected()) { refImageController.paneSize(); } else if (hisImageTab.isSelected()) { hisImageController.paneSize(); } } } @Override public void fitSize() { if (sychronizeZoom) { currentImageController.fitSize(); hisImageController.fitSize(); refImageController.fitSize(); } else { if (currentImageTab.isSelected()) { currentImageController.fitSize(); } else if (refImageTab.isSelected()) { refImageController.fitSize(); } else if (hisImageTab.isSelected()) { hisImageController.fitSize(); } } } @FXML @Override public void zoomIn() { if (sychronizeZoom) { currentImageController.zoomIn(); hisImageController.zoomIn(); refImageController.zoomIn(); } else { if (currentImageTab.isSelected()) { currentImageController.zoomIn(); } else if (refImageTab.isSelected()) { refImageController.zoomIn(); } else if (hisImageTab.isSelected()) { hisImageController.zoomIn(); } } } @FXML @Override public void zoomOut() { if (sychronizeZoom) { currentImageController.zoomOut(); hisImageController.zoomOut(); refImageController.zoomOut(); } else { if (currentImageTab.isSelected()) { currentImageController.zoomOut(); } else if (refImageTab.isSelected()) { refImageController.zoomOut(); } else if (hisImageTab.isSelected()) { hisImageController.zoomOut(); } } } @FXML public void refAction() { } @FXML public void popAction() { ImageViewerController controller = (ImageViewerController) openStage(CommonValues.ImageViewerFxml); if (currentImageTab.isSelected()) { controller.loadImage(currentImageController.imageView.getImage()); } else if (refImageTab.isSelected()) { controller.loadImage(refImageController.imageView.getImage()); } else if (hisImageTab.isSelected()) { controller.loadImage(hisImageController.imageView.getImage()); } controller.setAsPopped(); } public void updateBottom(ImageOperation operation) { try { String bottom = message(operation.name()) + " "; if (imageInformation != null) { bottom += message("Format") + ":" + imageInformation.getImageFormat() + " "; bottom += message("Pixels") + ":" + imageInformation.getWidth() + "x" + imageInformation.getHeight() + " "; } bottom += message("LoadedSize") + ":" + (int) imageView.getImage().getWidth() + "x" + (int) imageView.getImage().getHeight() + " " + message("DisplayedSize") + ":" + (int) imageView.getFitWidth() + "x" + (int) imageView.getFitHeight(); if (sourceFile != null) { bottom += " " + message("FileSize") + ":" + FileTools.showFileSize(sourceFile.length()) + " " + message("ModifyTime") + ":" + DateTools.datetimeToString(sourceFile.lastModified()) + " "; } bottomLabel.setText(bottom); } catch (Exception e) { logger.debug(e.toString()); } } /* History Image */ protected String hisDesc(ImageHistory his) { String s = DateTools.datetimeToString(his.getOperationTime()) + " " + message(his.getUpdateType()); if (his.getObjectType() != null && !his.getObjectType().isEmpty()) { s += " " + message(his.getObjectType()); } if (his.getOpType() != null && !his.getOpType().isEmpty()) { s += " " + message(his.getOpType()); } if (his.getScopeType() != null && !his.getScopeType().isEmpty()) { s += " " + message(his.getScopeType()); } if (his.getScopeName() != null && !his.getScopeName().isEmpty()) { s += " " + message(his.getScopeName()); } return s; } protected void initHisImageTab() { hisBox.setButtonCell(new ListCell<ImageHistory>() { { setContentDisplay(ContentDisplay.TEXT_ONLY); } @Override protected void updateItem(ImageHistory item, boolean empty) { super.updateItem(item, empty); if (empty || item == null) { setText(null); return; } setText(hisDesc(item)); } }); hisBox.setCellFactory(new Callback<ListView<ImageHistory>, ListCell<ImageHistory>>() { @Override public ListCell<ImageHistory> call(ListView<ImageHistory> param) { ListCell<ImageHistory> cell = new ListCell<ImageHistory>() { private final ImageView view; { setContentDisplay(ContentDisplay.LEFT); view = new ImageView(); view.setPreserveRatio(true); } @Override protected void updateItem(ImageHistory item, boolean empty) { super.updateItem(item, empty); if (empty || item == null) { setText(null); setGraphic(null); return; } String s = hisDesc(item); if (getIndex() == hisIndex.get()) { setStyle("-fx-text-fill: #961c1c; -fx-font-weight: bolder;"); s = "** " + message("CurrentImage") + " " + s; } else { setStyle(""); } view.setFitWidth(AppVariables.getUserConfigInt("ThumbnailWidth", 100)); view.setImage(item.getThumbnail()); setGraphic(view); setText(s); } }; return cell; } }); hisBox.setVisibleRowCount(6); hisBox.getSelectionModel().selectedIndexProperty().addListener(new ChangeListener<Number>() { @Override public void changed(ObservableValue<? extends Number> ov, Number oldVal, Number newVal) { loadImageHistory(newVal.intValue(), false); } }); imageManuHisAsCurrentButton.disableProperty().bind(hisBox.getSelectionModel().selectedItemProperty().isNull()); imageManuHisDeleteButton.disableProperty().bind(hisBox.getSelectionModel().selectedItemProperty().isNull()); checkHisNavigateButtons(); } public void checkHisNavigateButtons() { if (hisBox.getItems() == null || hisBox.getItems().isEmpty()) { imageManuHisPreviousButton.setDisable(true); imageManuHisNextButton.setDisable(true); return; } int select = hisBox.getSelectionModel().getSelectedIndex(); imageManuHisPreviousButton.setDisable(select <= 0); imageManuHisNextButton.setDisable(select >= hisBox.getItems().size() - 1); } protected void loadImageHistories() { synchronized (this) { if (loadTask != null) { return; } hisBox.getItems().clear(); hisIndex.set(-1); int max = AppVariables.getUserConfigInt("MaxImageHistories", 20); if (max <= 0 || sourceFile == null) { return; } hisBox.setPromptText(message("Loading...")); loadTask = new Task<Void>() { private List<ImageHistory> list; private File currentFile; @Override protected Void call() { try { currentFile = sourceFile; int max = AppVariables.getUserConfigInt("MaxImageHistories", 20); if (max <= 0 || currentFile == null) { return null; } list = TableImageHistory.read(currentFile.getAbsolutePath()); if (list != null) { for (ImageHistory his : list) { if (loadTask == null || isCancelled() || !currentFile.equals(sourceFile)) { return null; } loadThumbnail(his); } } } catch (Exception e) { logger.debug(e.toString()); } return null; } @Override protected void succeeded() { super.succeeded(); loadTask = null; Platform.runLater(new Runnable() { @Override public void run() { if (currentFile.equals(sourceFile)) { hisBox.setPromptText(""); if (list != null) { if (currentFile.equals(sourceFile)) { hisBox.getItems().addAll(list); } } checkHisNavigateButtons(); recordImageHistory(ImageOperation.Load, image); } } }); } @Override protected void failed() { super.failed(); loadTask = null; } @Override protected void cancelled() { super.cancelled(); loadTask = null; } }; Thread thread = new Thread(loadTask); // openHandlingStage(loadTask, Modality.WINDOW_MODAL); thread.setDaemon(true); thread.start(); } } protected void recordImageHistory(final ImageOperation operation, final Image newImage) { recordImageHistory(operation, null, null, newImage); } protected void recordImageHistory(final ImageOperation operation, String objectType, String opType, final Image newImage) { if (operation == ImageOperation.Load && !AppVariables.getUserConfigBoolean("RecordImageLoad", true)) { return; } int max = AppVariables.getUserConfigInt("MaxImageHistories", 20); if (sourceFile == null || max <= 0 || operation == null || newImage == null) { return; } if (imageHistoriesPath == null) { imageHistoriesPath = AppVariables.getImageHisPath(); } hisBox.setPromptText(message("Writing...")); synchronized (this) { if (task != null) { return; } task = new SingletonTask<Void>() { private File currentFile; private String finalname; private BufferedImage thumbnail; private String getFilename() { String name = imageHistoriesPath + File.separator + FileTools.getFilePrefix(currentFile.getName()) + "_" + (new Date().getTime()) + "_" + operation; if (objectType != null && !objectType.trim().isEmpty()) { name += "_" + objectType + "_" + new Random().nextInt(1000); } if (opType != null && !opType.trim().isEmpty()) { name += "_" + opType + "_" + new Random().nextInt(1000); } name += "_" + new Random().nextInt(1000); return name; } @Override protected boolean handle() { try { currentFile = sourceFile; BufferedImage bufferedImage = FxmlImageManufacture.getBufferedImage(newImage); if (isCancelled()) { return false; } String filename = getFilename(); while (new File(filename).exists()) { filename = getFilename(); } filename = new File(filename).getAbsolutePath(); finalname = new File(filename + ".png").getAbsolutePath(); ImageFileWriters.writeImageFile(bufferedImage, "png", finalname); if (isCancelled()) { return false; } thumbnail = ImageManufacture.scaleImageWidthKeep(bufferedImage, AppVariables.getUserConfigInt("ThumbnailWidth", 100)); String thumbname = new File(filename + "_thumbnail.png").getAbsolutePath(); if (isCancelled()) { return false; } return ImageFileWriters.writeImageFile(thumbnail, "png", thumbname); } catch (Exception e) { error = e.toString(); return false; } } @Override protected void whenSucceeded() { TableImageHistory.add(currentFile.getAbsolutePath(), finalname, operation.name(), objectType, opType, scope()); if (currentFile.equals(sourceFile)) { // The file may be changed while writing hisBox.setPromptText(""); ImageHistory his = new ImageHistory(); his.setImage(sourceFile.getAbsolutePath()); his.setHistoryLocation(finalname); his.setUpdateType(operation.name()); his.setObjectType(objectType); his.setOpType(opType); if (scope() != null) { his.setScopeType(scope().getScopeType().name()); his.setScopeName(scope().getName()); } his.setOperationTime(new Date()); his.setThumbnail(SwingFXUtils.toFXImage(thumbnail, null)); hisBox.getItems().add(0, his); hisIndex.set(0); undoButton.setDisable(false); checkHisNavigateButtons(); } } }; Thread thread = new Thread(task); // openHandlingStage(task, Modality.WINDOW_MODAL); thread.setDaemon(true); thread.start(); } } protected void loadThumbnail(ImageHistory his) { try { if (his == null) { return; } String fname = his.getHistoryLocation(); int width = AppVariables.getUserConfigInt("ThumbnailWidth", 100); String thumbname = FileTools.appendName(fname, "_thumbnail"); File thumbfile = new File(thumbname); BufferedImage bufferedImage; if (thumbfile.exists()) { bufferedImage = ImageFileReaders.readImage(thumbfile); } else { bufferedImage = ImageFileReaders.readFileByWidth("png", fname, width); } if (bufferedImage != null) { his.setThumbnail(SwingFXUtils.toFXImage(bufferedImage, null)); } } catch (Exception e) { logger.debug(e.toString()); } } protected void loadImageHistory(int index, boolean asCurrent) { synchronized (this) { if (loadTask != null) { return; } checkHisNavigateButtons(); int max = AppVariables.getUserConfigInt("MaxImageHistories", 20); if (max <= 0 || sourceFile == null) { undoButton.setDisable(true); return; } undoButton.setDisable(index < 0 || index >= hisBox.getItems().size() - 1); if (index < 0 || index > hisBox.getItems().size() - 1) { return; } loadTask = new Task<Void>() { private boolean ok; private Image hisImage; private String hisDesc; @Override protected Void call() { try { ImageHistory his = hisBox.getItems().get(index); File file = new File(his.getHistoryLocation()); if (!file.exists()) { TableImageHistory.deleteHistory(his.getImage(), his.getHistoryLocation()); Platform.runLater(new Runnable() { @Override public void run() { hisBox.getItems().remove(his); } }); return null; } BufferedImage bufferedImage = ImageFileReaders.readImage(file); if (bufferedImage != null) { hisImage = SwingFXUtils.toFXImage(bufferedImage, null); } hisDesc = DateTools.datetimeToString(his.getOperationTime()) + " " + message(his.getUpdateType()); ok = true; } catch (Exception e) { logger.debug(e.toString()); } return null; } @Override protected void succeeded() { super.succeeded(); loadTask = null; if (!ok) { return; } Platform.runLater(new Runnable() { @Override public void run() { if (asCurrent) { hisIndex.set(index); String info = MessageFormat.format(message("CurrentImageSetAs"), hisDesc); popText(info, AppVariables.getCommentsDelay(), "white", "1.5em", null); // Force listView to refresh // https://stackoverflow.com/questions/13906139/javafx-update-of-listview-if-an-element-of-observablelist-changes?r=SearchResults for (int i = 0; i < hisBox.getItems().size(); ++i) { hisBox.getItems().set(i, hisBox.getItems().get(i)); } loadImage(ImageOperation.History, hisImage); } else { hisImageController.updateImage(hisImage); } checkHisNavigateButtons(); } }); } @Override protected void failed() { super.failed(); loadTask = null; } @Override protected void cancelled() { super.cancelled(); loadTask = null; } }; Thread thread = new Thread(loadTask); openHandlingStage(loadTask, Modality.WINDOW_MODAL); thread.setDaemon(true); thread.start(); } } @FXML public void hisClearAction() { if (sourceFile == null) { return; } Alert alert = new Alert(Alert.AlertType.CONFIRMATION); alert.setTitle(getBaseTitle()); alert.setContentText(AppVariables.message("SureClear")); alert.getDialogPane().setMinHeight(Region.USE_PREF_SIZE); ButtonType buttonSure = new ButtonType(AppVariables.message("Sure")); ButtonType buttonCancel = new ButtonType(AppVariables.message("Cancel")); alert.getButtonTypes().setAll(buttonSure, buttonCancel); Stage stage = (Stage) alert.getDialogPane().getScene().getWindow(); stage.setAlwaysOnTop(true); stage.toFront(); Optional<ButtonType> result = alert.showAndWait(); if (result.get() != buttonSure) { return; } hisBox.getItems().clear(); TableImageHistory.clearImage(sourceFile.getAbsolutePath()); checkHisNavigateButtons(); } @FXML public void hisDeleteAction() { ImageHistory selected = hisBox.getSelectionModel().getSelectedItem(); if (selected == null) { return; } TableImageHistory.deleteHistory(selected.getImage(), selected.getHistoryLocation()); hisBox.getItems().remove(selected); checkHisNavigateButtons(); } @FXML public void hisAsCurrentAction() { loadImageHistory(hisBox.getSelectionModel().getSelectedIndex(), true); } @FXML public void hisNextAction() { int selected = hisBox.getSelectionModel().getSelectedIndex(); if (selected < 0) { hisBox.getSelectionModel().select(0); } else if (selected >= hisBox.getItems().size() - 1) { checkHisNavigateButtons(); } else { hisBox.getSelectionModel().select(selected + 1); } } @FXML public void hisPreviousAction() { int selected = hisBox.getSelectionModel().getSelectedIndex(); if (selected < 0) { hisBox.getSelectionModel().select(0); } else if (selected == 0) { checkHisNavigateButtons(); } else { hisBox.getSelectionModel().select(selected - 1); } } /* Reference */ @FXML public void selectReference() { try { final FileChooser fileChooser = new FileChooser(); File path = AppVariables.getUserConfigPath(sourcePathKey); if (path.exists()) { fileChooser.setInitialDirectory(path); } fileChooser.getExtensionFilters().addAll(sourceExtensionFilter); File file = fileChooser.showOpenDialog(getMyStage()); if (file == null) { return; } referenceSelected(file); } catch (Exception e) { // logger.error(e.toString()); } } public void referenceSelected(final File file) { try { if (file == null) { return; } recordFileOpened(file); synchronized (this) { if (task != null) { return; } task = new SingletonTask<Void>() { private ImageFileInformation finfo; private Image refImage; @Override protected boolean handle() { finfo = ImageFileReaders.readImageFileMetaData(file); if (finfo == null || finfo.getImageInformation() == null) { return false; } BufferedImage bufferedImage = ImageFileReaders.readImage(file); if (task == null || isCancelled() || bufferedImage == null) { return false; } refImage = SwingFXUtils.toFXImage(bufferedImage, null); if (task == null || isCancelled() || refImage == null) { return false; } return true; } @Override protected void whenSucceeded() { refFile = file; refInformation = finfo.getImageInformation(); refImageController.init(refFile, refImage, message("ReferenceImage")); refImageController.xZoomStep = (int) (refImage.getWidth() * zoomStep / 100); refImageController.yZoomStep = (int) (refImage.getHeight() * zoomStep / 100); } }; openHandlingStage(task, Modality.WINDOW_MODAL); Thread thread = new Thread(task); thread.setDaemon(true); thread.start(); } } catch (Exception e) { // logger.error(e.toString()); } } @FXML public void popRefFile(MouseEvent event) { if (AppVariables.fileRecentNumber <= 0) { return; } new RecentVisitMenu(this, event) { @Override public List<VisitHistory> recentFiles() { return recentSourceFiles(); } @Override public List<VisitHistory> recentPaths() { return recentSourcePathsBesidesFiles(); } @Override public void handleSelect() { selectReference(); } @Override public void handleFile(String fname) { File file = new File(fname); if (!file.exists()) { handleSelect(); return; } referenceSelected(file); } @Override public void handlePath(String fname) { handleSourcePath(fname); } }.pop(); } @FXML public void popRefInformation() { showImageInformation(refInformation); } @FXML public void popRefMeta() { showImageMetaData(refInformation); } /* Edit current image */ @FXML @Override public void undoAction() { if (undoButton.isDisabled()) { return; } loadImageHistory(hisIndex.get() + 1, true); currentImageController.clearValues(); } @FXML @Override public void redoAction() { if (redoButton.isDisabled()) { return; } loadImageHistory(hisIndex.get() - 1, true); currentImageController.clearValues(); } @FXML @Override public void recoverAction() { if (imageView == null) { return; } hisIndex.set(-1); currentImageController.updateImage(image); imageUpdated.set(false); updateBottom(ImageOperation.Recover); currentImageController.clearValues(); } @FXML @Override public void saveAction() { if (!editable.get() || saveButton.isDisabled()) { return; } if (sourceFile == null) { saveAsAction(); return; } if (saveConfirmCheck.isSelected()) { Alert alert = new Alert(Alert.AlertType.CONFIRMATION); alert.setTitle(getMyStage().getTitle()); alert.setContentText(AppVariables.message("SureOverrideFile")); alert.getDialogPane().setMinHeight(Region.USE_PREF_SIZE); ButtonType buttonSave = new ButtonType(AppVariables.message("Save")); ButtonType buttonSaveAs = new ButtonType(AppVariables.message("SaveAs")); ButtonType buttonCancel = new ButtonType(AppVariables.message("Cancel")); alert.getButtonTypes().setAll(buttonSave, buttonSaveAs, buttonCancel); Stage stage = (Stage) alert.getDialogPane().getScene().getWindow(); stage.setAlwaysOnTop(true); stage.toFront(); Optional<ButtonType> result = alert.showAndWait(); if (result.get() == buttonCancel) { return; } else if (result.get() == buttonSaveAs) { saveAsAction(); return; } } synchronized (this) { if (task != null) { return; } task = new SingletonTask<Void>() { @Override protected boolean handle() { String format = "png"; if (imageInformation != null) { format = imageInformation.getImageFormat(); } final BufferedImage bufferedImage = FxmlImageManufacture.getBufferedImage(currentImageController.image); if (bufferedImage == null || task == null || isCancelled()) { return false; } ok = ImageFileWriters.writeImageFile(bufferedImage, format, sourceFile.getAbsolutePath()); if (!ok || task == null || isCancelled()) { return false; } ImageFileInformation finfo = ImageFileReaders.readImageFileMetaData(sourceFile.getAbsolutePath()); if (finfo == null || finfo.getImageInformation() == null) { return false; } imageInformation = finfo.getImageInformation(); return true; } @Override protected void whenSucceeded() { imageUpdated.set(false); updateBottom(ImageOperation.Saved); } }; openHandlingStage(task, Modality.WINDOW_MODAL); Thread thread = new Thread(task); thread.setDaemon(true); thread.start(); } } @FXML @Override public void saveAsAction() { if (!editable.get() || saveAsButton.isDisabled()) { return; } try { final File file = chooseSaveFile(AppVariables.getUserConfigPath(targetPathKey), saveAsPrefix(), targetExtensionFilter, true); if (file == null) { return; } recordFileWritten(file); synchronized (this) { if (task != null) { return; } task = new SingletonTask<Void>() { @Override protected boolean handle() { String format = FileTools.getFileSuffix(file.getName()); final BufferedImage bufferedImage = FxmlImageManufacture.getBufferedImage(currentImageController.image); if (task == null || isCancelled()) { return false; } return ImageFileWriters.writeImageFile(bufferedImage, format, file.getAbsolutePath()); } @Override protected void whenSucceeded() { if (sourceFile == null || saveAsType == SaveAsType.Load) { sourceFileChanged(file); } else if (saveAsType == SaveAsType.Open) { openImageManufacture(file.getAbsolutePath()); } popSuccessful(); } }; openHandlingStage(task, Modality.WINDOW_MODAL); Thread thread = new Thread(task); thread.setDaemon(true); thread.start(); } } catch (Exception e) { logger.error(e.toString()); } } public void updateImage(ImageOperation operation, Image newImage, long cost) { if (!editable.get()) { return; } updateImage(operation, null, null, newImage, cost); } public void updateImage(ImageOperation operation, String objectType, String opType, Image newImage, long cost) { if (!editable.get()) { return; } try { currentImageController.updateImage(newImage); imageUpdated.set(true); recordImageHistory(operation, objectType, opType, newImage); String info; if (operation == ImageOperation.Scale) { info = AppVariables.message("Scale2") + " " + message("Successful"); } else { info = AppVariables.message(operation.name()) + " " + message("Successful"); } if (objectType != null) { info += " " + message(objectType); } if (opType != null) { info += " " + message(opType); } if (cost > 0) { info += " " + message("Cost") + ": " + DateTools.showTime(cost); } bottomLabel.setText(info); popText(info, AppVariables.getCommentsDelay(), "white", "1.5em", null); currentImageController.clearValues(); } catch (Exception e) { logger.debug(e.toString()); } } public void loadImage(ImageOperation operation, Image newImage) { try { currentImageController.updateImage(newImage); imageUpdated.set(true); updateBottom(operation); imageTabs.getSelectionModel().select(currentImageTab); currentImageController.clearValues(); } catch (Exception e) { logger.debug(e.toString()); } } // This should be implemented in frame since cropping happens anytime and right side may be other pane @FXML @Override public void cropAction() { if (AppVariables.getUserConfigBoolean("ImageCropPutClipboard", false)) { copy(true); } else { crop(); } } public void crop() { if (!editable.get() || scope() == null || scope().getScopeType() == ImageScope.ScopeType.All) { return; } synchronized (this) { if (task != null) { return; } task = new SingletonTask<Void>() { private Image newImage; @Override protected boolean handle() { String cString = AppVariables.getUserConfigValue("CropBackgroundColor", Color.TRANSPARENT.toString()); Color bgColor; try { bgColor = Color.web(cString); } catch (Exception e) { bgColor = Color.TRANSPARENT; } newImage = FxmlImageManufacture.crop(imageView.getImage(), scope(), bgColor, scope().isAreaExcluded()); if (task == null || isCancelled()) { return false; } return newImage != null; } @Override protected void whenSucceeded() { updateImage(ImageOperation.Crop, newImage, cost); } }; openHandlingStage(task, Modality.WINDOW_MODAL); Thread thread = new Thread(task); thread.setDaemon(true); thread.start(); } } // This can be implemented in right pane since clipboard always opened when this happens @FXML @Override public void pasteAction() { if (!editable.get()) { return; } // Ctrl-c/v are necessary for text editing if (operationController.myPane == operationController.textPane || operationController.myPane == operationController.richTextPane) { return; } if (operationController.myPane != operationController.clipboardPane) { operationController = operationController.expandPane(operationController.clipboardPane); timer = new Timer(); // Waiting for thumbs list loaded timer.schedule(new TimerTask() { @Override public void run() { Platform.runLater(new Runnable() { @Override public void run() { if (operationController.task == null) { operationController.pasteAction(); timer.cancel(); timer = null; } } }); } }, 0, 300); } else { operationController.accordionPane.setExpandedPane(operationController.clipboardPane); } } /* Browse */ @FXML @Override public void nextAction() { if (!checkBeforeNextAction()) { return; } if (nextFile != null) { loadImage(nextFile.getAbsoluteFile()); } } @FXML @Override public void previousAction() { if (!checkBeforeNextAction()) { return; } if (previousFile != null) { loadImage(previousFile.getAbsoluteFile()); } } @Override public boolean checkBeforeNextAction() { if (!imageLoaded.get() || !imageUpdated.get()) { return true; } Alert alert = new Alert(Alert.AlertType.CONFIRMATION); alert.setTitle(getMyStage().getTitle()); alert.setContentText(AppVariables.message("ImageChanged")); alert.getDialogPane().setMinHeight(Region.USE_PREF_SIZE); ButtonType buttonSave = new ButtonType(AppVariables.message("Save")); ButtonType buttonSaveAs = new ButtonType(AppVariables.message("SaveAs")); ButtonType buttonNotSave = new ButtonType(AppVariables.message("NotSave")); ButtonType buttonCancel = new ButtonType(AppVariables.message("Cancel")); alert.getButtonTypes().setAll(buttonSave, buttonSaveAs, buttonNotSave, buttonCancel); Stage stage = (Stage) alert.getDialogPane().getScene().getWindow(); stage.setAlwaysOnTop(true); stage.toFront(); Optional<ButtonType> result = alert.showAndWait(); if (result.get() == buttonSave) { saveAction(); return true; } else if (result.get() == buttonNotSave) { return true; } else if (result.get() == buttonSaveAs) { saveAsAction(); return true; } else { return false; } } @FXML @Override public void statisticAction() { ImageView view = null; File file = null; if (currentImageTab.isSelected()) { view = currentImageController.imageView; file = sourceFile; } else if (refImageTab.isSelected()) { view = refImageController.imageView; sourceFile = refFile; } else if (hisImageTab.isSelected()) { view = hisImageController.imageView; } if (view == null || view.getImage() == null) { return; } ImageAnalyseController controller = (ImageAnalyseController) FxmlStage.openStage(CommonValues.ImageAnalyseFxml); controller.init(file, view.getImage()); controller.setParentView(view); controller.loadData(); } }