From fde86026a46eaf597a9902b477d33c6e07c703bd Mon Sep 17 00:00:00 2001 From: tpietzsch Date: Tue, 25 Mar 2025 12:56:03 +0100 Subject: [PATCH 1/8] POM: Add slf4j dependency --- pom.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pom.xml b/pom.xml index 6c40c4bb..a4658711 100644 --- a/pom.xml +++ b/pom.xml @@ -235,6 +235,14 @@ org.yaml snakeyaml + + org.slf4j + slf4j-api + + + org.slf4j + slf4j-simple + From f9d8712005409ecc7729746accadf0513fcf6b88 Mon Sep 17 00:00:00 2001 From: tpietzsch Date: Tue, 25 Mar 2025 13:01:00 +0100 Subject: [PATCH 2/8] Add JsonUtils in particular @JsonIo annotation Typed instances of any T with a @JsonIo-annotated adapter are serialized as json objects with a "type" attribute with the value of the JsonIo.type() annotation, and an "obj" attribute, which is serialized using the annotated adapter. For deserialization, the correct adapter is looked up via the "type" attribute. --- .../AffineTransform3DJsonSerializer.java | 2 + src/main/java/bdv/tools/JsonUtils.java | 282 ++++++++++++++++++ 2 files changed, 284 insertions(+) create mode 100644 src/main/java/bdv/tools/JsonUtils.java diff --git a/src/main/java/bdv/img/remote/AffineTransform3DJsonSerializer.java b/src/main/java/bdv/img/remote/AffineTransform3DJsonSerializer.java index 047dfd4f..7cd52682 100644 --- a/src/main/java/bdv/img/remote/AffineTransform3DJsonSerializer.java +++ b/src/main/java/bdv/img/remote/AffineTransform3DJsonSerializer.java @@ -30,6 +30,7 @@ import java.lang.reflect.Type; +import bdv.tools.JsonUtils; import net.imglib2.realtransform.AffineTransform3D; import com.google.gson.JsonDeserializationContext; @@ -39,6 +40,7 @@ import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializer; +@JsonUtils.JsonIo( jsonType = "AffineTransform3D.Anchor", type = AffineTransform3D.class ) public class AffineTransform3DJsonSerializer implements JsonDeserializer< AffineTransform3D >, JsonSerializer< AffineTransform3D > { @Override diff --git a/src/main/java/bdv/tools/JsonUtils.java b/src/main/java/bdv/tools/JsonUtils.java new file mode 100644 index 00000000..9241afe4 --- /dev/null +++ b/src/main/java/bdv/tools/JsonUtils.java @@ -0,0 +1,282 @@ +package bdv.tools; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +import org.scijava.Priority; +import org.scijava.annotations.Index; +import org.scijava.annotations.IndexItem; +import org.scijava.annotations.Indexable; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +public class JsonUtils +{ + public static Gson gson() + { + GsonBuilder gsonBuilder = new GsonBuilder(); + JsonIos.registerTypeAdapters( gsonBuilder ); + return gsonBuilder.create(); + } + + /** + * Annotation for classes that de/serialize instances of {@code T} from/to JSON. + */ + @Retention( RetentionPolicy.RUNTIME ) + @Target( ElementType.TYPE ) + @Indexable + public @interface JsonIo + { + /** + * The value of the "type" attribute in serialized instances. + *

+ * This should be unique, because it is used to pick the correct {@code + * JsonIo} for deserialization of polymorphic {@link Typed} attributes. + */ + String jsonType(); + + /** + * The class of un-serialized instances. + */ + Class< ? > type(); + + double priority() default Priority.NORMAL; + } + + /** + * {@code Typed} instances of any {@code T} with a {@code @JsonIo}- + * annotated adapter are serialized as json objects with a "type" attribute + * with the value of the {@link JsonIo#type()} annotation, and an "obj" + * attribute, which is serialized using the annotated adapter. For + * deserialization, the correct adapter is looked up via the "type" + * attribute. + */ + public static class Typed< T > + { + private final T obj; + + Typed( T obj ) + { + Objects.requireNonNull( obj ); + this.obj = obj; + } + + public T get() + { + return obj; + } + + @Override + public String toString() + { + return "Typed{" + + "obj=" + obj + + '}'; + } + + @JsonIo( jsonType = "JsonUtils.Typed", type = Typed.class ) + static class JsonAdapter implements JsonSerializer< Typed< ? > >, JsonDeserializer< Typed< ? > > + { + @Override + public Typed< ? > deserialize( + final JsonElement json, + final Type typeOfT, + final JsonDeserializationContext context ) + { + final JsonObject obj = json.getAsJsonObject(); + final String jsonType = obj.get( "type" ).getAsString(); + final Type type = JsonIos.typeFor( jsonType ); + return typed( context.deserialize( obj.get( "data" ), type ) ); + } + + @Override + public JsonElement serialize( + final Typed< ? > src, + final Type typeOfSrc, + final JsonSerializationContext context ) + { + final JsonObject obj = new JsonObject(); + final String jsonType = JsonIos.jsonTypeFor( src.get().getClass() ); + obj.addProperty( "type", jsonType ); + obj.add( "data", context.serialize( src.get() ) ); + return obj; + } + } + } + + public static class TypedList< T > + { + private final List< T > list; + + public TypedList() + { + list = new ArrayList<>(); + } + + public TypedList( final List< T > list ) + { + this.list = list; + } + + public List< T > list() + { + return list; + } + + @Override + public String toString() + { + return "TypedList{" + list + '}'; + } + + + @JsonUtils.JsonIo( jsonType = "TypedList", type = TypedList.class ) + static class JsonAdapter implements JsonDeserializer< TypedList< ? > >, JsonSerializer< TypedList< ? > > + { + @Override + public TypedList< ? > deserialize( + final JsonElement json, + final Type typeOfT, + final JsonDeserializationContext context ) + { + return deserializeT( json, typeOfT, context ); + } + + private < T > TypedList< T > deserializeT( + final JsonElement json, + final Type typeOfT, + final JsonDeserializationContext context ) + { + final List< T > list = new ArrayList<>(); + for ( JsonElement element : json.getAsJsonArray() ) + { + final Typed< T > typed = context.deserialize( element, Typed.class ); + list.add( typed.get() ); + } + return new TypedList<>( list ); + } + + @Override + public JsonElement serialize( + final TypedList< ? > src, + final Type typeOfSrc, + final JsonSerializationContext context ) + { + final JsonArray array = new JsonArray(); + for ( final Object spec : src.list() ) { + array.add( context.serialize( typed( spec ) ) ); + } + return array; + } + } + } + + /** + * Wrap {@code obj} into a {@code Typed} for serialization of polymorphic objects. + */ + public static < T > Typed< T > typed( T obj ) + { + return new Typed<>( obj ); + } + + static class JsonIos + { + static void registerTypeAdapters( final GsonBuilder gsonBuilder ) + { + build(); + type_to_JsonAdapterClassName.forEach( ( type, adapterClassName ) -> { + if ( adapterClassName == null ) + { + throw new RuntimeException( "could not find JsonAdapter for " + type ); + } + + final Object adapter; + try + { + adapter = Class.forName( adapterClassName ).newInstance(); + } + catch ( final Exception e ) + { + throw new RuntimeException( "could not create \"" + adapterClassName + "\" instance", e ); + } + gsonBuilder.registerTypeAdapter( type, adapter ); + } ); + } + + private static String jsonTypeFor( final Type type ) + { + build(); + final String jsonType = type_to_JsonType.get( type ); + if ( jsonType == null ) + throw new RuntimeException( "could not find JsonIo implementation for " + type ); + return jsonType; + } + + private static Type typeFor( final String jsonType ) + { + build(); + final Type type = jsonType_to_Type.get( jsonType ); + if ( type == null ) + throw new RuntimeException( "could not find JsonIo implementation for " + jsonType ); + return type; + } + + private static final Map< Type, String > type_to_JsonType = new ConcurrentHashMap<>(); + private static final Map< String, Type > jsonType_to_Type = new ConcurrentHashMap<>(); + private static final Map< Type, String > type_to_JsonAdapterClassName = new ConcurrentHashMap<>(); + + private static volatile boolean buildWasCalled = false; + + private static void build() + { + if ( !buildWasCalled ) + { + synchronized ( JsonUtils.class ) + { + if ( !buildWasCalled ) + { + try + { + final ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + final Index< JsonIo > annotationIndex = Index.load( JsonUtils.JsonIo.class, classLoader ); + for ( final IndexItem< JsonIo > item : annotationIndex ) + { + final JsonUtils.JsonIo io = item.annotation(); + type_to_JsonType.put( io.type(), io.jsonType() ); + jsonType_to_Type.put( io.jsonType(), io.type() ); + type_to_JsonAdapterClassName.put( io.type(), item.className() ); + } + } + catch ( final Exception e ) + { + throw new RuntimeException( "problem accessing annotation index", e ); + } + buildWasCalled = true; + } + } + } + } + } + + public static String prettyPrint( final JsonElement jsonElement ) + { + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + return gson.toJson( jsonElement ); + } +} From ae1c027ab09bbb8484df097948165064505f62ff Mon Sep 17 00:00:00 2001 From: tpietzsch Date: Thu, 27 Feb 2025 17:53:00 +0100 Subject: [PATCH 3/8] ResourceManager will be used to match ResourceSpecs of pasted links --- .../tools/links/DefaultResourceManager.java | 85 +++++++++++++++++ .../java/bdv/tools/links/ResourceConfig.java | 15 +++ .../links/ResourceCreationException.java | 24 +++++ .../java/bdv/tools/links/ResourceManager.java | 33 +++++++ .../java/bdv/tools/links/ResourceSpec.java | 28 ++++++ .../tools/links/resource/UnknownResource.java | 95 +++++++++++++++++++ 6 files changed, 280 insertions(+) create mode 100644 src/main/java/bdv/tools/links/DefaultResourceManager.java create mode 100644 src/main/java/bdv/tools/links/ResourceConfig.java create mode 100644 src/main/java/bdv/tools/links/ResourceCreationException.java create mode 100644 src/main/java/bdv/tools/links/ResourceManager.java create mode 100644 src/main/java/bdv/tools/links/ResourceSpec.java create mode 100644 src/main/java/bdv/tools/links/resource/UnknownResource.java diff --git a/src/main/java/bdv/tools/links/DefaultResourceManager.java b/src/main/java/bdv/tools/links/DefaultResourceManager.java new file mode 100644 index 00000000..1b7b1b58 --- /dev/null +++ b/src/main/java/bdv/tools/links/DefaultResourceManager.java @@ -0,0 +1,85 @@ +package bdv.tools.links; + +import java.lang.ref.WeakReference; +import java.util.Map; +import java.util.WeakHashMap; + +import bdv.tools.links.resource.UnknownResource; +import net.imglib2.util.Cast; + +public class DefaultResourceManager implements ResourceManager +{ + private final Map< Object, ResourceSpec< ? > > resourceToSpec = new WeakHashMap<>(); + + private final Map< ResourceSpec< ? >, WeakReference< ? > > specToResource = new WeakHashMap<>(); + + private final Map< Object, Object > keepAlive = new WeakHashMap<>(); + + @Override + public synchronized < T > void put( final T resource, final ResourceSpec< T > spec ) + { + resourceToSpec.put( resource, spec ); + specToResource.put( spec, new WeakReference<>( resource ) ); + } + + @Override + public synchronized < T > ResourceSpec< T > getResourceSpec( final T resource ) + { + final ResourceSpec< ? > spec = resourceToSpec.get( resource ); + if ( spec == null ) + return new UnknownResource.Spec<>(); + else + return Cast.unchecked( spec ); + } + + @Override + public synchronized < T > T getResource( final ResourceSpec< T > spec ) + { + final WeakReference< ? > ref = specToResource.get( spec ); + return ref == null ? null : Cast.unchecked( ref.get() ); + } + + @Override + public synchronized < T > T getOrCreateResource( final ResourceSpec< T > spec ) throws ResourceCreationException + { + T resource = getResource( spec ); + if ( resource == null ) + { + resource = spec.create( this ); + } + return resource; + } + + @Override + public synchronized void keepAlive( final Object anchor, final Object object ) + { + keepAlive.put( anchor, object ); + } + + @Override + public String toString() + { + String result = "DefaultResources{\n"; + result += " resourceToSpec{\n"; + for ( Map.Entry< Object, ResourceSpec< ? > > entry : resourceToSpec.entrySet() ) + { + Object key = entry.getKey(); + ResourceSpec< ? > value = entry.getValue(); + result += " k = " + key + ", v = " + head( 30, value.toString() ) + "\n"; + } + result += " }, specToResource{\n"; + for ( Map.Entry< ResourceSpec< ? >, WeakReference< ? > > entry : specToResource.entrySet() ) + { + final ResourceSpec< ? > key = entry.getKey(); + final WeakReference< ? > value = entry.getValue(); + result += " k = " + head( 30, key.toString() ) + ", v = " + value.get() + "\n"; + } + result += " }\n"; + result += '}'; + return result; + } + + private static String head(int len, String s) { + return s.substring( 0, Math.min( len, s.length() ) ); + } +} diff --git a/src/main/java/bdv/tools/links/ResourceConfig.java b/src/main/java/bdv/tools/links/ResourceConfig.java new file mode 100644 index 00000000..11be318a --- /dev/null +++ b/src/main/java/bdv/tools/links/ResourceConfig.java @@ -0,0 +1,15 @@ +package bdv.tools.links; + +public interface ResourceConfig +{ + /** + * Apply this config to the resource corresponding to the given {@code + * spec}. + * + * @param spec + * spec of resource that this config should be applied to + * @param resources + * maps specs to resources + */ + void apply( ResourceSpec< ? > spec, ResourceManager resources ); +} diff --git a/src/main/java/bdv/tools/links/ResourceCreationException.java b/src/main/java/bdv/tools/links/ResourceCreationException.java new file mode 100644 index 00000000..458bccff --- /dev/null +++ b/src/main/java/bdv/tools/links/ResourceCreationException.java @@ -0,0 +1,24 @@ +package bdv.tools.links; + +public class ResourceCreationException extends Exception +{ + public ResourceCreationException() + { + super(); + } + + public ResourceCreationException( String message ) + { + super( message ); + } + + public ResourceCreationException( Throwable cause ) + { + super( cause ); + } + + public ResourceCreationException( String message, Throwable cause ) + { + super( message, cause ); + } +} diff --git a/src/main/java/bdv/tools/links/ResourceManager.java b/src/main/java/bdv/tools/links/ResourceManager.java new file mode 100644 index 00000000..14459988 --- /dev/null +++ b/src/main/java/bdv/tools/links/ResourceManager.java @@ -0,0 +1,33 @@ +package bdv.tools.links; + +/** + * Associates resources and {@code ResourceSpec}s for copy&paste between + * BigDataViewer instances. + *

+ * Resources are for example {@code SpimData} objects, {@code + * SourceAndConverter} for a particular setup in a {@code SpimData}, opened N5 + * datasets, etc. + */ +public interface ResourceManager +{ + < T > void put( final T resource, final ResourceSpec< T > spec ); + + /** + * Get ResourceSpec registered for resource. + * (Return null if no spec was registered.) + */ + < T > ResourceSpec< T > getResourceSpec( T resource ); + + /** + * If spec is registered, get the corresponding resource. + */ + < T > T getResource( ResourceSpec< T > spec ); + + < T > T getOrCreateResource( ResourceSpec< T > spec ) throws ResourceCreationException; + + /** + * Puts a mapping from {@code anchor} to {@code object} into a {@code WeakHashMap}. + * This will keep {@code object} alive while {@code anchor} is strongly referenced. + */ + void keepAlive( Object anchor, Object object ); +} diff --git a/src/main/java/bdv/tools/links/ResourceSpec.java b/src/main/java/bdv/tools/links/ResourceSpec.java new file mode 100644 index 00000000..cd691304 --- /dev/null +++ b/src/main/java/bdv/tools/links/ResourceSpec.java @@ -0,0 +1,28 @@ +package bdv.tools.links; + +public interface ResourceSpec< T > +{ + /** + * Creates the specified resource. Resources for nested specs are + * retrieved from a {@code Resources} map, or created and put into the map. + * + * @throws ResourceCreationException + * if the resource could not be created + */ + T create( ResourceManager resources ) throws ResourceCreationException; + + /** + * Create a {@code ResourceConfig} corresponding to this {@code ResourceSpec}. + *

+ * A typical implementation gets the resource corresponding to this {@code + * ResourceSpec} from {@code resources}, extracts dynamic properties (such + * as the current transform of a {@code TransformedSource}), and builds the + * config. + * + * @param resources + * maps specs to resources + * + * @return the current {@code ResourceConfig} of the resource corresponding to this spec + */ + ResourceConfig getConfig( ResourceManager resources ); +} diff --git a/src/main/java/bdv/tools/links/resource/UnknownResource.java b/src/main/java/bdv/tools/links/resource/UnknownResource.java new file mode 100644 index 00000000..4c7fe0af --- /dev/null +++ b/src/main/java/bdv/tools/links/resource/UnknownResource.java @@ -0,0 +1,95 @@ +package bdv.tools.links.resource; + +import java.lang.reflect.Type; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +import bdv.tools.JsonUtils; +import bdv.tools.links.ResourceConfig; +import bdv.tools.links.ResourceCreationException; +import bdv.tools.links.ResourceManager; +import bdv.tools.links.ResourceSpec; + +/** + * Used for resources that do not have associated specs. + *

+ * Equality is Object identity. This should make sure that ResourceSpecs + * wrapping {@link UnknownResource} are never equal unless they are the same + * instance. + */ +public interface UnknownResource +{ + class Spec< T > implements ResourceSpec< T > + { + @Override + public T create( final ResourceManager resources ) throws ResourceCreationException + { + throw new ResourceCreationException( "UnknownResource cannot be created" ); + } + + @Override + public ResourceConfig getConfig( final ResourceManager resources ) + { + return new Config(); + } + } + + class Config implements ResourceConfig + { + @Override + public void apply( final ResourceSpec< ? > spec, final ResourceManager resources ) + { + // nothing to configure + } + } + + @JsonUtils.JsonIo( jsonType = "UnknownResource.Spec", type = UnknownResource.Spec.class ) + class SpecAdapter implements JsonDeserializer< Spec >, JsonSerializer< Spec > + { + @Override + public Spec deserialize( + final JsonElement json, + final Type typeOfT, + final JsonDeserializationContext context ) throws JsonParseException + { + return new Spec<>(); + } + + @Override + public JsonElement serialize( + final Spec src, + final Type typeOfSrc, + final JsonSerializationContext context ) + { + return new JsonObject(); + } + } + + @JsonUtils.JsonIo( jsonType = "UnknownResource.Config", type = UnknownResource.Config.class ) + class ConfigAdapter implements JsonDeserializer< Config >, JsonSerializer< Config > + { + @Override + public Config deserialize( + final JsonElement json, + final Type typeOfT, + final JsonDeserializationContext context ) throws JsonParseException + { + return new Config(); + } + + @Override + public JsonElement serialize( + final Config src, + final Type typeOfSrc, + final JsonSerializationContext context ) + { + return new JsonObject(); + } + } +} From 9745a448f611e6eb77102e5eb14bbad38da65b87 Mon Sep 17 00:00:00 2001 From: tpietzsch Date: Tue, 25 Mar 2025 13:07:06 +0100 Subject: [PATCH 4/8] Move StyleElements to package bdv.ui.settings, make public --- .../java/bdv/ui/appearance/AppearanceIO.java | 6 ++- .../bdv/ui/appearance/AppearanceManager.java | 2 +- .../ui/appearance/AppearanceSettingsPage.java | 39 +++++++++------- .../StyleElements.java | 46 +++++++++++-------- 4 files changed, 55 insertions(+), 38 deletions(-) rename src/main/java/bdv/ui/{appearance => settings}/StyleElements.java (90%) diff --git a/src/main/java/bdv/ui/appearance/AppearanceIO.java b/src/main/java/bdv/ui/appearance/AppearanceIO.java index fe653c0d..dbc0e85c 100644 --- a/src/main/java/bdv/ui/appearance/AppearanceIO.java +++ b/src/main/java/bdv/ui/appearance/AppearanceIO.java @@ -39,6 +39,8 @@ import javax.swing.UIManager.LookAndFeelInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.Yaml; @@ -58,6 +60,8 @@ */ public class AppearanceIO { + private static final Logger LOG = LoggerFactory.getLogger( AppearanceIO.class ); + public static Appearance load( final String filename ) throws IOException { final FileReader input = new FileReader( filename ); @@ -182,7 +186,7 @@ public Object construct( final Node node ) } catch( final Exception e ) { - e.printStackTrace(); + LOG.info( "Error constructing Appearance", e ); } return null; } diff --git a/src/main/java/bdv/ui/appearance/AppearanceManager.java b/src/main/java/bdv/ui/appearance/AppearanceManager.java index 799c11df..99d7c183 100644 --- a/src/main/java/bdv/ui/appearance/AppearanceManager.java +++ b/src/main/java/bdv/ui/appearance/AppearanceManager.java @@ -162,7 +162,7 @@ void save( final String filename ) catch ( final Exception e ) { e.printStackTrace(); - System.out.println( "Error while reading appearance settings file " + filename + ". Using defaults." ); + System.out.println( "Error while writing appearance settings file " + filename + "." ); } } diff --git a/src/main/java/bdv/ui/appearance/AppearanceSettingsPage.java b/src/main/java/bdv/ui/appearance/AppearanceSettingsPage.java index bca60552..7fb133ba 100644 --- a/src/main/java/bdv/ui/appearance/AppearanceSettingsPage.java +++ b/src/main/java/bdv/ui/appearance/AppearanceSettingsPage.java @@ -30,8 +30,9 @@ import bdv.ui.settings.ModificationListener; import bdv.ui.settings.SettingsPage; +import bdv.ui.settings.StyleElements; import bdv.util.Prefs; -import bdv.ui.appearance.StyleElements.ComboBoxEntry; +import bdv.ui.settings.StyleElements.ComboBoxEntry; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -44,14 +45,14 @@ import net.miginfocom.swing.MigLayout; import org.scijava.listeners.Listeners; -import static bdv.ui.appearance.StyleElements.booleanElement; -import static bdv.ui.appearance.StyleElements.cbentry; -import static bdv.ui.appearance.StyleElements.colorElement; -import static bdv.ui.appearance.StyleElements.comboBoxElement; -import static bdv.ui.appearance.StyleElements.linkedCheckBox; -import static bdv.ui.appearance.StyleElements.linkedColorButton; -import static bdv.ui.appearance.StyleElements.linkedComboBox; -import static bdv.ui.appearance.StyleElements.separator; +import static bdv.ui.settings.StyleElements.booleanElement; +import static bdv.ui.settings.StyleElements.cbentry; +import static bdv.ui.settings.StyleElements.colorElement; +import static bdv.ui.settings.StyleElements.comboBoxElement; +import static bdv.ui.settings.StyleElements.linkedCheckBox; +import static bdv.ui.settings.StyleElements.linkedColorButton; +import static bdv.ui.settings.StyleElements.linkedComboBox; +import static bdv.ui.settings.StyleElements.separator; /** * Preferences page for changing {@link Appearance}. @@ -131,7 +132,7 @@ public AppearancePanel( final Appearance appearance ) lafs.add( cbentry( feel, feel.getName() ) ); final List< StyleElements.StyleElement > styleElements = Arrays.asList( - comboBoxElement( "look-and-feel", appearance::lookAndFeel, appearance::setLookAndFeel, lafs ), + comboBoxElement( "look-and-feel:", appearance::lookAndFeel, appearance::setLookAndFeel, lafs ), separator(), booleanElement( "show scalebar", appearance::showScaleBar, appearance::setShowScaleBar ), booleanElement( "show scalebar in movies", appearance::showScaleBarInMovie, appearance::setShowScaleBarInMovie ), @@ -140,7 +141,8 @@ public AppearancePanel( final Appearance appearance ) separator(), booleanElement( "show minimap", appearance::showMultibox, appearance::setShowMultibox ), booleanElement( "show source info", appearance::showTextOverlay, appearance::setShowTextOverlay ), - comboBoxElement( "source name position", appearance::sourceNameOverlayPosition, appearance::setSourceNameOverlayPosition, Prefs.OverlayPosition.values() ) + separator(), + comboBoxElement( "source name position:", appearance::sourceNameOverlayPosition, appearance::setSourceNameOverlayPosition, Prefs.OverlayPosition.values() ) ); final JColorChooser colorChooser = new JColorChooser(); @@ -156,22 +158,25 @@ public void visit( final StyleElements.Separator separator ) @Override public void visit( final StyleElements.ColorElement element ) { - add( new JLabel( element.getLabel() ), "r" ); - add( linkedColorButton( element, colorChooser ), "l, wrap" ); + JPanel row = new JPanel(new MigLayout( "insets 0, fillx", "[r][l]", "" )); + row.add( linkedColorButton( element, colorChooser ), "l" ); + row.add( new JLabel( element.getLabel() ), "l, growx" ); + add( row, "l, span 2, wrap" ); } @Override public void visit( final StyleElements.BooleanElement element ) { - add( new JLabel( element.getLabel() ), "r" ); - add( linkedCheckBox( element ), "l, wrap" ); + add( linkedCheckBox( element, element.getLabel() ), "l, span 2, wrap" ); } @Override public void visit( final StyleElements.ComboBoxElement< ? > element ) { - add( new JLabel( element.getLabel() ), "r" ); - add( linkedComboBox( element ), "l, wrap" ); + JPanel row = new JPanel(new MigLayout( "insets 0, fillx", "[r][l]", "" )); + row.add( new JLabel( element.getLabel() ), "l" ); + row.add( linkedComboBox( element ), "l, growx" ); + add( row, "l, span 2, wrap" ); } } ) ); diff --git a/src/main/java/bdv/ui/appearance/StyleElements.java b/src/main/java/bdv/ui/settings/StyleElements.java similarity index 90% rename from src/main/java/bdv/ui/appearance/StyleElements.java rename to src/main/java/bdv/ui/settings/StyleElements.java index f8493189..77f9867b 100644 --- a/src/main/java/bdv/ui/appearance/StyleElements.java +++ b/src/main/java/bdv/ui/settings/StyleElements.java @@ -6,13 +6,13 @@ * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE @@ -26,9 +26,10 @@ * POSSIBILITY OF SUCH DAMAGE. * #L% */ -package bdv.ui.appearance; +package bdv.ui.settings; import bdv.tools.brightness.ColorIcon; + import java.awt.Color; import java.awt.Insets; import java.awt.event.ActionEvent; @@ -38,13 +39,12 @@ import java.util.Arrays; import java.util.List; import java.util.Objects; -import java.util.Vector; import java.util.function.BooleanSupplier; import java.util.function.Consumer; import java.util.function.IntConsumer; import java.util.function.IntSupplier; import java.util.function.Supplier; -import java.util.stream.Collectors; + import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JColorChooser; @@ -57,11 +57,9 @@ * Helpers for building settings pages: * Checkboxes, color-selection icons, ... */ -// TODO: Polish a bit and make public. -// This is a modified version of the StyleElements class from Mastodon. -// Currently it's only used in AppearanceSettingsPage. +// TODO: This is a modified version of the StyleElements class from Mastodon. // Eventually this should be unified with the Mastodon one and reused. -class StyleElements +public class StyleElements { public static Separator separator() { @@ -115,10 +113,21 @@ public static < T extends Enum< T > > ComboBoxElement< T > comboBoxElement( fina final Supplier< T > get, final Consumer< T > set, final T[] entries ) { - final List< ComboBoxEntry< T > > list = Arrays.stream( entries ) - .map( v -> new ComboBoxEntry<>( v, v.toString() ) ) - .collect( Collectors.toList() ); - return comboBoxElement( label, get, set, list ); + final String[] entryLabels = new String[entries.length]; + Arrays.setAll( entryLabels, i -> entries[ i ].toString() ); + return comboBoxElement( label, get, set, entries, entryLabels ); + } + + public static < T extends Enum< T > > ComboBoxElement< T > comboBoxElement( final String label, + final Supplier< T > get, final Consumer< T > set, + final T[] entries, + final String[] entryLabels ) + { + if ( entries.length != entryLabels.length ) + throw new IllegalArgumentException( "lengths of entries and entryLabels arrays do not match" ); + final ComboBoxEntry< T >[] cbentries = new ComboBoxEntry[ entries.length ]; + Arrays.setAll( cbentries, i -> new ComboBoxEntry<>( entries[ i ], entryLabels[ i ] ) ); + return comboBoxElement( label, get, set, Arrays.asList( cbentries ) ); } public static < T > ComboBoxElement< T > comboBoxElement( final String label, @@ -269,7 +278,7 @@ public void update() public abstract void set( boolean b ); } - static class ComboBoxEntry< T > + public static class ComboBoxEntry< T > { private final T value; @@ -350,7 +359,6 @@ public List< ComboBoxEntry< T > > entries() public static JCheckBox linkedCheckBox( final BooleanElement element, final String label ) { final JCheckBox checkbox = new JCheckBox( label, element.get() ); - checkbox.setFocusable( false ); checkbox.addActionListener( ( e ) -> element.set( checkbox.isSelected() ) ); element.onSet( b -> { if ( b != checkbox.isSelected() ) @@ -416,11 +424,11 @@ public void actionPerformed( final ActionEvent arg0 ) return button; } + @SuppressWarnings( "unchecked" ) public static < T > JComboBox< ComboBoxEntry< T > > linkedComboBox( final ComboBoxElement< T > element ) { - Vector< ComboBoxEntry< T > > vector = new Vector<>(); - vector.addAll( element.entries() ); - final JComboBox< ComboBoxEntry< T > > comboBox = new JComboBox<>( vector ); + final ComboBoxEntry< T >[] cbentries = element.entries.toArray( new ComboBoxEntry[ 0 ] ); + final JComboBox< ComboBoxEntry< T > > comboBox = new JComboBox<>( cbentries ); comboBox.setEditable( false ); comboBox.addItemListener( e -> { if ( e.getStateChange() == ItemEvent.SELECTED ) @@ -430,7 +438,7 @@ public static < T > JComboBox< ComboBoxEntry< T > > linkedComboBox( final ComboB } } ); final Consumer< T > setEntryForValue = value -> { - for ( ComboBoxEntry< T > entry : vector ) + for ( ComboBoxEntry< T > entry : cbentries ) if ( Objects.equals( entry.value(), value ) ) { comboBox.setSelectedItem( entry ); From 533a6b15eb2907f8410bb62a4406661822e008ea Mon Sep 17 00:00:00 2001 From: tpietzsch Date: Tue, 25 Mar 2025 13:11:21 +0100 Subject: [PATCH 5/8] Add ResourceManager to BigDataViewer and track added Sources --- src/main/java/bdv/BigDataViewer.java | 158 +++++++++++----- .../resource/SpimDataMinimalFileResource.java | 149 +++++++++++++++ .../resource/SpimDataSetupSourceResource.java | 168 +++++++++++++++++ .../resource/TransformedSourceResource.java | 170 ++++++++++++++++++ src/main/java/bdv/util/BdvOptions.java | 24 ++- src/main/java/bdv/viewer/ViewerOptions.java | 28 ++- 6 files changed, 649 insertions(+), 48 deletions(-) create mode 100644 src/main/java/bdv/tools/links/resource/SpimDataMinimalFileResource.java create mode 100644 src/main/java/bdv/tools/links/resource/SpimDataSetupSourceResource.java create mode 100644 src/main/java/bdv/tools/links/resource/TransformedSourceResource.java diff --git a/src/main/java/bdv/BigDataViewer.java b/src/main/java/bdv/BigDataViewer.java index 84cebd57..77ff9656 100644 --- a/src/main/java/bdv/BigDataViewer.java +++ b/src/main/java/bdv/BigDataViewer.java @@ -28,16 +28,6 @@ */ package bdv; -import bdv.tools.PreferencesDialog; -import bdv.ui.UIUtils; -import bdv.ui.keymap.Keymap; -import bdv.ui.keymap.KeymapManager; -import bdv.ui.keymap.KeymapSettingsPage; -import bdv.viewer.ConverterSetups; -import bdv.viewer.ViewerState; -import bdv.ui.appearance.AppearanceManager; -import bdv.ui.appearance.AppearanceSettingsPage; -import dev.dirs.ProjectDirectories; import java.io.File; import java.io.FileNotFoundException; import java.io.FileWriter; @@ -53,16 +43,6 @@ import javax.swing.SwingUtilities; import javax.swing.filechooser.FileFilter; -import net.imglib2.Volatile; -import net.imglib2.converter.Converter; -import net.imglib2.display.ColorConverter; -import net.imglib2.display.RealARGBColorConverter; -import net.imglib2.display.ScaledARGBConverter; -import net.imglib2.type.numeric.ARGBType; -import net.imglib2.type.numeric.NumericType; -import net.imglib2.type.numeric.RealType; -import net.imglib2.type.volatiles.VolatileARGBType; - import org.jdom2.Document; import org.jdom2.Element; import org.jdom2.JDOMException; @@ -71,6 +51,7 @@ import org.jdom2.output.XMLOutputter; import org.scijava.ui.behaviour.io.InputTriggerConfig; import org.scijava.ui.behaviour.io.yaml.YamlConfigIO; +import org.scijava.ui.behaviour.util.Actions; import bdv.cache.CacheControl; import bdv.export.ProgressWriter; @@ -80,6 +61,7 @@ import bdv.spimdata.XmlIoSpimDataMinimal; import bdv.tools.HelpDialog; import bdv.tools.InitializeViewerState; +import bdv.tools.PreferencesDialog; import bdv.tools.RecordMaxProjectionDialog; import bdv.tools.RecordMovieDialog; import bdv.tools.VisibilityAndGroupingDialog; @@ -91,21 +73,42 @@ import bdv.tools.brightness.RealARGBColorConverterSetup; import bdv.tools.brightness.SetupAssignments; import bdv.tools.crop.CropDialog; +import bdv.tools.links.ResourceManager; +import bdv.tools.links.resource.SpimDataMinimalFileResource; +import bdv.tools.links.resource.SpimDataSetupSourceResource; +import bdv.tools.links.resource.TransformedSourceResource; import bdv.tools.transformation.ManualTransformation; import bdv.tools.transformation.ManualTransformationEditor; import bdv.tools.transformation.TransformedSource; +import bdv.ui.UIUtils; +import bdv.ui.appearance.AppearanceManager; +import bdv.ui.appearance.AppearanceSettingsPage; +import bdv.ui.keymap.Keymap; +import bdv.ui.keymap.KeymapManager; +import bdv.ui.keymap.KeymapSettingsPage; +import bdv.viewer.ConverterSetups; import bdv.viewer.NavigationActions; import bdv.viewer.SourceAndConverter; import bdv.viewer.ViewerFrame; import bdv.viewer.ViewerOptions; import bdv.viewer.ViewerPanel; +import bdv.viewer.ViewerState; +import dev.dirs.ProjectDirectories; import mpicbg.spim.data.SpimDataException; import mpicbg.spim.data.generic.AbstractSpimData; import mpicbg.spim.data.generic.sequence.AbstractSequenceDescription; import mpicbg.spim.data.generic.sequence.BasicViewSetup; import mpicbg.spim.data.sequence.Angle; import mpicbg.spim.data.sequence.Channel; -import org.scijava.ui.behaviour.util.Actions; +import net.imglib2.Volatile; +import net.imglib2.converter.Converter; +import net.imglib2.display.ColorConverter; +import net.imglib2.display.RealARGBColorConverter; +import net.imglib2.display.ScaledARGBConverter; +import net.imglib2.type.numeric.ARGBType; +import net.imglib2.type.numeric.NumericType; +import net.imglib2.type.numeric.RealType; +import net.imglib2.type.volatiles.VolatileARGBType; public class BigDataViewer { @@ -137,6 +140,8 @@ public class BigDataViewer private final AppearanceManager appearanceManager; + private final ResourceManager resourceManager; + protected final PreferencesDialog preferencesDialog; protected final ManualTransformationEditor manualTransformationEditor; @@ -257,49 +262,103 @@ public static ConverterSetup createConverterSetup( final SourceAndConverter< ? > */ public static < T, V extends Volatile< T > > SourceAndConverter< T > wrapWithTransformedSource( final SourceAndConverter< T > soc ) { - if ( soc.asVolatile() == null ) - return new SourceAndConverter<>( new TransformedSource<>( soc.getSpimSource() ), soc.getConverter() ); + return wrapWithTransformedSource( soc, null ); + } - @SuppressWarnings( "unchecked" ) - final SourceAndConverter< V > vsoc = ( SourceAndConverter< V > ) soc.asVolatile(); + /** + * Decorate source with an extra transformation, that can be edited manually + * in this viewer. {@link SourceAndConverter#asVolatile() Nested volatile} + * {@code SourceAndConverter} are wrapped as well, if present. + * + * @param soc + * source to decorate + * @param resources + * if non-null, a {@code ResourceSpec} for the created {@code TransformedSource} wrapper. + * + * @return {@code TransformedSource} wrapper around {@code soc} + */ + public static < T, V extends Volatile< T > > SourceAndConverter< T > wrapWithTransformedSource( final SourceAndConverter< T > soc, final ResourceManager resources ) + { final TransformedSource< T > ts = new TransformedSource<>( soc.getSpimSource() ); - final TransformedSource< V > vts = new TransformedSource<>( vsoc.getSpimSource(), ts ); - return new SourceAndConverter<>( ts, soc.getConverter(), new SourceAndConverter<>( vts, vsoc.getConverter() ) ); + + final SourceAndConverter< V > vtsoc; + if ( soc.asVolatile() == null ) + { + vtsoc = null; + } + else + { + @SuppressWarnings( "unchecked" ) + final SourceAndConverter< V > vsoc = ( SourceAndConverter< V > ) soc.asVolatile(); + final TransformedSource< V > vts = new TransformedSource<>( vsoc.getSpimSource(), ts ); + vtsoc = new SourceAndConverter<>( vts, vsoc.getConverter() ); + } + + final SourceAndConverter< T > tsoc = new SourceAndConverter<>( ts, soc.getConverter(), vtsoc ); + if (resources != null) + { + final TransformedSourceResource.Spec spec = new TransformedSourceResource.Spec( + resources.getResourceSpec( soc ) ); + resources.put( tsoc, spec ); + resources.keepAlive( tsoc, soc ); + } + return tsoc; } private static < T extends NumericType< T >, V extends Volatile< T > & NumericType< V > > void initSetupNumericType( final AbstractSpimData< ? > spimData, final BasicViewSetup setup, final List< ConverterSetup > converterSetups, - final List< SourceAndConverter< ? > > sources ) + final List< SourceAndConverter< ? > > sources, + final ResourceManager resources ) { final int setupId = setup.getId(); + final String setupName = createSetupName( setup ); + final SourceAndConverter< T > soc = createSetupSourceNumericType( spimData, setupId, setupName, resources ); + final SourceAndConverter< T > tsoc = wrapWithTransformedSource( soc, resources ); + sources.add( tsoc ); + + final ConverterSetup converterSetup = createConverterSetup( tsoc, setupId ); + if ( converterSetup != null ) + converterSetups.add( converterSetup ); + } + + public static < T extends NumericType< T >, V extends Volatile< T > & NumericType< V > > SourceAndConverter< T > createSetupSourceNumericType( + final AbstractSpimData< ? > spimData, + final int setupId, + final String name, + final ResourceManager resources ) + { final ViewerImgLoader imgLoader = ( ViewerImgLoader ) spimData.getSequenceDescription().getImgLoader(); @SuppressWarnings( "unchecked" ) final ViewerSetupImgLoader< T, V > setupImgLoader = ( ViewerSetupImgLoader< T, V > ) imgLoader.getSetupImgLoader( setupId ); + if ( setupImgLoader == null ) + throw new IllegalArgumentException( "No SetupImgLoader for setup ID " + setupId + " found." ); + final T type = setupImgLoader.getImageType(); final V volatileType = setupImgLoader.getVolatileImageType(); if ( ! ( type instanceof NumericType ) ) throw new IllegalArgumentException( "ImgLoader of type " + type.getClass() + " not supported." ); - final String setupName = createSetupName( setup ); - SourceAndConverter< V > vsoc = null; if ( volatileType != null ) { - final VolatileSpimSource< V > vs = new VolatileSpimSource<>( spimData, setupId, setupName ); + final VolatileSpimSource< V > vs = new VolatileSpimSource<>( spimData, setupId, name ); vsoc = new SourceAndConverter<>( vs, createConverterToARGB( volatileType ) ); } - final SpimSource< T > s = new SpimSource<>( spimData, setupId, setupName ); + final SpimSource< T > s = new SpimSource<>( spimData, setupId, name ); final SourceAndConverter< T > soc = new SourceAndConverter<>( s, createConverterToARGB( type ), vsoc ); - final SourceAndConverter< T > tsoc = wrapWithTransformedSource( soc ); - sources.add( tsoc ); - - final ConverterSetup converterSetup = createConverterSetup( tsoc, setupId ); - if ( converterSetup != null ) - converterSetups.add( converterSetup ); + if (resources != null) + { + final SpimDataSetupSourceResource.Spec spec = new SpimDataSetupSourceResource.Spec( + resources.getResourceSpec( spimData ), + setupId, name ); + resources.put( soc, spec ); + resources.keepAlive( soc, spimData ); + } + return soc; } public static void initSetups( @@ -308,7 +367,17 @@ public static void initSetups( final List< SourceAndConverter< ? > > sources ) { for ( final BasicViewSetup setup : spimData.getSequenceDescription().getViewSetupsOrdered() ) - initSetupNumericType( spimData, setup, converterSetups, sources ); + initSetupNumericType( spimData, setup, converterSetups, sources, null ); + } + + public static void initSetups( + final AbstractSpimData< ? > spimData, + final List< ConverterSetup > converterSetups, + final List< SourceAndConverter< ? > > sources, + final ResourceManager resources ) + { + for ( final BasicViewSetup setup : spimData.getSequenceDescription().getViewSetupsOrdered() ) + initSetupNumericType( spimData, setup, converterSetups, sources, resources ); } @Deprecated @@ -355,6 +424,7 @@ public BigDataViewer( final AppearanceManager optionsAppearanceManager = options.values.getAppearanceManager(); keymapManager = optionsKeymapManager != null ? optionsKeymapManager : new KeymapManager( configDir ); appearanceManager = optionsAppearanceManager != null ? optionsAppearanceManager : new AppearanceManager( configDir ); + resourceManager = options.values.getResourceManager(); InputTriggerConfig inputTriggerConfig = options.values.getInputTriggerConfig(); final Keymap keymap = this.keymapManager.getForwardSelectedKeymap(); @@ -531,7 +601,8 @@ public static BigDataViewer open( final AbstractSpimData< ? > spimData, final St final ArrayList< ConverterSetup > converterSetups = new ArrayList<>(); final ArrayList< SourceAndConverter< ? > > sources = new ArrayList<>(); - initSetups( spimData, converterSetups, sources ); + final ResourceManager resources = options.values.getResourceManager(); + initSetups( spimData, converterSetups, sources, resources ); final AbstractSequenceDescription< ?, ?, ? > seq = spimData.getSequenceDescription(); final int numTimepoints = seq.getTimePoints().size(); @@ -549,6 +620,8 @@ public static BigDataViewer open( final AbstractSpimData< ? > spimData, final St public static BigDataViewer open( final String xmlFilename, final String windowTitle, final ProgressWriter progressWriter, final ViewerOptions options ) throws SpimDataException { final SpimDataMinimal spimData = new XmlIoSpimDataMinimal().load( xmlFilename ); + final ResourceManager resources = options.values.getResourceManager(); + resources.put( spimData, new SpimDataMinimalFileResource.Spec( xmlFilename ) ); final BigDataViewer bdv = open( spimData, windowTitle, progressWriter, options ); if ( !bdv.tryLoadSettings( xmlFilename ) ) InitializeViewerState.initBrightness( 0.001, 0.999, bdv.viewerFrame ); @@ -609,6 +682,11 @@ public AppearanceManager getAppearanceManager() return appearanceManager; } + public ResourceManager getResourceManager() + { + return resourceManager; + } + public boolean tryLoadSettings( final String xmlFilename ) { proposedSettingsFile = null; diff --git a/src/main/java/bdv/tools/links/resource/SpimDataMinimalFileResource.java b/src/main/java/bdv/tools/links/resource/SpimDataMinimalFileResource.java new file mode 100644 index 00000000..149a1ae6 --- /dev/null +++ b/src/main/java/bdv/tools/links/resource/SpimDataMinimalFileResource.java @@ -0,0 +1,149 @@ +package bdv.tools.links.resource; + +import java.io.File; +import java.lang.reflect.Type; +import java.net.URI; +import java.util.Objects; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +import bdv.spimdata.SpimDataMinimal; +import bdv.spimdata.XmlIoSpimDataMinimal; +import bdv.tools.JsonUtils; +import bdv.tools.links.ResourceConfig; +import bdv.tools.links.ResourceCreationException; +import bdv.tools.links.ResourceSpec; +import bdv.tools.links.ResourceManager; +import mpicbg.spim.data.SpimDataException; + +public interface SpimDataMinimalFileResource +{ + class Spec implements ResourceSpec< SpimDataMinimal > + { + // NB: URI to a local file! + private final URI xmlURI; + + public Spec( final URI xmlURI ) + { + this.xmlURI = xmlURI; + } + + public Spec( final String xmlFilename ) + { + this( new File( xmlFilename ).toURI() ); + } + + @Override + public SpimDataMinimal create( ResourceManager resources ) throws ResourceCreationException + { + try + { + final SpimDataMinimal spimData = new XmlIoSpimDataMinimal().load( toFile( xmlURI ).toString() ); + resources.put( spimData, this ); + return spimData; + } + catch ( SpimDataException e ) + { + throw new ResourceCreationException( e ); + } + } + + @Override + public ResourceConfig getConfig( final ResourceManager resources ) + { + return new Config(); + } + + private static File toFile( final URI uri ) + { + if ( "file".equalsIgnoreCase( uri.getScheme() ) ) + return new File( uri ); + throw new IllegalArgumentException( uri + " is not a file" ); + } + + @Override + public String toString() + { + return "SpimDataMinimalFileResource.Spec{" + + "xmlURI=" + xmlURI + + '}'; + } + + @Override + public boolean equals( final Object o ) + { + if ( !( o instanceof Spec ) ) + return false; + final Spec that = ( Spec ) o; + return Objects.equals( xmlURI.normalize(), that.xmlURI.normalize() ); + } + + @Override + public int hashCode() + { + return Objects.hashCode( xmlURI.normalize() ); + } + } + + class Config implements ResourceConfig + { + @Override + public void apply( final ResourceSpec< ? > spec, final ResourceManager resources ) + { + // nothing to configure + } + } + + @JsonUtils.JsonIo( jsonType = "SpimDataMinimalFileResource.Spec", type = SpimDataMinimalFileResource.Spec.class ) + class SpecAdapter implements JsonDeserializer< Spec >, JsonSerializer< Spec > + { + @Override + public Spec deserialize( + final JsonElement json, + final Type typeOfT, + final JsonDeserializationContext context ) + { + final JsonObject obj = json.getAsJsonObject(); + final String uri = obj.get( "uri" ).getAsString(); + return new Spec( URI.create( uri ) ); + } + + @Override + public JsonElement serialize( + final Spec src, + final Type typeOfSrc, + final JsonSerializationContext context ) + { + final JsonObject obj = new JsonObject(); + obj.addProperty( "uri", src.xmlURI.toString() ); + return obj; + } + } + + @JsonUtils.JsonIo( jsonType = "SpimDataMinimalFileResource.Config", type = SpimDataMinimalFileResource.Config.class ) + class ConfigAdapter implements JsonDeserializer< Config >, JsonSerializer< Config > + { + @Override + public Config deserialize( + final JsonElement json, + final Type typeOfT, + final JsonDeserializationContext context ) + { + return new Config(); + } + + @Override + public JsonElement serialize( + final Config src, + final Type typeOfSrc, + final JsonSerializationContext context ) + { + return new JsonObject(); + } + } +} diff --git a/src/main/java/bdv/tools/links/resource/SpimDataSetupSourceResource.java b/src/main/java/bdv/tools/links/resource/SpimDataSetupSourceResource.java new file mode 100644 index 00000000..9d2c836c --- /dev/null +++ b/src/main/java/bdv/tools/links/resource/SpimDataSetupSourceResource.java @@ -0,0 +1,168 @@ +package bdv.tools.links.resource; + +import static bdv.tools.JsonUtils.typed; + +import java.lang.reflect.Type; +import java.util.Objects; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +import bdv.BigDataViewer; +import bdv.tools.JsonUtils.Typed; +import bdv.tools.JsonUtils; +import bdv.tools.links.ResourceConfig; +import bdv.tools.links.ResourceCreationException; +import bdv.tools.links.ResourceSpec; +import bdv.tools.links.ResourceManager; +import bdv.viewer.SourceAndConverter; +import mpicbg.spim.data.generic.AbstractSpimData; + +public interface SpimDataSetupSourceResource +{ + class Spec implements ResourceSpec< SourceAndConverter< ? > > + { + private final ResourceSpec< ? extends AbstractSpimData< ? > > spimDataSpec; + + private final int setupId; + + private final String name; + + public Spec( + final ResourceSpec< ? extends AbstractSpimData< ? > > spimDataSpec, + final int setupId, + final String name ) + { + this.spimDataSpec = spimDataSpec; + this.setupId = setupId; + this.name = name; + } + + @Override + public SourceAndConverter< ? > create( final ResourceManager resources ) throws ResourceCreationException + { + final AbstractSpimData< ? > spimData = resources.getOrCreateResource( spimDataSpec ); + try + { + return BigDataViewer.createSetupSourceNumericType( spimData, setupId, name, resources ); + } + catch ( final Exception e ) + { + throw new ResourceCreationException( e ); + } + } + + @Override + public ResourceConfig getConfig( final ResourceManager resources ) + { + final ResourceConfig config = spimDataSpec.getConfig( resources ); + return new Config( config ); + } + + @Override + public String toString() + { + return "SpimDataSetupSourceResource.Spec{" + + "spimDataSpec=" + spimDataSpec + + ", setupId=" + setupId + + ", name='" + name + '\'' + + '}'; + } + + @Override + public boolean equals( final Object o ) + { + if ( !( o instanceof Spec ) ) + return false; + final Spec that = ( Spec ) o; + return setupId == that.setupId && Objects.equals( spimDataSpec, that.spimDataSpec ); + } + + @Override + public int hashCode() + { + return Objects.hash( spimDataSpec, setupId ); + } + } + + class Config implements ResourceConfig + { + private final ResourceConfig spimDataConfig; + + private Config( + final ResourceConfig spimDataConfig ) + { + this.spimDataConfig = spimDataConfig; + } + + @Override + public void apply( final ResourceSpec< ? > spec, final ResourceManager resources ) + { + if ( spec instanceof UnknownResource.Spec ) + return; + + if ( spec instanceof Spec ) + spimDataConfig.apply( ( ( Spec ) spec ).spimDataSpec, resources ); + } + } + + @JsonUtils.JsonIo( jsonType = "SpimDataSetupSourceResource.Spec", type = SpimDataSetupSourceResource.Spec.class ) + class JsonAdapter implements JsonDeserializer< Spec >, JsonSerializer< Spec > + { + @Override + public Spec deserialize( + final JsonElement json, + final Type typeOfT, + final JsonDeserializationContext context ) + { + final JsonObject obj = json.getAsJsonObject(); + final Typed< ResourceSpec< ? extends AbstractSpimData< ? > > > spimDataSpec = context.deserialize( obj.get( "spimData" ), Typed.class ); + final int setupId = obj.get( "setupId" ).getAsInt(); + final String name = obj.get( "name" ).getAsString(); + return new Spec( spimDataSpec.get(), setupId, name ); + } + + @Override + public JsonElement serialize( + final Spec src, + final Type typeOfSrc, + final JsonSerializationContext context ) + { + final JsonObject obj = new JsonObject(); + obj.add( "spimData", context.serialize( typed( src.spimDataSpec ) ) ); + obj.addProperty( "setupId", src.setupId ); + obj.addProperty( "name", src.name ); + return obj; + } + } + + @JsonUtils.JsonIo( jsonType = "SpimDataSetupSourceResource.Config", type = SpimDataSetupSourceResource.Config.class ) + class ConfigAdapter implements JsonDeserializer< Config >, JsonSerializer< Config > + { + @Override + public Config deserialize( + final JsonElement json, + final Type typeOfT, + final JsonDeserializationContext context ) + { + final JsonObject obj = json.getAsJsonObject(); + final Typed< ResourceConfig > spimDataConfig = context.deserialize( obj.get( "spimData" ), Typed.class ); + return new Config( spimDataConfig.get() ); + } + + @Override + public JsonElement serialize( + final Config src, + final Type typeOfSrc, + final JsonSerializationContext context ) + { + final JsonObject obj = new JsonObject(); + obj.add( "spimData", context.serialize( typed( src.spimDataConfig ) ) ); + return obj; + } + } +} diff --git a/src/main/java/bdv/tools/links/resource/TransformedSourceResource.java b/src/main/java/bdv/tools/links/resource/TransformedSourceResource.java new file mode 100644 index 00000000..6feafc75 --- /dev/null +++ b/src/main/java/bdv/tools/links/resource/TransformedSourceResource.java @@ -0,0 +1,170 @@ +package bdv.tools.links.resource; + +import static bdv.tools.JsonUtils.typed; + +import java.lang.reflect.Type; +import java.util.Objects; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +import bdv.BigDataViewer; +import bdv.tools.JsonUtils.Typed; +import bdv.tools.JsonUtils; +import bdv.tools.links.ResourceConfig; +import bdv.tools.links.ResourceCreationException; +import bdv.tools.links.ResourceSpec; +import bdv.tools.links.ResourceManager; +import bdv.tools.transformation.TransformedSource; +import bdv.viewer.Source; +import bdv.viewer.SourceAndConverter; +import net.imglib2.realtransform.AffineTransform3D; + +public interface TransformedSourceResource +{ + class Spec implements ResourceSpec< SourceAndConverter< ? > > + { + private final ResourceSpec< SourceAndConverter< ? > > delegateSpec; + + public Spec( final ResourceSpec< SourceAndConverter< ? > > delegateSpec ) + { + this.delegateSpec = delegateSpec; + } + + @Override + public SourceAndConverter< ? > create( final ResourceManager resources ) throws ResourceCreationException + { + final SourceAndConverter< ? > delegate = resources.getOrCreateResource( delegateSpec ); + return BigDataViewer.wrapWithTransformedSource( delegate, resources ); + } + + @Override + public ResourceConfig getConfig( final ResourceManager resources ) + { + final ResourceConfig delegateConfig = delegateSpec.getConfig( resources ); + final SourceAndConverter< ? > soc = resources.getResource( this ); + final TransformedSource< ? > ts = ( TransformedSource< ? > ) soc.getSpimSource(); + final AffineTransform3D transform = new AffineTransform3D(); + ts.getFixedTransform( transform ); + return new Config( delegateConfig, transform ); + } + + @Override + public String toString() + { + return "TransformedSourceResource.Spec{" + + "delegateSpec=" + delegateSpec + + '}'; + } + + @Override + public boolean equals( final Object o ) + { + if ( !( o instanceof Spec ) ) + return false; + final Spec that = ( Spec ) o; + return Objects.equals( delegateSpec, that.delegateSpec ); + } + + @Override + public int hashCode() + { + return Objects.hashCode( delegateSpec ); + } + } + + class Config implements ResourceConfig + { + private final ResourceConfig delegateConfig; + + private final AffineTransform3D transform; + + private Config( + final ResourceConfig delegateConfig, + final AffineTransform3D transform ) + { + this.delegateConfig = delegateConfig; + this.transform = transform; + } + + @Override + public void apply( final ResourceSpec< ? > spec, final ResourceManager resources ) + { + if ( spec instanceof UnknownResource.Spec ) + return; + + if ( spec instanceof Spec ) + delegateConfig.apply( ( ( Spec ) spec ).delegateSpec, resources ); + + final Object resource = resources.getResource( spec ); + if ( resource instanceof SourceAndConverter ) + { + SourceAndConverter< ? > soc = ( SourceAndConverter< ? > ) resource; + final Source< ? > source = soc.getSpimSource(); + if ( source instanceof TransformedSource ) + { + final TransformedSource< ? > ts = ( TransformedSource< ? > ) source; + ts.setFixedTransform( transform ); + } + } + } + } + + @JsonUtils.JsonIo( jsonType = "TransformedSourceResource.Spec", type = TransformedSourceResource.Spec.class ) + class SpecAdapter implements JsonDeserializer< Spec >, JsonSerializer< Spec > + { + @Override + public Spec deserialize( + final JsonElement json, + final Type typeOfT, + final JsonDeserializationContext context ) + { + final JsonObject obj = json.getAsJsonObject(); + final Typed< ResourceSpec< SourceAndConverter< ? > > > delegateSpec = context.deserialize( obj.get( "delegate" ), Typed.class ); + return new Spec( delegateSpec.get() ); + } + + @Override + public JsonElement serialize( + final Spec src, + final Type typeOfSrc, + final JsonSerializationContext context ) + { + final JsonObject obj = new JsonObject(); + obj.add( "delegate", context.serialize( typed( src.delegateSpec ) ) ); + return obj; + } + } + + @JsonUtils.JsonIo( jsonType = "TransformedSourceResource.Config", type = TransformedSourceResource.Config.class ) + class ConfigAdapter implements JsonDeserializer< Config >, JsonSerializer< Config > + { + @Override + public Config deserialize( + final JsonElement json, + final Type typeOfT, + final JsonDeserializationContext context ) + { + final JsonObject obj = json.getAsJsonObject(); + final Typed< ResourceConfig > delegateConfig = context.deserialize( obj.get( "delegate" ), Typed.class ); + final AffineTransform3D transform = context.deserialize( obj.get( "transform" ), AffineTransform3D.class ); + return new Config( delegateConfig.get(), transform ); + } + + @Override + public JsonElement serialize( + final Config src, + final Type typeOfSrc, + final JsonSerializationContext context ) + { + final JsonObject obj = new JsonObject(); + obj.add( "delegate", context.serialize( typed( src.delegateConfig ) ) ); + obj.add( "transform", context.serialize( src.transform ) ); + return obj; + } + } +} diff --git a/src/main/java/bdv/util/BdvOptions.java b/src/main/java/bdv/util/BdvOptions.java index 16a9155a..aa27bfb3 100644 --- a/src/main/java/bdv/util/BdvOptions.java +++ b/src/main/java/bdv/util/BdvOptions.java @@ -31,6 +31,8 @@ import bdv.TransformEventHandler2D; import bdv.TransformEventHandler3D; import bdv.TransformEventHandlerFactory; +import bdv.tools.links.DefaultResourceManager; +import bdv.tools.links.ResourceManager; import bdv.ui.appearance.AppearanceManager; import bdv.ui.keymap.KeymapManager; import bdv.viewer.render.AccumulateProjectorARGB; @@ -192,6 +194,17 @@ public BdvOptions appearanceManager( final AppearanceManager appearanceManager ) return this; } + /** + * Set the {@link ResourceManager}. + */ + public BdvOptions resourceManager( final ResourceManager resourceManager ) + { + if ( resourceManager == null ) + throw new NullPointerException( "resourceManager cannot be null" ); + values.resourceManager = resourceManager; + return this; + } + /** * Set the transform of the {@link BdvSource} to be created. * @@ -304,6 +317,8 @@ public static class Values private AppearanceManager appearanceManager = null; + private ResourceManager resourceManager = new DefaultResourceManager(); + private final AffineTransform3D sourceTransform = new AffineTransform3D(); private String frameTitle = "BigDataViewer"; @@ -332,6 +347,7 @@ public BdvOptions optionsFromValues() .inputTriggerConfig( inputTriggerConfig ) .keymapManager( keymapManager ) .appearanceManager( appearanceManager ) + .resourceManager( resourceManager ) .sourceTransform( sourceTransform ) .frameTitle( frameTitle ) .axisOrder( axisOrder ) @@ -353,7 +369,8 @@ public ViewerOptions getViewerOptions() .accumulateProjectorFactory( accumulateProjectorFactory ) .inputTriggerConfig( inputTriggerConfig ) .keymapManager( keymapManager ) - .appearanceManager( appearanceManager ); + .appearanceManager( appearanceManager ) + .resourceManager( resourceManager ); if ( hasPreferredSize() ) o.width( width ).height( height ); return o; @@ -399,6 +416,11 @@ public AppearanceManager getAppearanceManager() return appearanceManager; } + public ResourceManager getResourceManager() + { + return resourceManager; + } + public Bdv addTo() { return addTo; diff --git a/src/main/java/bdv/viewer/ViewerOptions.java b/src/main/java/bdv/viewer/ViewerOptions.java index 7290daa3..33161caa 100644 --- a/src/main/java/bdv/viewer/ViewerOptions.java +++ b/src/main/java/bdv/viewer/ViewerOptions.java @@ -30,12 +30,9 @@ import java.awt.event.KeyListener; -import net.imglib2.RandomAccessible; -import net.imglib2.RandomAccessibleInterval; -import net.imglib2.converter.Converter; -import net.imglib2.converter.Converters; -import net.imglib2.type.numeric.integer.UnsignedByteType; -import net.imglib2.type.numeric.real.FloatType; +import bdv.tools.links.DefaultResourceManager; +import bdv.tools.links.ResourceManager; + import org.scijava.ui.behaviour.KeyPressedManager; import org.scijava.ui.behaviour.io.InputTriggerConfig; @@ -260,6 +257,15 @@ public ViewerOptions appearanceManager( final AppearanceManager appearanceManage return this; } + /** + * Set the {@link ResourceManager}. + */ + public ViewerOptions resourceManager( final ResourceManager resourceManager ) + { + values.resourceManager = resourceManager; + return this; + } + /** * Read-only {@link ViewerOptions} values. */ @@ -295,6 +301,8 @@ public static class Values private AppearanceManager appearanceManager = null; + private ResourceManager resourceManager = new DefaultResourceManager(); + public ViewerOptions optionsFromValues() { return new ViewerOptions(). @@ -312,7 +320,8 @@ public ViewerOptions optionsFromValues() inputTriggerConfig( inputTriggerConfig ). shareKeyPressedEvents( keyPressedManager ). keymapManager( keymapManager ). - appearanceManager( appearanceManager ); + appearanceManager( appearanceManager ). + resourceManager( resourceManager); } public int getWidth() @@ -389,5 +398,10 @@ public AppearanceManager getAppearanceManager() { return appearanceManager; } + + public ResourceManager getResourceManager() + { + return resourceManager; + } } } From 1ae96402eab0a0379be1a2e27c173f1a43bc4700 Mon Sep 17 00:00:00 2001 From: tpietzsch Date: Tue, 25 Mar 2025 13:15:09 +0100 Subject: [PATCH 6/8] Add copy&paste of BDV state as JSON --- src/main/java/bdv/BigDataViewer.java | 23 + .../java/bdv/tools/links/BdvPropertiesV0.java | 277 ++++++++++++ .../java/bdv/tools/links/ClipboardUtils.java | 40 ++ .../java/bdv/tools/links/JsonAdapters.java | 174 ++++++++ .../java/bdv/tools/links/LinkActions.java | 137 ++++++ src/main/java/bdv/tools/links/Links.java | 417 ++++++++++++++++++ .../java/bdv/tools/links/PasteSettings.java | 72 +++ src/main/java/bdv/ui/links/LinkCard.java | 25 ++ src/main/java/bdv/ui/links/LinkSettings.java | 315 +++++++++++++ .../java/bdv/ui/links/LinkSettingsIO.java | 157 +++++++ .../bdv/ui/links/LinkSettingsManager.java | 108 +++++ .../java/bdv/ui/links/LinkSettingsPage.java | 244 ++++++++++ src/main/java/bdv/util/BdvFunctions.java | 18 +- src/main/java/bdv/util/BdvHandle.java | 6 + src/main/java/bdv/util/BdvHandleFrame.java | 14 + src/main/java/bdv/util/BdvHandlePanel.java | 28 ++ src/main/java/bdv/util/BdvOptions.java | 19 + src/main/java/bdv/viewer/DisplayMode.java | 2 +- src/main/java/bdv/viewer/ViewerOptions.java | 18 + src/main/resources/bdv/ui/keymap/default.yaml | 8 + 20 files changed, 2094 insertions(+), 8 deletions(-) create mode 100644 src/main/java/bdv/tools/links/BdvPropertiesV0.java create mode 100644 src/main/java/bdv/tools/links/ClipboardUtils.java create mode 100644 src/main/java/bdv/tools/links/JsonAdapters.java create mode 100644 src/main/java/bdv/tools/links/LinkActions.java create mode 100644 src/main/java/bdv/tools/links/Links.java create mode 100644 src/main/java/bdv/tools/links/PasteSettings.java create mode 100644 src/main/java/bdv/ui/links/LinkCard.java create mode 100644 src/main/java/bdv/ui/links/LinkSettings.java create mode 100644 src/main/java/bdv/ui/links/LinkSettingsIO.java create mode 100644 src/main/java/bdv/ui/links/LinkSettingsManager.java create mode 100644 src/main/java/bdv/ui/links/LinkSettingsPage.java diff --git a/src/main/java/bdv/BigDataViewer.java b/src/main/java/bdv/BigDataViewer.java index 77ff9656..d1cfd763 100644 --- a/src/main/java/bdv/BigDataViewer.java +++ b/src/main/java/bdv/BigDataViewer.java @@ -73,6 +73,8 @@ import bdv.tools.brightness.RealARGBColorConverterSetup; import bdv.tools.brightness.SetupAssignments; import bdv.tools.crop.CropDialog; +import bdv.tools.links.LinkActions; +import bdv.tools.links.PasteSettings; import bdv.tools.links.ResourceManager; import bdv.tools.links.resource.SpimDataMinimalFileResource; import bdv.tools.links.resource.SpimDataSetupSourceResource; @@ -86,6 +88,9 @@ import bdv.ui.keymap.Keymap; import bdv.ui.keymap.KeymapManager; import bdv.ui.keymap.KeymapSettingsPage; +import bdv.ui.links.LinkCard; +import bdv.ui.links.LinkSettingsManager; +import bdv.ui.links.LinkSettingsPage; import bdv.viewer.ConverterSetups; import bdv.viewer.NavigationActions; import bdv.viewer.SourceAndConverter; @@ -140,6 +145,8 @@ public class BigDataViewer private final AppearanceManager appearanceManager; + private final LinkSettingsManager linkSettingsManager; + private final ResourceManager resourceManager; protected final PreferencesDialog preferencesDialog; @@ -422,8 +429,10 @@ public BigDataViewer( { final KeymapManager optionsKeymapManager = options.values.getKeymapManager(); final AppearanceManager optionsAppearanceManager = options.values.getAppearanceManager(); + final LinkSettingsManager optionsLinkSettingsManager = options.values.getLinkSettingsManager(); keymapManager = optionsKeymapManager != null ? optionsKeymapManager : new KeymapManager( configDir ); appearanceManager = optionsAppearanceManager != null ? optionsAppearanceManager : new AppearanceManager( configDir ); + linkSettingsManager = optionsLinkSettingsManager != null ? optionsLinkSettingsManager : new LinkSettingsManager( configDir ); resourceManager = options.values.getResourceManager(); InputTriggerConfig inputTriggerConfig = options.values.getInputTriggerConfig(); @@ -512,6 +521,7 @@ public boolean accept( final File f ) preferencesDialog = new PreferencesDialog( viewerFrame, keymap, new String[] { KeyConfigContexts.BIGDATAVIEWER } ); preferencesDialog.addPage( new AppearanceSettingsPage( "Appearance", appearanceManager ) ); preferencesDialog.addPage( new KeymapSettingsPage( "Keymap", this.keymapManager, this.keymapManager.getCommandDescriptions() ) ); + preferencesDialog.addPage( new LinkSettingsPage( "Links", linkSettingsManager ) ); appearanceManager.appearance().updateListeners().add( viewerFrame::repaint ); appearanceManager.addLafComponent( fileChooser ); SwingUtilities.invokeLater(() -> appearanceManager.updateLookAndFeel()); @@ -524,9 +534,17 @@ public boolean accept( final File f ) bdvActions.install( viewerFrame.getKeybindings(), "bdv" ); BigDataViewerActions.install( bdvActions, this ); + final Actions linkActions = new Actions( inputTriggerConfig, "bdv" ); + linkActions.install( viewerFrame.getKeybindings(), "links" ); + final PasteSettings pasteSettings = linkSettingsManager.linkSettings().pasteSettings(); + LinkActions.install( linkActions, viewerFrame.getViewerPanel(), viewerFrame.getConverterSetups(), pasteSettings, resourceManager ); + + LinkCard.install( linkSettingsManager.linkSettings(), viewerFrame.getCardPanel() ); + keymap.updateListeners().add( () -> { navigationActions.updateKeyConfig( keymap.getConfig() ); bdvActions.updateKeyConfig( keymap.getConfig() ); + linkActions.updateKeyConfig( keymap.getConfig() ); viewerFrame.getTransformBehaviours().updateKeyConfig( keymap.getConfig() ); } ); @@ -682,6 +700,11 @@ public AppearanceManager getAppearanceManager() return appearanceManager; } + public LinkSettingsManager getLinkSettingsManager() + { + return linkSettingsManager; + } + public ResourceManager getResourceManager() { return resourceManager; diff --git a/src/main/java/bdv/tools/links/BdvPropertiesV0.java b/src/main/java/bdv/tools/links/BdvPropertiesV0.java new file mode 100644 index 00000000..ea887d9c --- /dev/null +++ b/src/main/java/bdv/tools/links/BdvPropertiesV0.java @@ -0,0 +1,277 @@ +package bdv.tools.links; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.IntStream; + +import bdv.tools.brightness.ConverterSetup; +import bdv.tools.JsonUtils.TypedList; +import bdv.util.Bounds; +import bdv.viewer.ConverterSetups; +import bdv.viewer.DisplayMode; +import bdv.viewer.Interpolation; +import bdv.viewer.SourceAndConverter; +import bdv.viewer.ViewerState; +import net.imglib2.Dimensions; +import net.imglib2.FinalDimensions; +import net.imglib2.Point; +import net.imglib2.realtransform.AffineTransform3D; +import net.imglib2.type.numeric.ARGBType; + +class BdvPropertiesV0 +{ + private final AffineTransform3D transform; + private final int timepoint; + private final DisplayMode displaymode; + private final Interpolation interpolation; + private final TypedList< ResourceSpec< ? > > sourceSpecs; + private final TypedList< ResourceConfig > sourceConfigs; + private final List< SourceConverterConfig > converterConfigs; + private final int currentSourceIndex; + private final int[] activeSourceIndices; + private final long[] panelsize; + private final long[] mousepos; + + private final Anchor anchor; + + public enum Anchor + { + CENTER( "center" ), + MOUSE( "mouse" ); + + private final String label; + + Anchor( final String label ) + { + this.label = label; + } + + @Override + public String toString() + { + return label; + } + + public static Anchor fromString( final String string ) + { + for ( final Anchor value : values() ) + if ( value.toString().equals( string ) ) + return value; + return null; + } + } + + public BdvPropertiesV0() + { + transform = new AffineTransform3D(); + timepoint = 0; + displaymode = DisplayMode.SINGLE; + interpolation = Interpolation.NLINEAR; + sourceSpecs = new TypedList<>(); + sourceConfigs = new TypedList<>(); + converterConfigs = new ArrayList<>(); + currentSourceIndex = -1; + activeSourceIndices = new int[ 0 ]; + panelsize = new long[ 2 ]; + mousepos = new long[ 2 ]; + anchor = Anchor.CENTER; + } + + public BdvPropertiesV0( + final ViewerState state, + final ConverterSetups converterSetups, + final Dimensions panelsize, + final Point mousepos, + final ResourceManager resources ) + { + this.transform = state.getViewerTransform(); + this.timepoint = state.getCurrentTimepoint(); + this.displaymode = state.getDisplayMode(); + this.interpolation = state.getInterpolation(); + this.sourceSpecs = getSourceSpecs( state.getSources(), resources ); + this.sourceConfigs = getSourceConfigs( sourceSpecs.list(), resources ); + this.converterConfigs = getSourceConverterConfigs( state.getSources(), converterSetups ); + this.currentSourceIndex = getCurrentSourceIndex( state ); + this.activeSourceIndices = getActiveSourceIndices( state ); + this.panelsize = panelsize.dimensionsAsLongArray(); + this.mousepos = mousepos.positionAsLongArray(); + this.anchor = Anchor.CENTER; + } + + private static int getCurrentSourceIndex( final ViewerState state ) + { + return state.getSources().indexOf( state.getCurrentSource() ); + } + + private static int[] getActiveSourceIndices( final ViewerState state ) + { + final List< SourceAndConverter< ? > > sources = state.getSources(); + return IntStream.range( 0, sources.size() ).filter( i -> state.isSourceActive( sources.get( i ) ) ).toArray(); + } + + private static List< SourceConverterConfig > getSourceConverterConfigs( final List< SourceAndConverter< ? > > sources, final ConverterSetups converterSetups ) + { + List< SourceConverterConfig > configs = new ArrayList<>(); + for ( SourceAndConverter< ? > soc : sources ) + { + final ConverterSetup setup = converterSetups.getConverterSetup( soc ); + final boolean hasColor = setup.supportsColor(); + final int color = setup.getColor().get(); + final double min = setup.getDisplayRangeMin(); + final double max = setup.getDisplayRangeMax(); + final Bounds bounds = converterSetups.getBounds().getBounds( setup ); + final double minBound = bounds.getMinBound(); + final double maxBound = bounds.getMaxBound(); + configs.add( new SourceConverterConfig( hasColor, color, min, max, minBound, maxBound ) ); + } + return configs; + } + + private static TypedList< ResourceSpec< ? > > getSourceSpecs( final List< SourceAndConverter< ? > > sources, final ResourceManager resources ) + { + List< ResourceSpec< ? > > sourceSpecs = new ArrayList<>(); + for ( final SourceAndConverter< ? > soc : sources ) + { + sourceSpecs.add( resources.getResourceSpec( soc ) ); + } + return new TypedList<>( sourceSpecs ); + } + + private static TypedList< ResourceConfig > getSourceConfigs( final List< ResourceSpec< ? > > sourceSpecs, final ResourceManager resources ) + { + final List< ResourceConfig > sourceConfigs = new ArrayList<>(); + for ( ResourceSpec< ? > spec : sourceSpecs ) { + sourceConfigs.add( spec.getConfig( resources ) ); + } + return new TypedList<>( sourceConfigs ); + } + + public AffineTransform3D transform() + { + return transform; + } + + public int timepoint() + { + return timepoint; + } + + public Dimensions panelsize() + { + return FinalDimensions.wrap( panelsize ); + } + + public Point mousepos() + { + return Point.wrap( mousepos ); + } + + public Anchor getAnchor() + { + return anchor; + } + + public DisplayMode displaymode() + { + return displaymode; + } + + public Interpolation interpolation() + { + return interpolation; + } + + public List< ResourceSpec< ? > > sourceSpecs() + { + return sourceSpecs.list(); + } + + public List< ResourceConfig > sourceConfigs() + { + return sourceConfigs.list(); + } + + public List< SourceConverterConfig > converterConfigs() + { + return converterConfigs; + } + + public int currentSourceIndex() + { + return currentSourceIndex; + } + + public int[] activeSourceIndices() + { + return activeSourceIndices; + } + + @Override + public String toString() + { + return "BdvPropertiesV0{" + + "transform=" + transform + + ", timepoint=" + timepoint + + ", displaymode=" + displaymode + + ", interpolation=" + interpolation + + ", sourceSpecs =" + sourceSpecs + + ", sourceConfigs =" + sourceConfigs + + ", converterConfigs=" + converterConfigs + + ", currentSourceIndex=" + currentSourceIndex + + ", activeSourceIndices=" + Arrays.toString( activeSourceIndices ) + + ", panelsize=" + Arrays.toString( panelsize ) + + ", mousepos=" + Arrays.toString( mousepos ) + + ", anchor=" + anchor + + '}'; + } + + public static class SourceConverterConfig + { + final boolean hasColor; + final int color; + final double min; + final double max; + final double minBound; + final double maxBound; + + public SourceConverterConfig( final boolean hasColor, final int color, final double min, final double max, final double minBound, final double maxBound ) + { + this.hasColor = hasColor; + this.color = color; + this.min = min; + this.max = max; + this.minBound = minBound; + this.maxBound = maxBound; + } + + public double rangeMin() { + return min; + } + + public double rangeMax() { + return max; + } + + public ARGBType color() { + return new ARGBType( color ); + } + + public Bounds bounds() { + return new Bounds( minBound, maxBound ); + } + + @Override + public String toString() + { + return "SourceConverterConfig{" + + "hasColor=" + hasColor + + ", color=" + String.format("#%08x", color) + + ", min=" + min + + ", max=" + max + + ", minBound=" + minBound + + ", maxBound=" + maxBound + + '}'; + } + } +} diff --git a/src/main/java/bdv/tools/links/ClipboardUtils.java b/src/main/java/bdv/tools/links/ClipboardUtils.java new file mode 100644 index 00000000..e191c36e --- /dev/null +++ b/src/main/java/bdv/tools/links/ClipboardUtils.java @@ -0,0 +1,40 @@ +package bdv.tools.links; + +import java.awt.Toolkit; +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.StringSelection; +import java.awt.datatransfer.Transferable; +import java.awt.datatransfer.UnsupportedFlavorException; +import java.io.IOException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class ClipboardUtils +{ + private static final Logger LOG = LoggerFactory.getLogger( ClipboardUtils.class ); + + static void copyToClipboard( final String string ) + { + final Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + clipboard.setContents( new StringSelection( string ), null ); + } + + static String getFromClipboard() + { + final Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + try + { + Transferable transferable = clipboard.getContents(DataFlavor.stringFlavor); + if ( transferable != null ) + { + return ( String ) transferable.getTransferData( DataFlavor.stringFlavor ); + } + } + catch ( UnsupportedFlavorException | IOException e ) + { + LOG.debug( "Unable to retrieve string from system clipboard", e ); + } + return null; + } +} diff --git a/src/main/java/bdv/tools/links/JsonAdapters.java b/src/main/java/bdv/tools/links/JsonAdapters.java new file mode 100644 index 00000000..56de8ef3 --- /dev/null +++ b/src/main/java/bdv/tools/links/JsonAdapters.java @@ -0,0 +1,174 @@ +package bdv.tools.links; + +import java.lang.reflect.Type; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +import bdv.tools.JsonUtils; +import bdv.viewer.DisplayMode; +import bdv.viewer.Interpolation; + +class JsonAdapters +{ + @JsonUtils.JsonIo( jsonType = "BdvPropertiesV0.Anchor", type = BdvPropertiesV0.Anchor.class ) + static class AnchorAdapter implements JsonDeserializer< BdvPropertiesV0.Anchor >, JsonSerializer< BdvPropertiesV0.Anchor > + { + @Override + public BdvPropertiesV0.Anchor deserialize( + final JsonElement json, + final Type typeOfT, + final JsonDeserializationContext context ) + { + return BdvPropertiesV0.Anchor.fromString( json.getAsString() ); + } + + @Override + public JsonElement serialize( + final BdvPropertiesV0.Anchor src, + final Type typeOfSrc, + final JsonSerializationContext context ) + { + return new JsonPrimitive( src.toString() ); + } + } + + @JsonUtils.JsonIo( jsonType = "BdvProperiesV0.SourceConverterConfig", type = BdvPropertiesV0.SourceConverterConfig.class ) + static class SourceConverterConfigAdapter implements JsonDeserializer< BdvPropertiesV0.SourceConverterConfig >, JsonSerializer< BdvPropertiesV0.SourceConverterConfig > + { + @Override + public BdvPropertiesV0.SourceConverterConfig deserialize( + final JsonElement json, + final Type typeOfT, + final JsonDeserializationContext context ) + { + JsonObject obj = json.getAsJsonObject(); + final JsonElement colorElement = obj.get( "color" ); + final boolean hasColor = colorElement != null; + final int color = hasColor ? (int) Long.parseLong( colorElement.getAsString(), 16 ) : -1; + final double min = obj.get("min").getAsDouble(); + final double max = obj.get("max").getAsDouble(); + final double minBound = obj.get("minBound").getAsDouble(); + final double maxBound = obj.get("maxBound").getAsDouble(); + return new BdvPropertiesV0.SourceConverterConfig( hasColor, color, min, max, minBound, maxBound ); + } + + @Override + public JsonElement serialize( + final BdvPropertiesV0.SourceConverterConfig src, + final Type typeOfSrc, + final JsonSerializationContext context ) + { + final JsonObject obj = new JsonObject(); + if ( src.hasColor ) + obj.addProperty( "color", String.format( "%08x", src.color ) ); + obj.addProperty( "min", src.min ); + obj.addProperty( "max", src.max ); + obj.addProperty( "minBound", src.minBound ); + obj.addProperty( "maxBound", src.maxBound ); + return obj; + } + } + + @JsonUtils.JsonIo( jsonType = "DisplayMode", type = DisplayMode.class ) + static class DisplayModeAdapter implements JsonDeserializer< DisplayMode >, JsonSerializer< DisplayMode > + { + @Override + public DisplayMode deserialize( + final JsonElement json, + final Type typeOfT, + final JsonDeserializationContext context ) throws JsonParseException + { + final String mode = json.getAsString(); + switch ( mode ) + { + case "single-source": + return DisplayMode.SINGLE; + case "single-group": + return DisplayMode.GROUP; + case "fused-source": + return DisplayMode.FUSED; + case "fused-group": + return DisplayMode.FUSEDGROUP; + default: + throw new JsonParseException( "Unsupported display mode: " + mode ); + } + } + + @Override + public JsonElement serialize( + final DisplayMode src, + final Type typeOfSrc, + final JsonSerializationContext context ) + { + final String mode; + switch ( src ) + { + case SINGLE: + mode = "single-source"; + break; + case GROUP: + mode = "single-group"; + break; + case FUSED: + mode = "fused-source"; + break; + case FUSEDGROUP: + mode = "fused-group"; + break; + default: + throw new IllegalArgumentException("Unexpected value: " + src); + } + return new JsonPrimitive( mode ); + } + } + + @JsonUtils.JsonIo( jsonType = "Interpolation", type = Interpolation.class ) + static class InterpolationAdapter implements JsonDeserializer< Interpolation >, JsonSerializer< Interpolation > + { + @Override + public Interpolation deserialize( + final JsonElement json, + final Type typeOfT, + final JsonDeserializationContext context ) throws JsonParseException + { + final String mode = json.getAsString(); + switch ( mode ) + { + case "nearest": + return Interpolation.NEARESTNEIGHBOR; + case "linear": + return Interpolation.NLINEAR; + default: + throw new JsonParseException( "Unsupported interpolation mode: " + mode ); + } + } + + @Override + public JsonElement serialize( + final Interpolation src, + final Type typeOfSrc, + final JsonSerializationContext context ) + { + final String mode; + switch ( src ) + { + case NEARESTNEIGHBOR: + mode = "nearest"; + break; + case NLINEAR: + mode = "linear"; + break; + default: + throw new IllegalArgumentException("Unexpected value: " + src); + } + return new JsonPrimitive( mode ); + } + } +} diff --git a/src/main/java/bdv/tools/links/LinkActions.java b/src/main/java/bdv/tools/links/LinkActions.java new file mode 100644 index 00000000..a6514b50 --- /dev/null +++ b/src/main/java/bdv/tools/links/LinkActions.java @@ -0,0 +1,137 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2025 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.tools.links; + +import static bdv.tools.links.ClipboardUtils.getFromClipboard; + +import org.scijava.plugin.Plugin; +import org.scijava.ui.behaviour.io.gui.CommandDescriptionProvider; +import org.scijava.ui.behaviour.io.gui.CommandDescriptions; +import org.scijava.ui.behaviour.util.Actions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; + +import bdv.KeyConfigContexts; +import bdv.KeyConfigScopes; +import bdv.viewer.AbstractViewerPanel; +import bdv.viewer.ConverterSetups; + +public class LinkActions +{ + private static final Logger LOG = LoggerFactory.getLogger( LinkActions.class ); + + public static final String COPY_VIEWER_STATE = "copy viewer state"; + public static final String PASTE_VIEWER_STATE = "paste viewer state"; + + public static final String[] COPY_VIEWER_STATE_KEYS = new String[] { "ctrl C", "meta C" }; + public static final String[] PASTE_VIEWER_STATE_KEYS = new String[] { "ctrl V", "meta V" }; + + /* + * Command descriptions for all provided commands + */ + @Plugin( type = CommandDescriptionProvider.class ) + public static class Descriptions extends CommandDescriptionProvider + { + public Descriptions() + { + super( KeyConfigScopes.BIGDATAVIEWER, KeyConfigContexts.BIGDATAVIEWER ); + } + + @Override + public void getCommandDescriptions( final CommandDescriptions descriptions ) + { + descriptions.add( COPY_VIEWER_STATE, COPY_VIEWER_STATE_KEYS, "Copy the current viewer state as a string." ); + descriptions.add( PASTE_VIEWER_STATE, PASTE_VIEWER_STATE_KEYS, "Paste the current viewer state from a string." ); + } + } + + private static void copyViewerState( + final AbstractViewerPanel panel, + final ConverterSetups converterSetups, + final ResourceManager resources ) + { + final JsonElement json = Links.copyJson( panel, converterSetups, resources ); + ClipboardUtils.copyToClipboard( json.toString() ); + } + + private static void pasteViewerState( + final AbstractViewerPanel panel, + final ConverterSetups converterSetups, + final PasteSettings pasteSettings, + final ResourceManager resources ) + { + final String pastedText = getFromClipboard(); + if ( pastedText == null ) + { + LOG.debug( "Couldn't get pasted text from clipboard." ); + return; + } + + final JsonElement json; + try + { + json = JsonParser.parseString( pastedText ); + } + catch ( JsonSyntaxException e ) + { + LOG.debug( "couldn't parse pasted string as JSON:\n\"{}\"", pastedText ); + return; + } + + try + { + Links.paste( json, panel, converterSetups, pasteSettings, resources ); + } + catch ( final JsonParseException | IllegalArgumentException e ) + { + LOG.debug( "pasted JSON is malformed:\n\"{}\"", pastedText, e ); + } + } + + /** + * Install into the specified {@link Actions}. + */ + public static void install( + final Actions actions, + final AbstractViewerPanel panel, + final ConverterSetups converterSetups, + final PasteSettings pasteSettings, + final ResourceManager resources ) + { + actions.runnableAction( () -> copyViewerState( panel, converterSetups, resources ), + COPY_VIEWER_STATE, COPY_VIEWER_STATE_KEYS ); + actions.runnableAction( () -> pasteViewerState( panel, converterSetups, pasteSettings, resources ), + PASTE_VIEWER_STATE, PASTE_VIEWER_STATE_KEYS ); + } +} diff --git a/src/main/java/bdv/tools/links/Links.java b/src/main/java/bdv/tools/links/Links.java new file mode 100644 index 00000000..6524c805 --- /dev/null +++ b/src/main/java/bdv/tools/links/Links.java @@ -0,0 +1,417 @@ +package bdv.tools.links; + +import static bdv.BigDataViewer.createConverterSetup; +import static bdv.tools.links.PasteSettings.RecenterMethod.MOUSE_POS; +import static bdv.tools.links.PasteSettings.RecenterMethod.PANEL_CENTER; +import static bdv.tools.links.PasteSettings.SourceMatchingMethod.BY_INDEX; +import static bdv.tools.links.PasteSettings.SourceMatchingMethod.BY_SPEC_LOAD_MISSING; + +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +import bdv.tools.JsonUtils; +import bdv.tools.brightness.ConverterSetup; +import bdv.tools.links.BdvPropertiesV0.SourceConverterConfig; +import bdv.tools.links.PasteSettings.RecenterMethod; +import bdv.tools.links.PasteSettings.RescaleMethod; +import bdv.viewer.AbstractViewerPanel; +import bdv.viewer.ConverterSetups; +import bdv.viewer.SourceAndConverter; +import bdv.viewer.ViewerState; +import net.imglib2.Dimensions; +import net.imglib2.FinalDimensions; +import net.imglib2.Point; +import net.imglib2.realtransform.AffineTransform3D; +import net.imglib2.util.Cast; + +class Links +{ + private static final Logger LOG = LoggerFactory.getLogger( Links.class ); + + static JsonElement copyJson( + final AbstractViewerPanel panel, + final ConverterSetups converterSetups, + final ResourceManager resources ) + { + final BdvPropertiesV0 properties = copyV0( panel, converterSetups, resources ); + final Gson gson = JsonUtils.gson(); + final VersionAndProperties versionAndProperties = new VersionAndProperties( 0, gson.toJsonTree( properties ) ); + return gson.toJsonTree( versionAndProperties ); + } + + static void paste( + final JsonElement json, + final AbstractViewerPanel panel, + final ConverterSetups converterSetups, + final PasteSettings pasteSettings, + final ResourceManager resources ) throws JsonParseException + { + final Gson gson = JsonUtils.gson(); + final VersionAndProperties versionAndProperties = gson.fromJson( json, VersionAndProperties.class ); + if ( versionAndProperties.version() == 0 ) + { + final BdvPropertiesV0 properties = gson.fromJson( versionAndProperties.properties(), BdvPropertiesV0.class ); + pasteV0( properties, panel, converterSetups, resources, pasteSettings ); + panel.requestRepaint(); + } + else + { + throw new JsonParseException( "Unsupported version: " + versionAndProperties.version() ); + } + } + + private static BdvPropertiesV0 copyV0( + final AbstractViewerPanel panel, + final ConverterSetups converterSetups, + final ResourceManager resources ) + { + final ViewerState state = panel.state(); + synchronized ( state ) + { + final Dimensions panelsize = new FinalDimensions( + panel.getDisplayComponent().getWidth(), + panel.getDisplayComponent().getHeight() ); + final Point mouse = new Point( 2 ); + panel.getMouseCoordinates( mouse ); + return new BdvPropertiesV0( state, converterSetups, panelsize, mouse, resources ); + } + } + + private static void pasteV0( + final BdvPropertiesV0 properties, + final AbstractViewerPanel panel, + final ConverterSetups converterSetups, + final ResourceManager resources, + final PasteSettings pasteSettings ) + { + final ViewerState state = panel.state(); + synchronized ( state ) + { + if ( pasteSettings.pasteViewerTransform() ) + { + final int panelWidth = panel.getDisplayComponent().getWidth(); + final int panelHeight = panel.getDisplayComponent().getHeight(); + final AffineTransform3D transform = adjustedViewerTransform( + properties, panelWidth, panelHeight, + pasteSettings.recenterMethod(), + pasteSettings.rescaleMethod() ); + state.setViewerTransform( transform ); + } + + if ( pasteSettings.pasteCurrentTimepoint() ) + { + state.setCurrentTimepoint( properties.timepoint() ); + } + + final int[] sourceMapping = getSourceMapping( + pasteSettings.sourceMatchingMethod(), + properties.sourceSpecs(), + state, converterSetups, resources ); + + if ( pasteSettings.pasteSourceConfigs() ) + { + setSourceConfigs( sourceMapping, properties.sourceConfigs(), state.getSources(), resources ); + } + + if ( pasteSettings.pasteDisplayMode() ) + { + state.setDisplayMode( properties.displaymode() ); + } + + if ( pasteSettings.pasteInterpolation() ) + { + state.setInterpolation( properties.interpolation() ); + } + if ( pasteSettings.pasteConverterConfigs() ) + { + setConverterConfigs( sourceMapping, properties.converterConfigs(), state.getSources(), converterSetups ); + } + if ( pasteSettings.pasteSourceVisibility() ) + { + setSourceVisibility( sourceMapping, properties.currentSourceIndex(), properties.activeSourceIndices(), state ); + } + } + } + + private static int[] getSourceMapping( + final PasteSettings.SourceMatchingMethod method, + final List< ResourceSpec< ? > > specs, + final ViewerState state, + final ConverterSetups converterSetups, + final ResourceManager resources ) + { + final int[] sourceMapping = matchSources( method, specs, state.getSources(), resources ); + if ( method == BY_SPEC_LOAD_MISSING ) + loadUnmatchedSources( sourceMapping, specs, state, converterSetups, resources ); + return sourceMapping; + } + + // return a mapping from index i in specs to index j in sources + // (j==-1 if no source matches the spec at i) + private static int[] matchSources( + final PasteSettings.SourceMatchingMethod method, + final List< ResourceSpec< ? > > specs, + final List< SourceAndConverter< ? > > sources, + final ResourceManager resources ) + { + final int[] matches = new int[ specs.size() ]; + if ( method == BY_INDEX ) + { + final int numSources = sources.size(); + Arrays.setAll( matches, i -> i < numSources ? i : -1 ); + } + else + { + for ( int i = 0; i < specs.size(); i++ ) + { + final SourceAndConverter< ? > soc = Cast.unchecked( resources.getResource( specs.get( i ) ) ); + matches[ i ] = sources.indexOf( soc ); + } + } + return matches; + } + + private static void loadUnmatchedSources( + final int[] sourceMapping, + final List< ResourceSpec< ? > > specs, + final ViewerState state, + final ConverterSetups converterSetups, + final ResourceManager resources ) + { + for ( int i = 0; i < sourceMapping.length; i++ ) + { + if ( sourceMapping[ i ] < 0 ) + { + final ResourceSpec< SourceAndConverter< ? > > spec = Cast.unchecked( specs.get( i ) ); + try + { + final SourceAndConverter< ? > soc = resources. + getOrCreateResource( spec ); + final ConverterSetup setup = createConverterSetup( soc, 0 ); + converterSetups.put( soc, setup ); + state.addSource( soc ); + sourceMapping[ i ] = state.getSources().indexOf( soc ); + } + catch ( final ResourceCreationException e ) + { + LOG.debug( "Couldn't load resource.", e ); + } + } + } + } + + private static void setSourceConfigs( + final int[] sourceMapping, + final List< ResourceConfig > configs, + final List< SourceAndConverter< ? > > sources, + final ResourceManager resources ) + { + for ( int i = 0; i < sourceMapping.length; i++ ) + { + if ( sourceMapping[ i ] >= 0 ) + { + final SourceAndConverter< ? > soc = sources.get( sourceMapping[ i ] ); + final ResourceSpec< ? > spec = resources.getResourceSpec( soc ); + configs.get( i ).apply( spec, resources ); + } + } + } + + // apply display range and color settings to matched sources + // sourceMapping[i]==j --> converterConfigs[i] corresponds to sources[j] + private static void setConverterConfigs( + final int[] sourceMapping, + final List< SourceConverterConfig > converterConfigs, + final List< SourceAndConverter< ? > > sources, + final ConverterSetups converterSetups ) + { + for ( int i = 0; i < sourceMapping.length; i++ ) + { + if ( sourceMapping[ i ] >= 0 ) + { + final SourceConverterConfig config = converterConfigs.get( i ); + if ( config != null ) + { + final SourceAndConverter< ? > soc = sources.get( sourceMapping[ i ] ); + final ConverterSetup setup = converterSetups.getConverterSetup( soc ); + if ( setup != null ) + { + converterSetups.getBounds().setBounds( setup, config.bounds() ); + setup.setDisplayRange( config.rangeMin(), config.rangeMax() ); + setup.setColor( config.color() ); + } + } + } + } + } + + private static void setSourceVisibility( + final int[] sourceMapping, + final int currentSourceIndex, + final int[] activeSourceIndices, + final ViewerState state ) + { + final List< SourceAndConverter< ? > > sources = state.getSources(); + for ( int i = 0; i < sourceMapping.length; i++ ) + { + if ( sourceMapping[ i ] >= 0 ) + { + final SourceAndConverter< ? > soc = sources.get( sourceMapping[ i ] ); + state.setSourceActive( soc, contains( activeSourceIndices, i ) ); + if ( currentSourceIndex == i ) + state.setCurrentSource( soc ); + } + } + } + + private static boolean contains( int[] elements, int element ) + { + for ( int e : elements ) + if ( e == element ) + return true; + return false; + } + + private static AffineTransform3D adjustedViewerTransform( + final BdvPropertiesV0 properties, + final int panelWidth, + final int panelHeight, + final RecenterMethod recenterMethod, + final RescaleMethod rescaleMethod ) + { + // take the transform from properties + AffineTransform3D t = properties.transform(); + + if ( recenterMethod == PANEL_CENTER ) + { + // shift it to the center of the panel that it was copied from + final long sx = properties.panelsize().dimension( 0 ) / 2; + final long sy = properties.panelsize().dimension( 1 ) / 2; + t = shift( t, sx, sy ); + } + else if ( recenterMethod == MOUSE_POS ) + { + // shift it to the mouse position when it was copied + final long sx = properties.mousepos().getLongPosition( 0 ); + final long sy = properties.mousepos().getLongPosition( 1 ); + t = shift( t, sx, sy ); + } + + if ( rescaleMethod != RescaleMethod.NONE ) + { + // scale factors to fit panel width and height + final double scaleX = ( double ) panelWidth / properties.panelsize().dimension( 0 ); + final double scaleY = ( double ) panelHeight / properties.panelsize().dimension( 1 ); + final double scale = ( rescaleMethod == RescaleMethod.FIT_PANEL ) + ? Math.min( scaleX, scaleY ) + : Math.max( scaleX, scaleY ); + t.scale( scale, scale, 1 ); + } + + if ( recenterMethod != RecenterMethod.NONE ) + { + // shift it back to the top-left corner of the panel we want to paste it into + t = shift( t, -panelWidth / 2.0, -panelHeight / 2.0 ); + } + + return t; + } + + /** + * Shift world-to-screen transform by (sx,sy) in screen coordinates. + *

+ * For example, with (sx,sy) = (windowWidth/2, windowHeight/2), this make + * the viewer transform obtained from {@code ViewerState} relative to the + * window center instead of relative to the top-left corner. + */ + private static AffineTransform3D shift( final AffineTransform3D transform, final double sx, final double sy ) + { + final AffineTransform3D t = new AffineTransform3D(); + t.set( transform ); + t.set( t.get( 0, 3 ) - sx, 0, 3 ); + t.set( t.get( 1, 3 ) - sy, 1, 3 ); + return t; + } + + static class VersionAndProperties + { + private final int version; + + private final JsonElement properties; + + VersionAndProperties( int version, JsonElement properties ) + { + this.version = version; + this.properties = properties; + } + + public int version() + { + return version; + } + + public JsonElement properties() + { + return properties; + } + + @JsonUtils.JsonIo( jsonType = "VersionAndProperties", type = VersionAndProperties.class ) + static class Adapter implements JsonDeserializer< VersionAndProperties >, JsonSerializer< VersionAndProperties > + { + @Override + public VersionAndProperties deserialize( + final JsonElement json, + final Type typeOfT, + final JsonDeserializationContext context ) throws JsonParseException + { + if ( !json.isJsonObject() ) + throw new JsonParseException( "expected object. (got \"" + json + "\")" ); + + final JsonElement bdv = json.getAsJsonObject().get( "bdv" ); + if ( bdv == null || !bdv.isJsonObject() ) + throw new JsonParseException( "expected a \"bdv\" object attribute. (got \"" + json + "\")" ); + + final int version; + try { + version = bdv.getAsJsonObject().get( "version" ).getAsInt(); + } catch ( final Exception e ) { + throw new JsonParseException( "expected a \"version\" integer attribute. (got \"" + bdv + "\")" ); + } + + final JsonElement properties = bdv.getAsJsonObject().get( "properties" ); + if ( properties == null || !properties.isJsonObject() ) + throw new JsonParseException( "expected a \"properties\" object attribute. (got \"" + bdv + "\")" ); + + return new VersionAndProperties( version, properties ); + } + + @Override + public JsonElement serialize( + final VersionAndProperties src, + final Type typeOfSrc, + final JsonSerializationContext context ) + { + final JsonObject bdv = new JsonObject(); + bdv.addProperty( "version", src.version ); + bdv.add( "properties", src.properties ); + final JsonObject obj = new JsonObject(); + obj.add( "bdv", bdv ); + return obj; + } + } + } +} + diff --git a/src/main/java/bdv/tools/links/PasteSettings.java b/src/main/java/bdv/tools/links/PasteSettings.java new file mode 100644 index 00000000..489fc0db --- /dev/null +++ b/src/main/java/bdv/tools/links/PasteSettings.java @@ -0,0 +1,72 @@ +package bdv.tools.links; + +public interface PasteSettings +{ + enum SourceMatchingMethod + { + BY_SPEC_LOAD_MISSING, + BY_SPEC, + BY_INDEX + } + + enum RescaleMethod + { + /** + * Do not rescale transform. + */ + NONE, + /** + * Compute scale factors from {@link BdvPropertiesV0#panelsize() + * recorded} to current panel width, and recorded to current panel + * height. Use the smaller of those scale factors. + */ + FIT_PANEL, + /** + * Compute scale factors from {@link BdvPropertiesV0#panelsize() + * recorded} to current panel width, and recorded to current panel + * height. Use the larger of those scale factors. + */ + FILL_PANEL + } + + enum RecenterMethod + { + /** + * Do not recenter. That means, the world coordinate mapping to the + * recorded panel min corner is mapped to the current panel min corner. + */ + NONE, + /** + * Shift such the world coordinate mapping to the {@link + * BdvPropertiesV0#panelsize() recorded} panel center is mapped to the + * current panel center. + */ + PANEL_CENTER, + /** + * Shift such the world coordinate mapping to the {@link + * BdvPropertiesV0#mousepos() recorded} mouse position is mapped to the + * current panel center. + */ + MOUSE_POS + } + + boolean pasteViewerTransform(); + + boolean pasteCurrentTimepoint(); + + SourceMatchingMethod sourceMatchingMethod(); + + RescaleMethod rescaleMethod(); + + RecenterMethod recenterMethod(); + + boolean pasteSourceConfigs(); + + boolean pasteDisplayMode(); + + boolean pasteInterpolation(); + + boolean pasteConverterConfigs(); + + boolean pasteSourceVisibility(); +} diff --git a/src/main/java/bdv/ui/links/LinkCard.java b/src/main/java/bdv/ui/links/LinkCard.java new file mode 100644 index 00000000..a81bd2c0 --- /dev/null +++ b/src/main/java/bdv/ui/links/LinkCard.java @@ -0,0 +1,25 @@ +package bdv.ui.links; + +import java.awt.Insets; + +import bdv.ui.CardPanel; +import bdv.ui.links.LinkSettingsPage.LinkSettingsPanel; + +public class LinkCard +{ + public static final String BDV_LINK_SETTINGS_CARD = "bdv link settings card"; + + public static void install( final LinkSettings linkSettings, final CardPanel cards ) + { + final LinkSettings.UpdateListener update = () -> { + final boolean show = linkSettings.showLinkSettingsCard(); + final boolean shown = cards.indexOf( BDV_LINK_SETTINGS_CARD ) > 0; + if ( show && !shown ) + cards.addCard( BDV_LINK_SETTINGS_CARD, "Copy&Paste", new LinkSettingsPanel( linkSettings ), true, new Insets( 3, 20, 0, 0 ) ); + else if ( shown && !show ) + cards.removeCard( BDV_LINK_SETTINGS_CARD ); + }; + linkSettings.updateListeners().add( update ); + update.appearanceChanged(); + } +} diff --git a/src/main/java/bdv/ui/links/LinkSettings.java b/src/main/java/bdv/ui/links/LinkSettings.java new file mode 100644 index 00000000..2354d380 --- /dev/null +++ b/src/main/java/bdv/ui/links/LinkSettings.java @@ -0,0 +1,315 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2025 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.ui.links; + +import static bdv.tools.links.PasteSettings.SourceMatchingMethod.BY_SPEC_LOAD_MISSING; + +import org.scijava.listeners.Listeners; + +import bdv.tools.links.PasteSettings; +import bdv.tools.links.PasteSettings.RecenterMethod; +import bdv.tools.links.PasteSettings.RescaleMethod; +import bdv.tools.links.PasteSettings.SourceMatchingMethod; + +/** + * Settings for copying and pasting links. + *

+ * Listeners can be registered and will be notified of changes. + */ +public class LinkSettings +{ + private boolean pasteDisplayMode = true; + private boolean pasteViewerTransform = true; + private boolean pasteCurrentTimepoint = true; + private boolean pasteSourceVisibility = true; + private boolean pasteSourceConverterConfigs = true; + private boolean pasteSourceConfigs = true; + private SourceMatchingMethod sourceMatchingMethod = BY_SPEC_LOAD_MISSING; + private RecenterMethod recenterMethod = RecenterMethod.PANEL_CENTER; + private RescaleMethod rescaleMethod = RescaleMethod.FIT_PANEL; + private boolean showLinkSettingsCard = false; + + public interface UpdateListener + { + void appearanceChanged(); + } + + private final Listeners.List< UpdateListener > updateListeners; + + public LinkSettings() + { + updateListeners = new Listeners.SynchronizedList<>(); + } + + public void set( final LinkSettings other ) + { + this.pasteDisplayMode = other.pasteDisplayMode; + this.pasteViewerTransform = other.pasteViewerTransform; + this.pasteCurrentTimepoint = other.pasteCurrentTimepoint; + this.pasteSourceVisibility = other.pasteSourceVisibility; + this.pasteSourceConverterConfigs = other.pasteSourceConverterConfigs; + this.pasteSourceConfigs = other.pasteSourceConfigs; + this.sourceMatchingMethod = other.sourceMatchingMethod; + this.recenterMethod = other.recenterMethod; + this.rescaleMethod = other.rescaleMethod; + this.showLinkSettingsCard = other.showLinkSettingsCard; + notifyListeners(); + } + + private void notifyListeners() + { + updateListeners.list.forEach( UpdateListener::appearanceChanged ); + } + + public Listeners< UpdateListener > updateListeners() + { + return updateListeners; + } + + public boolean pasteDisplayMode() + { + return pasteDisplayMode; + } + + public void setPasteDisplayMode( final boolean b ) + { + if ( pasteDisplayMode != b ) + { + pasteDisplayMode = b; + notifyListeners(); + } + } + + public boolean pasteViewerTransform() + { + return pasteViewerTransform; + } + + public void setPasteViewerTransform( final boolean b ) + { + if ( pasteViewerTransform != b ) + { + pasteViewerTransform = b; + notifyListeners(); + } + } + + public boolean pasteCurrentTimepoint() + { + return pasteCurrentTimepoint; + } + + public void setPasteCurrentTimepoint( final boolean b ) + { + if ( pasteCurrentTimepoint != b ) + { + pasteCurrentTimepoint = b; + notifyListeners(); + } + } + + public boolean pasteSourceVisibility() + { + return pasteSourceVisibility; + } + + public void setPasteSourceVisibility( final boolean b ) + { + if ( pasteSourceVisibility != b ) + { + pasteSourceVisibility = b; + notifyListeners(); + } + } + + public boolean pasteSourceConverterConfigs() + { + return pasteSourceConverterConfigs; + } + + public void setPasteSourceConverterConfigs( final boolean b ) + { + if ( pasteSourceConverterConfigs != b ) + { + pasteSourceConverterConfigs = b; + notifyListeners(); + } + } + + public boolean pasteSourceConfigs() + { + return pasteSourceConfigs; + } + + public void setPasteSourceConfigs( final boolean b ) + { + if ( pasteSourceConfigs != b ) + { + pasteSourceConfigs = b; + notifyListeners(); + } + } + + public SourceMatchingMethod sourceMatchingMethod() + { + return sourceMatchingMethod; + } + + public void setSourceMatchingMethod( final SourceMatchingMethod m ) + { + if ( sourceMatchingMethod != m ) + { + sourceMatchingMethod = m; + notifyListeners(); + } + } + + public RecenterMethod recenterMethod() + { + return recenterMethod; + } + + public void setRecenterMethod( final RecenterMethod m ) + { + if ( recenterMethod != m ) + { + recenterMethod = m; + notifyListeners(); + } + } + + public RescaleMethod rescaleMethod() + { + return rescaleMethod; + } + + public void setRescaleMethod( final RescaleMethod m ) + { + if ( rescaleMethod != m ) + { + rescaleMethod = m; + notifyListeners(); + } + } + + public boolean showLinkSettingsCard() + { + return showLinkSettingsCard; + } + + public void setShowLinkSettingsCard( final boolean b ) + { + if ( showLinkSettingsCard != b ) + { + showLinkSettingsCard = b; + notifyListeners(); + } + } + + @Override + public String toString() + { + return "LinkSettings{" + "pasteDisplayMode=" + pasteDisplayMode + + ", pasteViewerTransform=" + pasteViewerTransform + + ", pasteCurrentTimepoint=" + pasteCurrentTimepoint + + ", pasteSourceVisibility=" + pasteSourceVisibility + + ", pasteSourceConverterConfigs=" + pasteSourceConverterConfigs + + ", pasteSourceConfigs=" + pasteSourceConfigs + + ", sourceMatchingMethod=" + sourceMatchingMethod + + ", showLinkSettingsCard=" + showLinkSettingsCard + + '}'; + } + + public PasteSettings pasteSettings() + { + // view of this LinkSettings as PasteSettings + return new PasteSettings() + { + @Override + public boolean pasteViewerTransform() + { + return pasteViewerTransform; + } + + @Override + public boolean pasteCurrentTimepoint() + { + return pasteCurrentTimepoint; + } + + @Override + public SourceMatchingMethod sourceMatchingMethod() + { + return sourceMatchingMethod; + } + + @Override + public RescaleMethod rescaleMethod() + { + return rescaleMethod; + } + + @Override + public RecenterMethod recenterMethod() + { + return recenterMethod; + } + + @Override + public boolean pasteSourceConfigs() + { + return pasteSourceConfigs; + } + + @Override + public boolean pasteDisplayMode() + { + return pasteDisplayMode; + } + + @Override + public boolean pasteInterpolation() + { + return pasteDisplayMode; + } + + @Override + public boolean pasteConverterConfigs() + { + return pasteSourceConverterConfigs; + } + + @Override + public boolean pasteSourceVisibility() + { + return pasteSourceVisibility; + } + }; + } +} diff --git a/src/main/java/bdv/ui/links/LinkSettingsIO.java b/src/main/java/bdv/ui/links/LinkSettingsIO.java new file mode 100644 index 00000000..fab88bdd --- /dev/null +++ b/src/main/java/bdv/ui/links/LinkSettingsIO.java @@ -0,0 +1,157 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2025 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.ui.links; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.AbstractConstruct; +import org.yaml.snakeyaml.constructor.Constructor; +import org.yaml.snakeyaml.nodes.MappingNode; +import org.yaml.snakeyaml.nodes.Node; +import org.yaml.snakeyaml.nodes.Tag; +import org.yaml.snakeyaml.representer.Represent; +import org.yaml.snakeyaml.representer.Representer; + +import bdv.tools.links.PasteSettings; + +/** + * De/serialize {@link LinkSettings} to/from YAML file + */ +public class LinkSettingsIO +{ + private static final Logger LOG = LoggerFactory.getLogger( LinkSettingsIO.class ); + + public static LinkSettings load( final String filename ) throws IOException + { + final FileReader input = new FileReader( filename ); + final LoaderOptions loaderOptions = new LoaderOptions(); + final Yaml yaml = new Yaml( new LinkSettingsConstructor( loaderOptions ) ); + final Iterable< Object > objs = yaml.loadAll( input ); + final List< Object > list = new ArrayList<>(); + objs.forEach( list::add ); + if ( list.size() != 1 ) + throw new IllegalArgumentException( "unexpected input in yaml file" ); + return ( LinkSettings ) list.get( 0 ); + } + + public static void save( final LinkSettings settings, final String filename ) throws IOException + { + new File( filename ).getParentFile().mkdirs(); + final FileWriter output = new FileWriter( filename ); + final DumperOptions dumperOptions = new DumperOptions(); + dumperOptions.setDefaultFlowStyle( DumperOptions.FlowStyle.BLOCK ); + final Yaml yaml = new Yaml( new LinkSettingsRepresenter( dumperOptions ), dumperOptions ); + final ArrayList< Object > objects = new ArrayList<>(); + objects.add( settings ); + yaml.dumpAll( objects.iterator(), output ); + output.close(); + } + + static final Tag LINKSETTINGS_TAG = new Tag( "!linksettings" ); + + static class LinkSettingsRepresenter extends Representer + { + public LinkSettingsRepresenter( final DumperOptions dumperOptions ) + { + super( dumperOptions ); + this.representers.put( LinkSettings.class, new RepresentLinkSettings() ); + } + + private class RepresentLinkSettings implements Represent + { + @Override + public Node representData( final Object data ) + { + final LinkSettings s = ( LinkSettings ) data; + final Map< String, Object > mapping = new LinkedHashMap<>(); + mapping.put( "pasteDisplayMode", s.pasteDisplayMode() ); + mapping.put( "pasteViewerTransform", s.pasteViewerTransform() ); + mapping.put( "recenterMethod", s.recenterMethod().name() ); + mapping.put( "rescaleMethod", s.rescaleMethod().name() ); + mapping.put( "pasteCurrentTimepoint", s.pasteCurrentTimepoint() ); + mapping.put( "pasteSourceVisibility", s.pasteSourceVisibility() ); + mapping.put( "pasteSourceConverterConfigs", s.pasteSourceConverterConfigs() ); + mapping.put( "pasteSourceConfigs", s.pasteSourceConfigs() ); + mapping.put( "sourceMatchingMethod", s.sourceMatchingMethod().name() ); + mapping.put( "showLinkSettingsCard", s.showLinkSettingsCard() ); + return representMapping( LINKSETTINGS_TAG, mapping, getDefaultFlowStyle() ); + } + } + } + + static class LinkSettingsConstructor extends Constructor + { + public LinkSettingsConstructor( final LoaderOptions loaderOptions ) + { + super( loaderOptions ); + this.yamlConstructors.put( LINKSETTINGS_TAG, new ConstructLinkSettings() ); + } + + private class ConstructLinkSettings extends AbstractConstruct + { + @Override + public Object construct( final Node node ) + { + try + { + final Map< Object, Object > mapping = constructMapping( ( MappingNode ) node ); + final LinkSettings s = new LinkSettings(); + s.setPasteDisplayMode( ( Boolean ) mapping.get( "pasteDisplayMode" ) ); + s.setPasteViewerTransform( ( Boolean ) mapping.get( "pasteViewerTransform" ) ); + s.setRecenterMethod( PasteSettings.RecenterMethod.valueOf( ( String ) mapping.get( "recenterMethod" ) ) ); + s.setRescaleMethod( PasteSettings.RescaleMethod.valueOf( ( String ) mapping.get( "rescaleMethod" ) ) ); + s.setPasteCurrentTimepoint( ( Boolean ) mapping.get( "pasteCurrentTimepoint" ) ); + s.setPasteSourceVisibility( ( Boolean ) mapping.get( "pasteSourceVisibility" ) ); + s.setPasteSourceConverterConfigs( ( Boolean ) mapping.get( "pasteSourceConverterConfigs" ) ); + s.setPasteSourceConfigs( ( Boolean ) mapping.get( "pasteSourceConfigs" ) ); + s.setSourceMatchingMethod( PasteSettings.SourceMatchingMethod.valueOf( ( String ) mapping.get( "sourceMatchingMethod" ) ) ); + s.setShowLinkSettingsCard( ( Boolean ) mapping.get( "showLinkSettingsCard" ) ); + return s; + } + catch( final Exception e ) + { + LOG.info( "Error constructing LinkSettings", e ); + } + return null; + } + } + } +} diff --git a/src/main/java/bdv/ui/links/LinkSettingsManager.java b/src/main/java/bdv/ui/links/LinkSettingsManager.java new file mode 100644 index 00000000..cdfbd51a --- /dev/null +++ b/src/main/java/bdv/ui/links/LinkSettingsManager.java @@ -0,0 +1,108 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2025 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.ui.links; + +import java.io.FileNotFoundException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Manages the {@link LinkSettings} (save/load config file). + */ +public class LinkSettingsManager +{ + private static final Logger LOG = LoggerFactory.getLogger( LinkSettingsManager.class ); + + private static final String CONFIG_FILE_NAME = "linksettings.yaml"; + + private final String configFile; + + /** + * The managed LinkSettings. This will be updated with changes from the + * Preferences (on "Apply" or "Ok"). + */ + private final LinkSettings settings; + + public LinkSettingsManager() + { + this( null ); + } + + public LinkSettingsManager( final String configDir ) + { + configFile = configDir == null ? null : configDir + "/" + CONFIG_FILE_NAME; + settings = new LinkSettings(); + load(); + } + + public LinkSettings linkSettings() + { + return settings; + } + + void load() + { + load( configFile ); + } + + void load( final String filename ) + { + try + { + final LinkSettings s = LinkSettingsIO.load( filename ); + settings.set( s ); + } + catch ( final FileNotFoundException e ) + { + LOG.info( "LinkSettings file {} not found. Using defaults.", filename, e ); + } + catch ( final Exception e ) + { + LOG.warn( "Error while reading LinkSettings file {}. Using defaults.", filename, e ); + } + } + + void save() + { + save( configFile ); + } + + void save( final String filename ) + { + try + { + LinkSettingsIO.save( settings, filename ); + } + catch ( final Exception e ) + { + LOG.warn( "Error while writing LinkSettings file {}", filename, e ); + } + } +} diff --git a/src/main/java/bdv/ui/links/LinkSettingsPage.java b/src/main/java/bdv/ui/links/LinkSettingsPage.java new file mode 100644 index 00000000..4c92adc8 --- /dev/null +++ b/src/main/java/bdv/ui/links/LinkSettingsPage.java @@ -0,0 +1,244 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2025 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.ui.links; + +import static bdv.ui.settings.StyleElements.booleanElement; +import static bdv.ui.settings.StyleElements.comboBoxElement; +import static bdv.ui.settings.StyleElements.linkedCheckBox; +import static bdv.ui.settings.StyleElements.linkedComboBox; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +import javax.swing.Box; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSeparator; + +import org.scijava.listeners.Listeners; + +import bdv.tools.links.PasteSettings.RecenterMethod; +import bdv.tools.links.PasteSettings.RescaleMethod; +import bdv.tools.links.PasteSettings.SourceMatchingMethod; +import bdv.ui.settings.ModificationListener; +import bdv.ui.settings.SettingsPage; +import bdv.ui.settings.StyleElements; +import bdv.ui.settings.StyleElements.BooleanElement; +import bdv.ui.settings.StyleElements.ComboBoxElement; +import net.miginfocom.swing.MigLayout; + +/** + * Preferences page for changing {@link LinkSettings}. + */ +public class LinkSettingsPage implements SettingsPage +{ + private final String treePath; + + private final LinkSettingsManager manager; + + private final Listeners.List< ModificationListener > modificationListeners; + + private final LinkSettings editedLinkSettings; + + private final JPanel panel; + + public LinkSettingsPage( final LinkSettingsManager manager ) + { + this( "LinkSettings", manager ); + } + + public LinkSettingsPage( final String treePath, final LinkSettingsManager manager ) + { + this.treePath = treePath; + this.manager = manager; + editedLinkSettings = new LinkSettings(); + editedLinkSettings.set( manager.linkSettings() ); + panel = new Panel( editedLinkSettings ); + modificationListeners = new Listeners.SynchronizedList<>(); + editedLinkSettings.updateListeners().add( () -> modificationListeners.list.forEach( l -> l.setModified() ) ); + } + + @Override + public String getTreePath() + { + return treePath; + } + + @Override + public JPanel getJPanel() + { + return panel; + } + + @Override + public Listeners< ModificationListener > modificationListeners() + { + return modificationListeners; + } + + @Override + public void cancel() + { + editedLinkSettings.set( manager.linkSettings() ); + } + + @Override + public void apply() + { + manager.linkSettings().set( editedLinkSettings ); + manager.save(); + } + + // -------------------------------------------------------------------- + + static class Panel extends JPanel + { + private boolean bla; + + public Panel( final LinkSettings ls ) + { + super( new MigLayout( "fillx", "[l]", "" ) ); + + final BooleanElement be = booleanElement( "show link settings in side panel", ls::showLinkSettingsCard, ls::setShowLinkSettingsCard ); + ls.updateListeners().add( be::update ); + + add( new LinkSettingsPanel( ls, false ), "l, wrap" ); + add( new JSeparator(), "growx, wrap" ); + add( linkedCheckBox( be, be.getLabel() ), "l, wrap" ); + } + } + + // -------------------------------------------------------------------- + + public static class LinkSettingsPanel extends JPanel + { + public LinkSettingsPanel( final LinkSettings ls ) + { + this( ls, true ); + } + + private final List< StyleElements.StyleElement > styleElements = new ArrayList<>(); + + private LinkSettingsPanel( final LinkSettings ls, boolean inCardPanel ) + { + super( new MigLayout( "fillx, insets " + ( inCardPanel ? "0" : "0 0 40 0" ), "[l]", "" ) ); + + final SourceMatchingMethod[] matchingMethods = { + SourceMatchingMethod.BY_SPEC_LOAD_MISSING, + SourceMatchingMethod.BY_SPEC, + SourceMatchingMethod.BY_INDEX + }; + final String[] matchingMethodLabels = { + "by spec, load unmatched", + "by spec", + "by index" + }; + + final RescaleMethod[] rescaleMethods = { + RescaleMethod.NONE, + RescaleMethod.FIT_PANEL, + RescaleMethod.FILL_PANEL + }; + final String[] rescaleMethodLabels = { + "--", + "fit to panel", + "fill panel" + }; + + final RecenterMethod[] recenterMethods = { + RecenterMethod.NONE, + RecenterMethod.PANEL_CENTER, + RecenterMethod.MOUSE_POS + }; + final String[] recenterMethodLabels = { + "--", + "pasted panel center", + "pasted mouse pos" + }; + + add( new JLabel( "when pasting links ..." ), "growx, gapbottom 5, wrap" ); + addCheckBox( booleanElement( "set display mode", ls::pasteDisplayMode, ls::setPasteDisplayMode ) ); + addCheckBox( booleanElement( "set timepoint", ls::pasteCurrentTimepoint, ls::setPasteCurrentTimepoint ) ); + final JCheckBox setTransformCheckBox = addCheckBox( booleanElement( "set view transform", ls::pasteViewerTransform, ls::setPasteViewerTransform ) ); + final Consumer< Boolean > enableRescale = addComboBox( true, 20, comboBoxElement( "rescale", ls::rescaleMethod, ls::setRescaleMethod, rescaleMethods, rescaleMethodLabels ) ); + final Consumer< Boolean > enableRecenter = addComboBox( true, 20, comboBoxElement( "recenter", ls::recenterMethod, ls::setRecenterMethod, recenterMethods, recenterMethodLabels ) ); + add( Box.createVerticalStrut( 5 ), "growx, wrap" ); + addCheckBox( booleanElement( "set source visibility", ls::pasteSourceVisibility, ls::setPasteSourceVisibility ) ); + addCheckBox( booleanElement( "set source min/max and color", ls::pasteSourceConverterConfigs, ls::setPasteSourceConverterConfigs ) ); + add( Box.createVerticalStrut( 5 ), "growx, wrap" ); + addComboBox( !inCardPanel, 0, comboBoxElement( "match sources", ls::sourceMatchingMethod, ls::setSourceMatchingMethod, matchingMethods, matchingMethodLabels ) ); + + ls.updateListeners().add( () -> { + styleElements.forEach( StyleElements.StyleElement::update ); + repaint(); + } ); + + setTransformCheckBox.addActionListener( e -> { + enableRescale.accept( setTransformCheckBox.isSelected() ); + enableRecenter.accept( setTransformCheckBox.isSelected() ); + } ); + enableRescale.accept( setTransformCheckBox.isSelected() ); + enableRecenter.accept( setTransformCheckBox.isSelected() ); + } + + private JCheckBox addCheckBox( BooleanElement element ) + { + final JCheckBox checkBox = linkedCheckBox( element, element.getLabel() ); + add( checkBox, "l, wrap" ); + styleElements.add( element ); + return checkBox; + } + + private Consumer< Boolean > addComboBox( boolean singleRow, int gapleft, ComboBoxElement< ? > element ) + { + JPanel row = new JPanel( new MigLayout( "insets 0, nogrid", "", "" ) ); + final JLabel label = new JLabel( element.getLabel() ); + final JComboBox< ? > comboBox = linkedComboBox( element ); + if ( singleRow ) + { + row.add( label, "l" ); + row.add( comboBox, "l, growx, wrap 0" ); + } + else + { + row.add( label, "l, wrap" ); + row.add( comboBox, "l, wrap 0" ); + } + add( row, "l, gapleft " + gapleft + ", growx, wrap" ); + styleElements.add( element ); + return b -> { + label.setEnabled( b ); + comboBox.setEnabled( b ); + }; + } + } +} diff --git a/src/main/java/bdv/util/BdvFunctions.java b/src/main/java/bdv/util/BdvFunctions.java index edcea22d..c745d029 100644 --- a/src/main/java/bdv/util/BdvFunctions.java +++ b/src/main/java/bdv/util/BdvFunctions.java @@ -32,6 +32,7 @@ import java.util.Collections; import java.util.List; +import bdv.tools.links.ResourceManager; import net.imglib2.FinalInterval; import net.imglib2.Interval; import net.imglib2.RandomAccessible; @@ -571,7 +572,7 @@ private static < T extends NumericType< T > > BdvStackSource< T > addRandomAcces { s = new RandomAccessibleIntervalSource<>( stack, type, sourceTransform, name ); } - addSourceToListsGenericType( s, handle.getUnusedSetupId(), converterSetups, sources ); + addSourceToListsGenericType( s, handle.getUnusedSetupId(), converterSetups, sources, handle.getResourceManager() ); } handle.add( converterSetups, sources, numTimepoints ); final BdvStackSource< T > bdvSource = new BdvStackSource<>( handle, numTimepoints, type, converterSetups, sources ); @@ -629,7 +630,7 @@ private static < T extends NumericType< T > > BdvStackSource< T > addRandomAcces s = new RandomAccessibleSource4D<>( stack, stackInterval, type, sourceTransform, name ); else s = new RandomAccessibleSource<>( stack, stackInterval, type, sourceTransform, name ); - addSourceToListsGenericType( s, handle.getUnusedSetupId(), converterSetups, sources ); + addSourceToListsGenericType( s, handle.getUnusedSetupId(), converterSetups, sources, handle.getResourceManager() ); } handle.add( converterSetups, sources, numTimepoints ); @@ -708,7 +709,7 @@ private static < T > BdvStackSource< T > addSource( final T type = source.getType(); final List< ConverterSetup > converterSetups = new ArrayList<>(); final List< SourceAndConverter< T > > sources = new ArrayList<>(); - addSourceToListsGenericType( source, handle.getUnusedSetupId(), converterSetups, sources ); + addSourceToListsGenericType( source, handle.getUnusedSetupId(), converterSetups, sources, handle.getResourceManager() ); handle.add( converterSetups, sources, numTimepoints ); final BdvStackSource< T > bdvSource = new BdvStackSource<>( handle, numTimepoints, type, converterSetups, sources ); handle.addBdvSource( bdvSource ); @@ -737,11 +738,12 @@ private static < T > void addSourceToListsGenericType( final Source< T > source, final int setupId, final List< ConverterSetup > converterSetups, - final List< SourceAndConverter< T > > sources ) + final List< SourceAndConverter< T > > sources, + final ResourceManager resourceManager ) { final T type = source.getType(); if ( type instanceof RealType || type instanceof ARGBType || type instanceof VolatileARGBType ) - addSourceToListsNumericType( ( Source ) source, setupId, converterSetups, ( List ) sources ); + addSourceToListsNumericType( ( Source ) source, setupId, converterSetups, ( List ) sources, resourceManager ); else throw new IllegalArgumentException( "Unknown source type. Expected RealType, ARGBType, or VolatileARGBType" ); } @@ -767,11 +769,13 @@ private static < T extends NumericType< T > > void addSourceToListsNumericType( final Source< T > source, final int setupId, final List< ConverterSetup > converterSetups, - final List< SourceAndConverter< T > > sources ) + final List< SourceAndConverter< T > > sources, + final ResourceManager resourceManager ) { final T type = source.getType(); final SourceAndConverter< T > soc = BigDataViewer.wrapWithTransformedSource( - new SourceAndConverter<>( source, BigDataViewer.createConverterToARGB( type ) ) ); + new SourceAndConverter<>( source, BigDataViewer.createConverterToARGB( type ) ), + resourceManager ); converterSetups.add( BigDataViewer.createConverterSetup( soc, setupId ) ); sources.add( soc ); } diff --git a/src/main/java/bdv/util/BdvHandle.java b/src/main/java/bdv/util/BdvHandle.java index 54af84a7..8750b75b 100644 --- a/src/main/java/bdv/util/BdvHandle.java +++ b/src/main/java/bdv/util/BdvHandle.java @@ -28,9 +28,11 @@ */ package bdv.util; +import bdv.tools.links.ResourceManager; import bdv.ui.CardPanel; import bdv.ui.appearance.AppearanceManager; import bdv.ui.keymap.KeymapManager; +import bdv.ui.links.LinkSettingsManager; import bdv.ui.splitpanel.SplitPanel; import bdv.viewer.ConverterSetups; import bdv.viewer.ViewerStateChangeListener; @@ -130,6 +132,10 @@ public CacheControls getCacheControls() public abstract AppearanceManager getAppearanceManager(); + public abstract LinkSettingsManager getLinkSettingsManager(); + + public abstract ResourceManager getResourceManager(); + @Deprecated int getUnusedSetupId() { diff --git a/src/main/java/bdv/util/BdvHandleFrame.java b/src/main/java/bdv/util/BdvHandleFrame.java index ac604f19..bfb3201c 100644 --- a/src/main/java/bdv/util/BdvHandleFrame.java +++ b/src/main/java/bdv/util/BdvHandleFrame.java @@ -28,9 +28,11 @@ */ package bdv.util; +import bdv.tools.links.ResourceManager; import bdv.ui.UIUtils; import bdv.ui.appearance.AppearanceManager; import bdv.ui.keymap.KeymapManager; +import bdv.ui.links.LinkSettingsManager; import bdv.viewer.ViewerStateChange; import java.awt.event.WindowEvent; import java.util.ArrayList; @@ -101,6 +103,18 @@ public AppearanceManager getAppearanceManager() return bdv.getAppearanceManager(); } + @Override + public LinkSettingsManager getLinkSettingsManager() + { + return bdv.getLinkSettingsManager(); + } + + @Override + public ResourceManager getResourceManager() + { + return bdvOptions.values.getResourceManager(); + } + @Override public InputActionBindings getKeybindings() { diff --git a/src/main/java/bdv/util/BdvHandlePanel.java b/src/main/java/bdv/util/BdvHandlePanel.java index 17b45e36..62551db4 100644 --- a/src/main/java/bdv/util/BdvHandlePanel.java +++ b/src/main/java/bdv/util/BdvHandlePanel.java @@ -28,11 +28,15 @@ */ package bdv.util; +import bdv.tools.links.LinkActions; +import bdv.tools.links.PasteSettings; +import bdv.tools.links.ResourceManager; import bdv.ui.BdvDefaultCards; import bdv.ui.CardPanel; import bdv.ui.UIUtils; import bdv.ui.appearance.AppearanceManager; import bdv.ui.keymap.KeymapManager; +import bdv.ui.links.LinkSettingsManager; import bdv.ui.splitpanel.SplitPanel; import bdv.viewer.ConverterSetups; import java.awt.Frame; @@ -92,6 +96,10 @@ public class BdvHandlePanel extends BdvHandle private final AppearanceManager appearanceManager; + private final LinkSettingsManager linkSettingsManager; + + private final ResourceManager resourceManager; + public BdvHandlePanel( final Frame dialogOwner, final BdvOptions options ) { super( options ); @@ -99,8 +107,11 @@ public BdvHandlePanel( final Frame dialogOwner, final BdvOptions options ) final KeymapManager optionsKeymapManager = options.values.getKeymapManager(); final AppearanceManager optionsAppearanceManager = options.values.getAppearanceManager(); + final LinkSettingsManager optionsLinkSettingsManager = options.values.getLinkSettingsManager(); keymapManager = optionsKeymapManager != null ? optionsKeymapManager : new KeymapManager( BigDataViewer.configDir ); appearanceManager = optionsAppearanceManager != null ? optionsAppearanceManager : new AppearanceManager( BigDataViewer.configDir ); + linkSettingsManager = optionsLinkSettingsManager != null ? optionsLinkSettingsManager : new LinkSettingsManager( BigDataViewer.configDir ); + resourceManager = options.values.getResourceManager(); cacheControls = new CacheControls(); @@ -167,6 +178,11 @@ public void componentResized( final ComponentEvent e ) bdvActions.runnableAction( this::expandAndFocusCardPanel, EXPAND_CARDS, EXPAND_CARDS_KEYS ); bdvActions.runnableAction( this::collapseCardPanel, COLLAPSE_CARDS, COLLAPSE_CARDS_KEYS ); + final Actions linkActions = new Actions( inputTriggerConfig, "bdv" ); + linkActions.install( keybindings, "links" ); + final PasteSettings pasteSettings = linkSettingsManager.linkSettings().pasteSettings(); + LinkActions.install( linkActions, viewer, setups, pasteSettings, resourceManager ); + viewer.setDisplayMode( DisplayMode.FUSED ); } @@ -188,6 +204,18 @@ public AppearanceManager getAppearanceManager() return appearanceManager; } + @Override + public LinkSettingsManager getLinkSettingsManager() + { + return linkSettingsManager; + } + + @Override + public ResourceManager getResourceManager() + { + return resourceManager; + } + @Override public InputActionBindings getKeybindings() { diff --git a/src/main/java/bdv/util/BdvOptions.java b/src/main/java/bdv/util/BdvOptions.java index aa27bfb3..41b3c186 100644 --- a/src/main/java/bdv/util/BdvOptions.java +++ b/src/main/java/bdv/util/BdvOptions.java @@ -35,6 +35,7 @@ import bdv.tools.links.ResourceManager; import bdv.ui.appearance.AppearanceManager; import bdv.ui.keymap.KeymapManager; +import bdv.ui.links.LinkSettingsManager; import bdv.viewer.render.AccumulateProjectorARGB; import org.scijava.ui.behaviour.io.InputTriggerConfig; @@ -194,6 +195,15 @@ public BdvOptions appearanceManager( final AppearanceManager appearanceManager ) return this; } + /** + * Set the {@link LinkSettingsManager}. + */ + public BdvOptions linkSettingsManager( final LinkSettingsManager linkSettingsManager ) + { + values.linkSettingsManager = linkSettingsManager; + return this; + } + /** * Set the {@link ResourceManager}. */ @@ -317,6 +327,8 @@ public static class Values private AppearanceManager appearanceManager = null; + private LinkSettingsManager linkSettingsManager = null; + private ResourceManager resourceManager = new DefaultResourceManager(); private final AffineTransform3D sourceTransform = new AffineTransform3D(); @@ -347,6 +359,7 @@ public BdvOptions optionsFromValues() .inputTriggerConfig( inputTriggerConfig ) .keymapManager( keymapManager ) .appearanceManager( appearanceManager ) + .linkSettingsManager( linkSettingsManager ) .resourceManager( resourceManager ) .sourceTransform( sourceTransform ) .frameTitle( frameTitle ) @@ -370,6 +383,7 @@ public ViewerOptions getViewerOptions() .inputTriggerConfig( inputTriggerConfig ) .keymapManager( keymapManager ) .appearanceManager( appearanceManager ) + .linkSettingsManager( linkSettingsManager ) .resourceManager( resourceManager ); if ( hasPreferredSize() ) o.width( width ).height( height ); @@ -416,6 +430,11 @@ public AppearanceManager getAppearanceManager() return appearanceManager; } + public LinkSettingsManager getLinkSettingsManager() + { + return linkSettingsManager; + } + public ResourceManager getResourceManager() { return resourceManager; diff --git a/src/main/java/bdv/viewer/DisplayMode.java b/src/main/java/bdv/viewer/DisplayMode.java index 4861e257..7101de48 100644 --- a/src/main/java/bdv/viewer/DisplayMode.java +++ b/src/main/java/bdv/viewer/DisplayMode.java @@ -38,7 +38,7 @@ public enum DisplayMode private final int id; private final String name; - private DisplayMode( final int id, final String name ) + DisplayMode( final int id, final String name ) { this.id = id; this.name = name; diff --git a/src/main/java/bdv/viewer/ViewerOptions.java b/src/main/java/bdv/viewer/ViewerOptions.java index 33161caa..ed4f9012 100644 --- a/src/main/java/bdv/viewer/ViewerOptions.java +++ b/src/main/java/bdv/viewer/ViewerOptions.java @@ -42,6 +42,7 @@ import bdv.ui.UIUtils; import bdv.ui.appearance.AppearanceManager; import bdv.ui.keymap.KeymapManager; +import bdv.ui.links.LinkSettingsManager; import bdv.viewer.animate.MessageOverlayAnimator; import bdv.viewer.render.AccumulateProjector; import bdv.viewer.render.AccumulateProjectorARGB; @@ -266,6 +267,15 @@ public ViewerOptions resourceManager( final ResourceManager resourceManager ) return this; } + /** + * Set the {@link LinkSettingsManager}. + */ + public ViewerOptions linkSettingsManager( final LinkSettingsManager linkSettingsManager ) + { + values.linkSettingsManager = linkSettingsManager; + return this; + } + /** * Read-only {@link ViewerOptions} values. */ @@ -301,6 +311,8 @@ public static class Values private AppearanceManager appearanceManager = null; + private LinkSettingsManager linkSettingsManager = null; + private ResourceManager resourceManager = new DefaultResourceManager(); public ViewerOptions optionsFromValues() @@ -321,6 +333,7 @@ public ViewerOptions optionsFromValues() shareKeyPressedEvents( keyPressedManager ). keymapManager( keymapManager ). appearanceManager( appearanceManager ). + linkSettingsManager( linkSettingsManager ). resourceManager( resourceManager); } @@ -399,6 +412,11 @@ public AppearanceManager getAppearanceManager() return appearanceManager; } + public LinkSettingsManager getLinkSettingsManager() + { + return linkSettingsManager; + } + public ResourceManager getResourceManager() { return resourceManager; diff --git a/src/main/resources/bdv/ui/keymap/default.yaml b/src/main/resources/bdv/ui/keymap/default.yaml index 475ddedb..918ca1a2 100644 --- a/src/main/resources/bdv/ui/keymap/default.yaml +++ b/src/main/resources/bdv/ui/keymap/default.yaml @@ -387,3 +387,11 @@ action: 2d scroll rotate slow contexts: [bdv] triggers: [not mapped] +- !mapping + action: copy viewer state + contexts: [bdv] + triggers: [ctrl C, meta C] +- !mapping + action: paste viewer state + contexts: [bdv] + triggers: [ctrl V, meta V] \ No newline at end of file From 6729e65653299a63273ba245ebb45ccd9e77cab8 Mon Sep 17 00:00:00 2001 From: tpietzsch Date: Tue, 25 Mar 2025 13:49:09 +0100 Subject: [PATCH 7/8] fix javadoc error --- src/main/java/bdv/tools/links/ResourceManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/bdv/tools/links/ResourceManager.java b/src/main/java/bdv/tools/links/ResourceManager.java index 14459988..150a67d7 100644 --- a/src/main/java/bdv/tools/links/ResourceManager.java +++ b/src/main/java/bdv/tools/links/ResourceManager.java @@ -1,7 +1,7 @@ package bdv.tools.links; /** - * Associates resources and {@code ResourceSpec}s for copy&paste between + * Associates resources and {@code ResourceSpec}s for copy & paste between * BigDataViewer instances. *

* Resources are for example {@code SpimData} objects, {@code From 8a1ee3f123d66ede604fb594ef5e9a7998493aaf Mon Sep 17 00:00:00 2001 From: tpietzsch Date: Tue, 25 Mar 2025 14:45:14 +0100 Subject: [PATCH 8/8] Make JsonAdapters public --- src/main/java/bdv/tools/links/JsonAdapters.java | 8 ++++---- src/main/java/bdv/tools/links/Links.java | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/bdv/tools/links/JsonAdapters.java b/src/main/java/bdv/tools/links/JsonAdapters.java index 56de8ef3..a052da54 100644 --- a/src/main/java/bdv/tools/links/JsonAdapters.java +++ b/src/main/java/bdv/tools/links/JsonAdapters.java @@ -18,7 +18,7 @@ class JsonAdapters { @JsonUtils.JsonIo( jsonType = "BdvPropertiesV0.Anchor", type = BdvPropertiesV0.Anchor.class ) - static class AnchorAdapter implements JsonDeserializer< BdvPropertiesV0.Anchor >, JsonSerializer< BdvPropertiesV0.Anchor > + public static class AnchorAdapter implements JsonDeserializer< BdvPropertiesV0.Anchor >, JsonSerializer< BdvPropertiesV0.Anchor > { @Override public BdvPropertiesV0.Anchor deserialize( @@ -40,7 +40,7 @@ public JsonElement serialize( } @JsonUtils.JsonIo( jsonType = "BdvProperiesV0.SourceConverterConfig", type = BdvPropertiesV0.SourceConverterConfig.class ) - static class SourceConverterConfigAdapter implements JsonDeserializer< BdvPropertiesV0.SourceConverterConfig >, JsonSerializer< BdvPropertiesV0.SourceConverterConfig > + public static class SourceConverterConfigAdapter implements JsonDeserializer< BdvPropertiesV0.SourceConverterConfig >, JsonSerializer< BdvPropertiesV0.SourceConverterConfig > { @Override public BdvPropertiesV0.SourceConverterConfig deserialize( @@ -77,7 +77,7 @@ public JsonElement serialize( } @JsonUtils.JsonIo( jsonType = "DisplayMode", type = DisplayMode.class ) - static class DisplayModeAdapter implements JsonDeserializer< DisplayMode >, JsonSerializer< DisplayMode > + public static class DisplayModeAdapter implements JsonDeserializer< DisplayMode >, JsonSerializer< DisplayMode > { @Override public DisplayMode deserialize( @@ -130,7 +130,7 @@ public JsonElement serialize( } @JsonUtils.JsonIo( jsonType = "Interpolation", type = Interpolation.class ) - static class InterpolationAdapter implements JsonDeserializer< Interpolation >, JsonSerializer< Interpolation > + public static class InterpolationAdapter implements JsonDeserializer< Interpolation >, JsonSerializer< Interpolation > { @Override public Interpolation deserialize( diff --git a/src/main/java/bdv/tools/links/Links.java b/src/main/java/bdv/tools/links/Links.java index 6524c805..7f92eb80 100644 --- a/src/main/java/bdv/tools/links/Links.java +++ b/src/main/java/bdv/tools/links/Links.java @@ -369,7 +369,7 @@ public JsonElement properties() } @JsonUtils.JsonIo( jsonType = "VersionAndProperties", type = VersionAndProperties.class ) - static class Adapter implements JsonDeserializer< VersionAndProperties >, JsonSerializer< VersionAndProperties > + public static class Adapter implements JsonDeserializer< VersionAndProperties >, JsonSerializer< VersionAndProperties > { @Override public VersionAndProperties deserialize(