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 + diff --git a/src/main/java/bdv/BigDataViewer.java b/src/main/java/bdv/BigDataViewer.java index 84cebd57..d1cfd763 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,47 @@ 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; +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.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; 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 +145,10 @@ public class BigDataViewer private final AppearanceManager appearanceManager; + private final LinkSettingsManager linkSettingsManager; + + private final ResourceManager resourceManager; + protected final PreferencesDialog preferencesDialog; protected final ManualTransformationEditor manualTransformationEditor; @@ -257,49 +269,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 +374,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 @@ -353,8 +429,11 @@ 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(); final Keymap keymap = this.keymapManager.getForwardSelectedKeymap(); @@ -442,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()); @@ -454,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() ); } ); @@ -531,7 +619,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 +638,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 +700,16 @@ public AppearanceManager getAppearanceManager() return appearanceManager; } + public LinkSettingsManager getLinkSettingsManager() + { + return linkSettingsManager; + } + + public ResourceManager getResourceManager() + { + return resourceManager; + } + public boolean tryLoadSettings( final String xmlFilename ) { proposedSettingsFile = null; 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 ); + } +} 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/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/JsonAdapters.java b/src/main/java/bdv/tools/links/JsonAdapters.java new file mode 100644 index 00000000..a052da54 --- /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 ) + public 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 ) + public 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 ) + public 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 ) + public 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..7f92eb80 --- /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 ) + 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