diff --git a/checkstyle-filter.xml b/checkstyle-filter.xml index fd0a250..d4366c4 100644 --- a/checkstyle-filter.xml +++ b/checkstyle-filter.xml @@ -12,8 +12,9 @@ - + diff --git a/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/LFileModelType.java b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/LFileModelType.java index 991b5e4..a564544 100644 --- a/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/LFileModelType.java +++ b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/LFileModelType.java @@ -554,4 +554,18 @@ CompletableFuture captionsPaste( */ void exportClear(); + + /** + * Change the source of an image. + * + * @param image The image + * @param source The source + * + * @return The operation in progress + */ + + CompletableFuture imageSourceSet( + LImageID image, + URI source + ); } diff --git a/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandImageSourceSet.java b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandImageSourceSet.java new file mode 100644 index 0000000..fb3a996 --- /dev/null +++ b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LCommandImageSourceSet.java @@ -0,0 +1,164 @@ +/* + * 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.filemodel.internal; + +import com.io7m.laurel.model.LException; +import org.jooq.DSLContext; + +import java.util.Properties; + +import static com.io7m.laurel.filemodel.internal.Tables.IMAGES; + +/** + * Set an image source. + */ + +public final class LCommandImageSourceSet + extends LCommandAbstract +{ + private SavedData savedData; + + private record SavedData( + long savedImageId, + String savedOldURI, + String savedNewURI) + { + + } + + /** + * Set an image source. + */ + + public LCommandImageSourceSet() + { + + } + + /** + * @return A command factory + */ + + public static LCommandFactoryType provider() + { + return new LCommandFactory<>( + LCommandImageSourceSet.class.getCanonicalName(), + LCommandImageSourceSet::fromProperties + ); + } + + private static LCommandImageSourceSet fromProperties( + final Properties p) + { + final var c = new LCommandImageSourceSet(); + + c.savedData = new SavedData( + Long.parseUnsignedLong(p.getProperty("image")), + p.getProperty("oldURI"), + p.getProperty("newURI") + ); + + c.setExecuted(true); + return c; + } + + @Override + protected LCommandUndoable onExecute( + final LFileModel model, + final LDatabaseTransactionType transaction, + final LImageSourceSet request) + throws LException + { + final var context = + transaction.get(DSLContext.class); + + final var old = + context.select(IMAGES.IMAGE_SOURCE) + .from(IMAGES) + .where(IMAGES.IMAGE_ID.eq(request.image().value())) + .fetchOne(IMAGES.IMAGE_SOURCE); + + final var updated = + context.update(IMAGES) + .set(IMAGES.IMAGE_SOURCE, request.source().toString()) + .where(IMAGES.IMAGE_ID.eq(request.image().value())) + .execute(); + + if (updated == 0) { + return LCommandUndoable.COMMAND_NOT_UNDOABLE; + } + + this.savedData = new SavedData( + request.image().value(), + old, + request.source().toString() + ); + + model.setImagesAll(LCommandModelUpdates.listImages(context)); + return LCommandUndoable.COMMAND_UNDOABLE; + } + + + @Override + protected void onUndo( + final LFileModel model, + final LDatabaseTransactionType transaction) + { + final var context = + transaction.get(DSLContext.class); + + context.update(IMAGES) + .set(IMAGES.IMAGE_SOURCE, this.savedData.savedOldURI) + .where(IMAGES.IMAGE_ID.eq(this.savedData.savedImageId)) + .execute(); + + model.setImagesAll(LCommandModelUpdates.listImages(context)); + } + + @Override + protected void onRedo( + final LFileModel model, + final LDatabaseTransactionType transaction) + { + final var context = + transaction.get(DSLContext.class); + + context.update(IMAGES) + .set(IMAGES.IMAGE_SOURCE, this.savedData.savedNewURI) + .where(IMAGES.IMAGE_ID.eq(this.savedData.savedImageId)) + .execute(); + + model.setImagesAll(LCommandModelUpdates.listImages(context)); + } + + @Override + public Properties toProperties() + { + final var p = new Properties(); + p.setProperty("image", Long.toUnsignedString(this.savedData.savedImageId)); + p.setProperty("oldURI", this.savedData.savedOldURI); + p.setProperty("newURI", this.savedData.savedNewURI); + return p; + } + + @Override + public String describe() + { + return "Set image source"; + } +} diff --git a/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LFileModel.java b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LFileModel.java index 6e3d87b..5b5c7dd 100644 --- a/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LFileModel.java +++ b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LFileModel.java @@ -1225,6 +1225,20 @@ public void exportClear() this.exportEvents.set(List.of()); } + @Override + public CompletableFuture imageSourceSet( + final LImageID image, + final URI source) + { + Objects.requireNonNull(image, "image"); + Objects.requireNonNull(source, "source"); + + return this.runCommand( + new LCommandImageSourceSet(), + new LImageSourceSet(image, source) + ); + } + private Optional executeImageStream( final LImageID id) throws LException diff --git a/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LImageSourceSet.java b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LImageSourceSet.java new file mode 100644 index 0000000..c994dcf --- /dev/null +++ b/com.io7m.laurel.filemodel/src/main/java/com/io7m/laurel/filemodel/internal/LImageSourceSet.java @@ -0,0 +1,48 @@ +/* + * 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.filemodel.internal; + +import com.io7m.laurel.model.LImageID; + +import java.net.URI; +import java.util.Objects; + +/** + * A request to set a source for an image. + * + * @param image The image + * @param source The source + */ + +public record LImageSourceSet( + LImageID image, + URI source) +{ + /** + * A request to set a source for an image. + * + * @param image The image + * @param source The source + */ + + public LImageSourceSet + { + Objects.requireNonNull(image, "image"); + Objects.requireNonNull(source, "source"); + } +} diff --git a/com.io7m.laurel.filemodel/src/main/java/module-info.java b/com.io7m.laurel.filemodel/src/main/java/module-info.java index 82fe64d..aa17563 100644 --- a/com.io7m.laurel.filemodel/src/main/java/module-info.java +++ b/com.io7m.laurel.filemodel/src/main/java/module-info.java @@ -31,6 +31,7 @@ import com.io7m.laurel.filemodel.internal.LCommandImageCaptionsAssign; import com.io7m.laurel.filemodel.internal.LCommandImageCaptionsUnassign; import com.io7m.laurel.filemodel.internal.LCommandImageSelect; +import com.io7m.laurel.filemodel.internal.LCommandImageSourceSet; import com.io7m.laurel.filemodel.internal.LCommandImagesAdd; import com.io7m.laurel.filemodel.internal.LCommandImagesDelete; import com.io7m.laurel.filemodel.internal.LCommandMetadataPut; @@ -85,6 +86,7 @@ LCommandImageCaptionsAssign, LCommandImageCaptionsUnassign, LCommandImageSelect, + LCommandImageSourceSet, LCommandImagesAdd, LCommandImagesDelete, LCommandMetadataPut, 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 0ffb47e..3ce9c09 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 @@ -44,6 +44,7 @@ import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.control.TextField; +import javafx.scene.control.TextInputDialog; import javafx.scene.image.ImageView; import javafx.scene.layout.Pane; import javafx.stage.Stage; @@ -52,6 +53,7 @@ import java.io.IOException; import java.io.UncheckedIOException; +import java.net.URI; import java.util.List; import java.util.Map; import java.util.Objects; @@ -59,6 +61,7 @@ import java.util.Set; import java.util.stream.Collectors; +import static com.io7m.laurel.gui.internal.LStringConstants.IMAGES_SOURCE_SET; import static javafx.stage.Modality.APPLICATION_MODAL; /** @@ -96,6 +99,8 @@ public final class LCaptionsView extends LAbstractViewWithModel @FXML private MenuItem assignedCaptionsContextMenuCopy; @FXML private MenuItem assignedCaptionsContextMenuPaste; @FXML private MenuItem imagesCompareCaptions; + @FXML private TextField imageSource; + @FXML private Button imageSourceButton; private Stage imageDisplayWindow; private LImageView imageDisplay; @@ -145,6 +150,7 @@ protected void onInitialize() this.imageDelete.setDisable(true); this.imageCaptionAssign.setDisable(true); this.imageCaptionUnassign.setDisable(true); + this.imageSourceButton.setDisable(true); this.captionNew.setDisable(false); this.captionDelete.setDisable(true); this.captionModify.setDisable(true); @@ -413,10 +419,19 @@ private void onImageSelected( fileModel.imageSelect(Optional.empty()); this.imageView.setImage(null); this.imageDelete.setDisable(true); + this.imageSourceButton.setDisable(true); + this.imageSource.setText(""); return; } this.imageDelete.setDisable(false); + this.imageSourceButton.setDisable(false); + this.imageSource.setText( + image.image() + .source() + .map(URI::toString) + .orElse("") + ); fileModel.imageSelect(Optional.of(image.id())); fileModel.imageStream(image.id()) @@ -723,4 +738,24 @@ private void onCaptionsCompareSelected() this.comparisons.open(this.services, this.fileModelScope()); } + + @FXML + private void onImageSetSourceSelected() + { + final var dialog = new TextInputDialog(); + LCSS.setCSS(dialog.getDialogPane()); + + dialog.getEditor().setText(this.imageSource.getText()); + dialog.setHeaderText(this.strings.format(IMAGES_SOURCE_SET)); + + final var r = dialog.showAndWait(); + if (r.isPresent()) { + final var text = URI.create(r.get()); + final var fileModel = this.fileModelNow(); + fileModel.imageSourceSet( + fileModel.imageSelected().get().orElseThrow().id(), + text + ); + } + } } 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 ed34e41..5e2ab29 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 @@ -32,6 +32,7 @@ captions.compare.no_extra_captions_present=No captions are present in this\nimag captions.compare.presenceA=The captions that are on image A but not image B. captions.compare.presenceB=The captions that are on image B but not image A. captions.compare=Compare captions +captions.source=Image source URI 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. @@ -40,6 +41,7 @@ captions.tooltip.modify=Modify the selected caption. 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.source.set=Set image source URI... captions.unassigned=Unassigned categories.tooltip.add=Add a new category. categories.tooltip.assign=Assign captions to the category. @@ -65,6 +67,7 @@ globals=Global prefix captions history.compact_confirm=Are you sure you want to delete the undo/redo history and compact the file? This operation cannot be undone. history.tooltip.compact=Delete history and compact database... history=History +images.source.set=Set a new source URI. image=Image images.compare_captions=Compare captions... images.filename=Filename @@ -100,8 +103,8 @@ title.unsaved=Laurel ({0}) * title=Laurel undo=Undo undo_specific=Undo ({0}) -validation=Validation +validation.good=Dataset successfully validated without any issues. validation.tooltip.execute=Run validation now. validation.tooltip.goto=Go to the offending image. -validation.good=Dataset successfully validated without any issues. +validation=Validation 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 203c9d4..f1c507a 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 @@ -31,6 +31,7 @@ + @@ -113,6 +114,23 @@ + + + + + + diff --git a/com.io7m.laurel.gui/src/main/resources/com/io7m/laurel/gui/internal/image-source-set.png b/com.io7m.laurel.gui/src/main/resources/com/io7m/laurel/gui/internal/image-source-set.png new file mode 100644 index 0000000..1e401cf Binary files /dev/null and b/com.io7m.laurel.gui/src/main/resources/com/io7m/laurel/gui/internal/image-source-set.png differ diff --git a/com.io7m.laurel.tests/src/main/java/com/io7m/laurel/tests/LFileModelTest.java b/com.io7m.laurel.tests/src/main/java/com/io7m/laurel/tests/LFileModelTest.java index 35db436..2e7b7ad 100644 --- a/com.io7m.laurel.tests/src/main/java/com/io7m/laurel/tests/LFileModelTest.java +++ b/com.io7m.laurel.tests/src/main/java/com/io7m/laurel/tests/LFileModelTest.java @@ -44,6 +44,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.net.URI; import java.nio.file.Files; import java.nio.file.OpenOption; import java.nio.file.Path; @@ -1549,6 +1550,80 @@ public void testGlobalCaptionsModifyNonexistent() assertEquals(tx0, tx1); } + @Test + public void testImageSourceSet() + throws Exception + { + this.model.imageAdd( + "image-a", + this.imageFile, + Optional.of(this.imageFile.toUri()) + ).get(TIMEOUT, SECONDS); + + this.model.imageAdd( + "image-b", + this.imageFile, + Optional.of(this.imageFile.toUri()) + ).get(TIMEOUT, SECONDS); + + this.model.imageAdd( + "image-c", + this.imageFile, + Optional.of(this.imageFile.toUri()) + ).get(TIMEOUT, SECONDS); + + final var imagesThen = this.model.imageList().get(); + final var i0 = imagesThen.get(0).id(); + final var i1 = imagesThen.get(1).id(); + final var i2 = imagesThen.get(2).id(); + + this.model.imageSourceSet(i1, URI.create("urn:example")) + .get(TIMEOUT, SECONDS); + + assertEquals( + imagesThen.get(0), + this.model.imageList().get().get(0) + ); + assertEquals( + URI.create("urn:example"), + this.model.imageList().get().get(1).image().source().get() + ); + assertEquals( + imagesThen.get(2), + this.model.imageList().get().get(2) + ); + + this.model.undo().get(TIMEOUT, SECONDS); + + assertEquals( + imagesThen.get(0), + this.model.imageList().get().get(0) + ); + assertEquals( + imagesThen.get(1), + this.model.imageList().get().get(1) + ); + assertEquals( + imagesThen.get(2), + this.model.imageList().get().get(2) + ); + + this.model.redo().get(TIMEOUT, SECONDS); + + assertEquals( + imagesThen.get(0), + this.model.imageList().get().get(0) + ); + assertEquals( + URI.create("urn:example"), + this.model.imageList().get().get(1).image().source().get() + ); + assertEquals( + imagesThen.get(2), + this.model.imageList().get().get(2) + ); + } + private List globalCaptionsNow() { return this.model.globalCaptionList()