From 2b1befbd2e6e43350160024ef0df77ad20703459 Mon Sep 17 00:00:00 2001 From: Mark Raynsford Date: Wed, 24 Jan 2024 20:01:54 +0000 Subject: [PATCH] Add an image viewer. --- .../laurel/gui/internal/LCaptionsView.java | 23 ++- .../io7m/laurel/gui/internal/LController.java | 6 + .../laurel/gui/internal/LControllerType.java | 6 + .../io7m/laurel/gui/internal/LImageView.java | 193 ++++++++++++++++++ .../laurel/gui/internal/Messages.properties | 7 +- .../io7m/laurel/gui/internal/captions.fxml | 2 +- .../com/io7m/laurel/gui/internal/image.fxml | 28 +++ 7 files changed, 259 insertions(+), 6 deletions(-) create mode 100644 com.io7m.laurel.gui/src/main/java/com/io7m/laurel/gui/internal/LImageView.java create mode 100644 com.io7m.laurel.gui/src/main/resources/com/io7m/laurel/gui/internal/image.fxml diff --git a/com.io7m.laurel.gui/src/main/java/com/io7m/laurel/gui/internal/LCaptionsView.java b/com.io7m.laurel.gui/src/main/java/com/io7m/laurel/gui/internal/LCaptionsView.java index caff399..5ac97b9 100644 --- a/com.io7m.laurel.gui/src/main/java/com/io7m/laurel/gui/internal/LCaptionsView.java +++ b/com.io7m.laurel.gui/src/main/java/com/io7m/laurel/gui/internal/LCaptionsView.java @@ -88,6 +88,9 @@ public final class LCaptionsView implements LScreenViewType @FXML private TextField captionAvailableSearch; @FXML private TextField imageSearch; + private Stage imageDisplayWindow; + private LImageView imageDisplay; + /** * The captions view. * @@ -116,6 +119,14 @@ public void initialize( final URL url, final ResourceBundle resourceBundle) { + this.imageDisplayWindow = new Stage(); + this.imageDisplay = + LImageView.create( + this.imageDisplayWindow, + this.services, + this.strings + ); + this.captions.setDisable(true); this.captions.setVisible(false); @@ -340,8 +351,8 @@ private void onImageSelected( imageFileOpt.get().toUri().toString(), 256.0, 256.0, - false, - false, + true, + true, true ); @@ -548,4 +559,12 @@ private void onCaptionGlobal() throw new UncheckedIOException(e); } } + + @FXML + private void onImageClicked() + { + if (!this.imageDisplayWindow.isShowing()) { + this.imageDisplayWindow.show(); + } + } } diff --git a/com.io7m.laurel.gui/src/main/java/com/io7m/laurel/gui/internal/LController.java b/com.io7m.laurel.gui/src/main/java/com/io7m/laurel/gui/internal/LController.java index ddef192..76e2097 100644 --- a/com.io7m.laurel.gui/src/main/java/com/io7m/laurel/gui/internal/LController.java +++ b/com.io7m.laurel.gui/src/main/java/com/io7m/laurel/gui/internal/LController.java @@ -717,6 +717,12 @@ public void imagesDelete( ); } + @Override + public ReadOnlyProperty imageSelected() + { + return this.model.imageSelected(); + } + @Override public ReadOnlyProperty fileStatus() { diff --git a/com.io7m.laurel.gui/src/main/java/com/io7m/laurel/gui/internal/LControllerType.java b/com.io7m.laurel.gui/src/main/java/com/io7m/laurel/gui/internal/LControllerType.java index c479a79..e5c9f87 100644 --- a/com.io7m.laurel.gui/src/main/java/com/io7m/laurel/gui/internal/LControllerType.java +++ b/com.io7m.laurel.gui/src/main/java/com/io7m/laurel/gui/internal/LControllerType.java @@ -311,4 +311,10 @@ void globalPrefixCaptionModify( void imagesDelete( List images); + + /** + * @return The selected image + */ + + ReadOnlyProperty imageSelected(); } diff --git a/com.io7m.laurel.gui/src/main/java/com/io7m/laurel/gui/internal/LImageView.java b/com.io7m.laurel.gui/src/main/java/com/io7m/laurel/gui/internal/LImageView.java new file mode 100644 index 0000000..ee6dd99 --- /dev/null +++ b/com.io7m.laurel.gui/src/main/java/com/io7m/laurel/gui/internal/LImageView.java @@ -0,0 +1,193 @@ +/* + * Copyright © 2024 Mark Raynsford https://www.io7m.com + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR + * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + + +package com.io7m.laurel.gui.internal; + +import com.io7m.laurel.gui.internal.model.LMImage; +import com.io7m.repetoir.core.RPServiceDirectoryType; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.ProgressBar; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.Pane; +import javafx.scene.shape.Rectangle; +import javafx.stage.Stage; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.URL; +import java.util.Objects; +import java.util.Optional; +import java.util.ResourceBundle; + +/** + * The About screen. + */ + +public final class LImageView implements LScreenViewType +{ + private final Stage stage; + private final LControllerType controller; + + private @FXML Rectangle border; + private @FXML ImageView imageView; + private @FXML Button dismiss; + private @FXML ProgressBar imageProgress; + + /** + * The image view screen. + * + * @param services The services + * @param inStage The stage + */ + + public LImageView( + final RPServiceDirectoryType services, + final Stage inStage) + { + this.controller = + services.requireService(LControllerType.class); + this.stage = + Objects.requireNonNull(inStage, "inStage"); + } + + @Override + public void initialize( + final URL url, + final ResourceBundle resourceBundle) + { + this.border.widthProperty() + .bind(this.stage.widthProperty().subtract(24.0)); + this.border.heightProperty() + .bind(this.stage.heightProperty().subtract(32 + 32 + 16 + 8)); + + this.border.widthProperty() + .addListener((observable, oldValue, newValue) -> { + this.imageView.setFitWidth(newValue.doubleValue() - 2.0); + }); + this.border.heightProperty() + .addListener((observable, oldValue, newValue) -> { + this.imageView.setFitHeight(newValue.doubleValue() - 2.0); + }); + + this.controller.imageSelected() + .subscribe((image1, image2) -> { + this.onImageSelected(image2); + }); + } + + private void onImageSelected( + final LMImage image) + { + if (image == null) { + this.controller.imageSelect(Optional.empty()); + this.imageView.setImage(null); + return; + } + + final var imageFileOpt = + Optional.ofNullable(image.fileName().get()); + + this.imageProgress.setVisible(true); + if (imageFileOpt.isPresent()) { + final var imageValue = + new Image( + imageFileOpt.get().toUri().toString(), + this.imageView.getFitWidth(), + this.imageView.getFitHeight(), + false, + true, + true + ); + + this.imageProgress.setVisible(true); + + imageValue.exceptionProperty() + .subscribe(e -> { + this.imageProgress.setVisible(true); + }); + + imageValue.progressProperty() + .addListener((observable, oldValue, newValue) -> { + if (newValue.doubleValue() >= 1.0) { + this.imageProgress.setVisible(false); + } + }); + + this.imageProgress.progressProperty() + .bind(imageValue.progressProperty()); + + this.imageView.setImage(imageValue); + } else { + this.imageView.setImage(null); + this.imageProgress.setVisible(false); + } + } + + @FXML + private void onDismiss() + { + this.stage.close(); + } + + /** + * The image view screen. + * + * @param services The services + * @param strings The strings + * + * @return The about screen + */ + + public static LImageView create( + final Stage stage, + final RPServiceDirectoryType services, + final LStrings strings) + { + try { + final var layout = + LExporterDialogs.class.getResource( + "/com/io7m/laurel/gui/internal/image.fxml"); + + Objects.requireNonNull(layout, "layout"); + + final var loader = + new FXMLLoader(layout, strings.resources()); + + final var image = new LImageView(services, stage); + loader.setControllerFactory(param -> { + return image; + }); + + final Pane pane = loader.load(); + LCSS.setCSS(pane); + + stage.setTitle(strings.format("image")); + stage.setWidth(512); + stage.setMinWidth(256); + stage.setMinHeight(256); + stage.setHeight(512); + stage.setScene(new Scene(pane)); + return image; + } catch (final IOException e) { + throw new UncheckedIOException(e); + } + } +} diff --git a/com.io7m.laurel.gui/src/main/resources/com/io7m/laurel/gui/internal/Messages.properties b/com.io7m.laurel.gui/src/main/resources/com/io7m/laurel/gui/internal/Messages.properties index 9784f54..2966c1a 100644 --- a/com.io7m.laurel.gui/src/main/resources/com/io7m/laurel/gui/internal/Messages.properties +++ b/com.io7m.laurel.gui/src/main/resources/com/io7m/laurel/gui/internal/Messages.properties @@ -23,10 +23,10 @@ caption.text=Caption captions.tooltip.add=Add new caption. captions.tooltip.add_to_image=Add the selected caption to the current image. captions.tooltip.delete=Delete the selected caption. +captions.tooltip.global=Configure global prefix captions. captions.tooltip.priority_down=Reduce caption priority. captions.tooltip.priority_up=Increase caption priority. captions.tooltip.remove_from_image=Remove the selected caption from the current image. -captions.tooltip.global=Configure global prefix captions. confirm_unsaved=There is unsaved data. Do you want to save before continuing? discard=Discard error.dismiss=Dismiss @@ -38,11 +38,13 @@ export.directory=Directory export.include_images=Export Images export.select=Select... export=Export +globals=Global prefix captions +image=Image images.filename=Filename images.search_by_caption=Search by caption... -images.tooltip.search=Search for images with the given comma-separated list of captions. images.tooltip.add=Add a new image. images.tooltip.delete=Delete the selected image. +images.tooltip.search=Search for images with the given comma-separated list of captions. import.select=Select a directory filled with captioned images... placeholder=Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Euismod nisi porta lorem mollis aliquam. In cursus turpis massa tincidunt. Felis eget velit aliquet sagittis id consectetur purus ut. Felis eget velit aliquet sagittis id consectetur purus. Lacus sed turpis tincidunt id aliquet risus feugiat in ante. Orci nulla pellentesque dignissim enim. Urna porttitor rhoncus dolor purus non enim praesent elementum facilisis. Tortor aliquam nulla facilisi cras. Feugiat pretium nibh ipsum consequat. Mattis molestie a iaculis at erat pellentesque adipiscing commodo. Sagittis vitae et leo duis. Vitae et leo duis ut diam quam nulla. Ut ornare lectus sit amet est placerat in. Nec sagittis aliquam malesuada bibendum arcu vitae elementum. Duis convallis convallis tellus id interdum velit laoreet id donec. Rhoncus est pellentesque elit ullamcorper dignissim cras tincidunt. redo=Redo @@ -55,4 +57,3 @@ title.unsaved=Laurel ({0}) * title=Laurel undo=Undo undo_specific=Undo ({0}) -globals=Global prefix captions diff --git a/com.io7m.laurel.gui/src/main/resources/com/io7m/laurel/gui/internal/captions.fxml b/com.io7m.laurel.gui/src/main/resources/com/io7m/laurel/gui/internal/captions.fxml index c98a95e..cb97e82 100644 --- a/com.io7m.laurel.gui/src/main/resources/com/io7m/laurel/gui/internal/captions.fxml +++ b/com.io7m.laurel.gui/src/main/resources/com/io7m/laurel/gui/internal/captions.fxml @@ -23,7 +23,7 @@ - + diff --git a/com.io7m.laurel.gui/src/main/resources/com/io7m/laurel/gui/internal/image.fxml b/com.io7m.laurel.gui/src/main/resources/com/io7m/laurel/gui/internal/image.fxml new file mode 100644 index 0000000..e431071 --- /dev/null +++ b/com.io7m.laurel.gui/src/main/resources/com/io7m/laurel/gui/internal/image.fxml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + +