+ * 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
+ * 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 )
+ public 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/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..150a67d7
--- /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/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/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();
+ }
+ }
+}
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/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/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 );
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 16a9155a..41b3c186 100644
--- a/src/main/java/bdv/util/BdvOptions.java
+++ b/src/main/java/bdv/util/BdvOptions.java
@@ -31,8 +31,11 @@
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.ui.links.LinkSettingsManager;
import bdv.viewer.render.AccumulateProjectorARGB;
import org.scijava.ui.behaviour.io.InputTriggerConfig;
@@ -192,6 +195,26 @@ 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}.
+ */
+ 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 +327,10 @@ public static class Values
private AppearanceManager appearanceManager = null;
+ private LinkSettingsManager linkSettingsManager = null;
+
+ private ResourceManager resourceManager = new DefaultResourceManager();
+
private final AffineTransform3D sourceTransform = new AffineTransform3D();
private String frameTitle = "BigDataViewer";
@@ -332,6 +359,8 @@ public BdvOptions optionsFromValues()
.inputTriggerConfig( inputTriggerConfig )
.keymapManager( keymapManager )
.appearanceManager( appearanceManager )
+ .linkSettingsManager( linkSettingsManager )
+ .resourceManager( resourceManager )
.sourceTransform( sourceTransform )
.frameTitle( frameTitle )
.axisOrder( axisOrder )
@@ -353,7 +382,9 @@ public ViewerOptions getViewerOptions()
.accumulateProjectorFactory( accumulateProjectorFactory )
.inputTriggerConfig( inputTriggerConfig )
.keymapManager( keymapManager )
- .appearanceManager( appearanceManager );
+ .appearanceManager( appearanceManager )
+ .linkSettingsManager( linkSettingsManager )
+ .resourceManager( resourceManager );
if ( hasPreferredSize() )
o.width( width ).height( height );
return o;
@@ -399,6 +430,16 @@ public AppearanceManager getAppearanceManager()
return appearanceManager;
}
+ public LinkSettingsManager getLinkSettingsManager()
+ {
+ return linkSettingsManager;
+ }
+
+ public ResourceManager getResourceManager()
+ {
+ return resourceManager;
+ }
+
public Bdv addTo()
{
return addTo;
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 7290daa3..ed4f9012 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;
@@ -45,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;
@@ -260,6 +258,24 @@ public ViewerOptions appearanceManager( final AppearanceManager appearanceManage
return this;
}
+ /**
+ * Set the {@link ResourceManager}.
+ */
+ public ViewerOptions resourceManager( final ResourceManager resourceManager )
+ {
+ values.resourceManager = resourceManager;
+ return this;
+ }
+
+ /**
+ * Set the {@link LinkSettingsManager}.
+ */
+ public ViewerOptions linkSettingsManager( final LinkSettingsManager linkSettingsManager )
+ {
+ values.linkSettingsManager = linkSettingsManager;
+ return this;
+ }
+
/**
* Read-only {@link ViewerOptions} values.
*/
@@ -295,6 +311,10 @@ public static class Values
private AppearanceManager appearanceManager = null;
+ private LinkSettingsManager linkSettingsManager = null;
+
+ private ResourceManager resourceManager = new DefaultResourceManager();
+
public ViewerOptions optionsFromValues()
{
return new ViewerOptions().
@@ -312,7 +332,9 @@ public ViewerOptions optionsFromValues()
inputTriggerConfig( inputTriggerConfig ).
shareKeyPressedEvents( keyPressedManager ).
keymapManager( keymapManager ).
- appearanceManager( appearanceManager );
+ appearanceManager( appearanceManager ).
+ linkSettingsManager( linkSettingsManager ).
+ resourceManager( resourceManager);
}
public int getWidth()
@@ -389,5 +411,15 @@ 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