diff --git a/.github/badges/code_issues.svg b/.github/badges/code_issues.svg index 4112a8cb..a1bb67f3 100644 --- a/.github/badges/code_issues.svg +++ b/.github/badges/code_issues.svg @@ -1 +1 @@ -code issuescode issues17961796 \ No newline at end of file +code issuescode issues17851785 \ No newline at end of file diff --git a/code/+nansen/+processing/@MotionCorrectionPreview/MotionCorrectionPreview.m b/code/+nansen/+processing/@MotionCorrectionPreview/MotionCorrectionPreview.m index 81dde168..43f53fc5 100644 --- a/code/+nansen/+processing/@MotionCorrectionPreview/MotionCorrectionPreview.m +++ b/code/+nansen/+processing/@MotionCorrectionPreview/MotionCorrectionPreview.m @@ -7,7 +7,7 @@ % nansen.plugin.imviewer.NoRMCorre as subclasses? properties (Abstract) - settings + Options ImviewerObj end @@ -21,8 +21,8 @@ methods (Access = protected) - function onSettingsChanged(obj, name, value) - %onSettingsChanged Update value in settings if value changes. + function onOptionsChanged(obj, name, value) + %onOptionsChanged Update value in options if value changes. % Deal with specific fields switch name @@ -33,9 +33,9 @@ function onSettingsChanged(obj, name, value) msgbox('This is not implemented yet, constant bidirectional correction will be used') end case 'OutputFormat' - oldFilename = obj.settings.Export.FileName; + oldFilename = obj.Options.Export.FileName; newFilename = obj.buildFilenameWithExtension(oldFilename); - obj.settings.Export.FileName = newFilename; + obj.Options.Export.FileName = newFilename; end defaultFields = fieldnames(obj.DefaultOptions); @@ -43,15 +43,15 @@ function onSettingsChanged(obj, name, value) subFields = fieldnames( obj.DefaultOptions.(defaultFields{i}) ); if any(strcmp(subFields, name)) - obj.settings.(defaultFields{i}).(name) = value; + obj.Options.(defaultFields{i}).(name) = value; end end end - function assertPreviewSettingsValid(obj) + function assertPreviewOptionsValid(obj) % Check if saveResult or showResults is selected - if ~obj.settings.Preview.saveResults && ~obj.settings.Preview.showResults + if ~obj.Options.Preview.saveResults && ~obj.Options.Preview.showResults msg = 'Aborted, because neither "Save Results" nor "Show Results" are selected'; obj.ImviewerObj.displayMessage(msg); return @@ -63,7 +63,7 @@ function assertPreviewSettingsValid(obj) % Strip current filename of all extensions. fileName = strsplit(fileName, '.'); % For file with multiple extentsions, i.e .ome.tif - switch obj.settings.Export.OutputFormat + switch obj.Options.Export.OutputFormat case 'Binary' fileName = sprintf('%s.raw', fileName{1}); case 'Tiff' @@ -75,12 +75,12 @@ function assertPreviewSettingsValid(obj) function dataSet = prepareTargetDataset(obj) - folderPath = obj.settings.Export.SaveDirectory; + folderPath = obj.Options.Export.SaveDirectory; %folderPath = fileparts( obj.ImviewerObj.ImageStack.FileName ); %folderPath = fullfile(folderPath, 'motion_correction_flowreg'); if ~isfolder(folderPath); mkdir(folderPath); end - [~, datasetID] = fileparts(obj.settings.Export.FileName); + [~, datasetID] = fileparts(obj.Options.Export.FileName); dataSet = nansen.dataio.dataset.SingleFolderDataSet(folderPath, ... 'DataSetID', datasetID ); @@ -89,7 +89,7 @@ function assertPreviewSettingsValid(obj) 'Data', obj.ImviewerObj.ImageStack) dataSet.addVariable('TwoPhotonSeries_Corrected', ... - 'FilePath', obj.settings.Export.FileName, ... + 'FilePath', obj.Options.Export.FileName, ... 'Subfolder', 'motion_corrected'); end @@ -111,7 +111,7 @@ function assertPreviewSettingsValid(obj) rootDir = obj.DataIoModel.getTargetFolder(); saveDir = fullfile(rootDir, 'image_registration'); else - rootDir = fileparts( obj.settings.Export.SaveDirectory ); + rootDir = fileparts( obj.Options.Export.SaveDirectory ); saveDir = rootDir; end @@ -126,15 +126,15 @@ function assertPreviewSettingsValid(obj) end function imArray = loadSelectedFrameSet(obj) - %loadSelectedFrameSet Load images for frame interval in settings + %loadSelectedFrameSet Load images for frame interval in options import nansen.wrapper.normcorre.utility.apply_bidirectional_offset imArray = []; - % Get frame interval from settings - firstFrame = obj.settings.Preview.firstFrame; - lastFrame = (firstFrame-1) + obj.settings.Preview.numFrames; + % Get frame interval from options + firstFrame = obj.Options.Preview.firstFrame; + lastFrame = (firstFrame-1) + obj.Options.Preview.numFrames; % Make sure we dont grab more than is available. firstFrame = max([1, firstFrame]); @@ -156,9 +156,9 @@ function assertPreviewSettingsValid(obj) imArray = obj.ImviewerObj.ImageStack.getFrameSet(firstFrame:lastFrame); imArray = squeeze(imArray); - if obj.settings.Preprocessing.NumFlybackLines ~= 0 + if obj.Options.Preprocessing.NumFlybackLines ~= 0 IND = repmat({':'}, 1, ndims(imArray)); - IND{1} = obj.settings.Preprocessing.NumFlybackLines : size(imArray, 1); + IND{1} = obj.Options.Preprocessing.NumFlybackLines : size(imArray, 1); imArray = imArray(IND{:}); end @@ -166,7 +166,7 @@ function assertPreviewSettingsValid(obj) % % % % end - if ~strcmp( obj.settings.Preprocessing.BidirectionalCorrection, 'None') + if ~strcmp( obj.Options.Preprocessing.BidirectionalCorrection, 'None') if ndims(imArray) == 4 imArrayMean = squeeze( mean(imArray, 3) ); colShift = correct_bidirectional_offset(imArrayMean, size(imArray,4), 10); diff --git a/code/+nansen/+processing/DataMethod.m b/code/+nansen/+processing/DataMethod.m index ad1d3066..15c726b0 100644 --- a/code/+nansen/+processing/DataMethod.m +++ b/code/+nansen/+processing/DataMethod.m @@ -133,8 +133,8 @@ % Abort if h is invalid (improper exit) if ~isvalid(hPreviewApp); wasSuccess = false; return; end - obj.Parameters = hPreviewApp.settings; - obj.Options = hPreviewApp.settings; + obj.Parameters = hPreviewApp.Options; + obj.Options = hPreviewApp.Options; wasSuccess = ~hPreviewApp.wasAborted; delete(hPreviewApp) diff --git a/code/apps/+imviewer/+plugin/@RoiClassifier/RoiClassifier.m b/code/apps/+imviewer/+plugin/@RoiClassifier/RoiClassifier.m index d49bbe24..9ba06fd0 100644 --- a/code/apps/+imviewer/+plugin/@RoiClassifier/RoiClassifier.m +++ b/code/apps/+imviewer/+plugin/@RoiClassifier/RoiClassifier.m @@ -1,32 +1,23 @@ -classdef RoiClassifier < applify.mixin.AppPlugin - - properties (Constant, Hidden = true) % Inherited from applify.mixin.UserSettings via AppPlugin - USE_DEFAULT_SETTINGS = false - DEFAULT_SETTINGS = imviewer.plugin.RoiClassifier.getDefaultSettings() % Todo... This is classifier settings.I guess these should be settings relevant for connecting the two apps... - end - +classdef RoiClassifier < applify.mixin.AppBridgePlugin + properties (Constant) % Inherited from uim.applify.AppPlugin Name = 'Roiclassifier' end - - properties - PrimaryAppName = 'Imviewer'; - end - + properties ClassifierApp end - - methods (Static) - S = getDefaultSettings() + + properties (Access = private) + ClassifierDestroyedListener event.listener end - + methods function obj = RoiClassifier(imviewerApp) %openRoiClassifier Open roiClassifier on request from imviewer - obj@applify.mixin.AppPlugin(imviewerApp) + obj@applify.mixin.AppBridgePlugin(imviewerApp) % Find roimanager handle success=false; @@ -92,6 +83,8 @@ % Initialize roi classifier hClsf = roiclassifier.App(roiGroup, 'tileUnits', 'scaled'); obj.ClassifierApp = hClsf; + obj.ClassifierDestroyedListener = addlistener(hClsf, ... + 'ObjectBeingDestroyed', @(s,e) obj.onClassifierAppDestroyed()); success = true; end @@ -99,36 +92,38 @@ if ~success imviewerApp.displayMessage('Error: No rois are present') + delete(obj) end end function delete(obj) - + if ~isempty(obj.ClassifierDestroyedListener) && isvalid(obj.ClassifierDestroyedListener) + delete(obj.ClassifierDestroyedListener) + end + if ~isempty(obj.ClassifierApp) && isvalid(obj.ClassifierApp) + delete(obj.ClassifierApp) + end end end methods function setFilePath(obj, filePath) - obj.ClassifierApp.dataFilePath = filePath; - end - end - - methods (Access = protected) - - function onSettingsChanged(obj, name, value) - - end - - function onPluginActivated(obj) - % fprintf('roiclassifier plugin activated...') - + if ~isempty(obj.ClassifierApp) && isvalid(obj.ClassifierApp) + obj.ClassifierApp.dataFilePath = filePath; + end end end - - methods (Static) - function icon = getPluginIcon() - + + methods (Access = private) + + function onClassifierAppDestroyed(obj) + obj.ClassifierDestroyedListener = event.listener.empty; + obj.ClassifierApp = []; + if isvalid(obj) + delete(obj) + end end + end end diff --git a/code/apps/+imviewer/+plugin/@RoiClassifier/getDefaultSettings.m b/code/apps/+imviewer/+plugin/@RoiClassifier/getDefaultSettings.m deleted file mode 100644 index b7c3a35a..00000000 --- a/code/apps/+imviewer/+plugin/@RoiClassifier/getDefaultSettings.m +++ /dev/null @@ -1,13 +0,0 @@ -function S = getDefaultSettings() - - S = struct(... - 'GridSize_', {{'3x5', '4x6', '5x8', '6x10', '7x12', '8x12', '9x14', '10x16'}}, ... - 'GridSize', '8x12', ... - 'ImageSize_', {{'30x30', '50x50', '75x75', '100x100', '128x128', '256x256'}}, ... - 'ImageSize', '100x100', ... - 'TileAlpha_', struct('type', 'slider', 'args', {{'Min', 0, 'Max', 1}}), ... - 'TileAlpha', 0.5, ... - 'DeleteRejectedTilesOnRefresh', false, ... - 'IgnoreClassifiedTileOnShiftClick', true); - -end diff --git a/code/apps/+imviewer/+plugin/@RoiManager/RoiManager.m b/code/apps/+imviewer/+plugin/@RoiManager/RoiManager.m index 0be5528e..ed4b2020 100755 --- a/code/apps/+imviewer/+plugin/@RoiManager/RoiManager.m +++ b/code/apps/+imviewer/+plugin/@RoiManager/RoiManager.m @@ -1,4 +1,4 @@ -classdef RoiManager < imviewer.ImviewerPlugin & roimanager.RoiGroupFileIoAppMixin +classdef RoiManager < imviewer.ImviewerPlugin & applify.mixin.UserSettings & roimanager.RoiGroupFileIoAppMixin %imviewer.plugin.RoiManager Open the roimanager tool as a plugin in imviewer % Todo: @@ -131,6 +131,7 @@ function obj = RoiManager(varargin) obj@imviewer.ImviewerPlugin(varargin{:}) + obj@applify.mixin.UserSettings() % % [nvPairs, varargin] = utility.getnvpairs(varargin); % % @@ -673,8 +674,10 @@ function runAutoSegmentation(obj) function openManualRoiClassifier(obj) % todo.... - hClassifier = imviewer.plugin.RoiClassifier(obj.ImviewerObj); - hClassifier.setFilePath(obj.roiFilePath); + hClassifier = obj.ImviewerObj.openPlugin('RoiClassifier'); + if ~isempty(hClassifier) && isvalid(hClassifier) + hClassifier.setFilePath(obj.roiFilePath); + end end % /function openManualRoiClassifier @@ -1231,8 +1234,7 @@ function initializePointerTools(obj) hAxes = obj.PrimaryApp.Axes; hMap = obj.roiDisplay; - isMatch = contains({hViewer.plugins.pluginName}, 'pointerManager'); - obj.PointerManager = hViewer.plugins(isMatch).pluginHandle; + obj.PointerManager = hViewer.PointerManager; pointerNames = {'selectObject', 'polyDraw', 'circleSelect', 'autoDetect', 'freehandDraw'}; @@ -1497,10 +1499,6 @@ function assertValidChannelIndex(obj, value, message) S = getDefaultSettings() - function icon = getPluginIcon() - - end - function pathStr = getIconPath() % Get system dependent absolute path for icons. pathStr = roimanager.localpath('toolbar_icons'); diff --git a/code/apps/+imviewer/@App/App.m b/code/apps/+imviewer/@App/App.m index c990d1e1..9a629e87 100755 --- a/code/apps/+imviewer/@App/App.m +++ b/code/apps/+imviewer/@App/App.m @@ -122,7 +122,10 @@ LinkedApps % Todo: Make superclass for timeseries viewer. Property: currentSampleNum & Methods for syncing samplenumber... uiwidgets = struct() % Todo: Inherit from modular app? - plugins = struct('pluginName', {}, 'pluginHandle', {}); % Todo: migrate to AppWithPlugin superclass +end + +properties (Access = {?imviewer.App, ?imviewer.ImviewerPlugin}) % App-owned interaction interface + PointerManager % uim.interface.pointerManager — owns zoom/pan pointer tools end properties (Access = private) % Private components (todo: clean up) @@ -502,10 +505,7 @@ function initializeViewer(obj) %obj.displayMessage('Initializing...') % Initialize the pointer interface. - pif = uim.interface.pointerManager(obj.Figure, obj.uiaxes.imdisplay, {'zoomIn', 'zoomOut', 'pan'}); - pif.pointers.pan.buttonMotionCallback = @obj.moveImage; - obj.plugins(end+1).pluginName = 'pointerManager'; - obj.plugins(end).pluginHandle = pif; + obj.initialisePointerManager(); % A bit random to do this here, but for now it only influences the % pointerManager zoom tools @@ -1442,19 +1442,15 @@ function addImageToolbar(obj) hToolbar.addButton('Icon', obj.ICONS.zoomOut, 'Mode', 'togglebutton', 'Tag', 'zoomOut', 'Tooltip', 'Zoom Out (w)', buttonProps{:}) hToolbar.addButton('Icon', obj.ICONS.hand4, 'Mode', 'togglebutton', 'Tag', 'pan', 'Tooltip', 'Pan (y)', buttonProps{:}) - % Get handle for pointerManager interface - isMatch = contains({obj.plugins.pluginName}, 'pointerManager'); - pifHandle = obj.plugins(isMatch).pluginHandle; - % Add listeners for toggling of modes from the pointertools to the % buttons. Also connect to buttonDown to toggling of the pointer % tools. pointerModes = {'zoomIn', 'zoomOut', 'pan'}; - + for i = 1:numel(pointerModes) hBtn = hToolbar.getHandle(pointerModes{i}); - hBtn.Callback = @(s,e,h,str) togglePointerMode(pifHandle, pointerModes{i}); - hBtn.addToggleListener(pifHandle.pointers.(pointerModes{i}), 'ToggledPointerTool') + hBtn.Callback = @(s,e,h,str) togglePointerMode(obj.PointerManager, pointerModes{i}); + hBtn.addToggleListener(obj.PointerManager.pointers.(pointerModes{i}), 'ToggledPointerTool') end % Add toolbar to the widgets property. @@ -1488,7 +1484,7 @@ function addTaskbar(obj, showMessage) hBtn = hTaskbar.addButton('Type', 'pushbutton', ... 'Tag', 'manualclassifier', 'Tooltip', 'Manual Classifier', ... 'Icon', obj.ICONS.manualClassifier, 'UseDefaultIcon', false); - hBtn.ButtonDownFcn = @(s, e, h) imviewer.plugin.RoiClassifier(obj); + hBtn.ButtonDownFcn = @(s, e, h) obj.openPlugin('RoiClassifier'); % Add button for the RoiManager plugin hBtn = hTaskbar.addButton('Type', 'pushbutton', ... @@ -1774,34 +1770,29 @@ function reparent(obj, newParent, mode) obj.Axes.UIContextMenu.Parent = hFig; % TEMPORARY. Todo: should make separate method for this! - % Get handle for pointerManager interface - isMatch = contains({obj.plugins.pluginName}, 'pointerManager'); - pifHandle = obj.plugins(isMatch).pluginHandle; - delete(pifHandle) - + delete(obj.PointerManager) + obj.Figure.CloseRequestFcn = []; delete(obj.Figure) - + obj.Figure = hFig; - + obj.uiaxes.imdisplay.UIContextMenu = uicontextmenu(obj.Figure); obj.createImageMenu(obj.uiaxes.imdisplay.UIContextMenu); - + obj.uiaxes.imdisplay.ButtonDownFcn = []; - pifHandle = uim.interface.pointerManager(obj.Figure, obj.uiaxes.imdisplay, {'zoomIn', 'zoomOut', 'pan'}); - pifHandle.pointers.pan.buttonMotionCallback = @obj.moveImage; - obj.plugins(isMatch).pluginHandle = pifHandle; + obj.initialisePointerManager(); + % Add listeners for toggling of modes from the pointertools to the % buttons. Also connect to buttonDown to toggling of the pointer % tools. - hToolbar = obj.uiwidgets.Toolbar; pointerModes = {'zoomIn', 'zoomOut', 'pan'}; - + for i = 1:numel(pointerModes) hBtn = hToolbar.getHandle(pointerModes{i}); - hBtn.Callback = @(s,e,h,str) togglePointerMode(pifHandle, pointerModes{i}); - hBtn.addToggleListener(pifHandle.pointers.(pointerModes{i}), 'ToggledPointerTool') + hBtn.Callback = @(s,e,h,str) togglePointerMode(obj.PointerManager, pointerModes{i}); + hBtn.addToggleListener(obj.PointerManager.pointers.(pointerModes{i}), 'ToggledPointerTool') end % From updateImageDisplay @@ -4363,7 +4354,27 @@ function showHelp(obj, ~, ~) % % Todo: Remove/resolve this function editSettings(obj, ~, ~) - obj.editSettings@applify.mixin.UserSettings() + % Collect settings from the app and any active plugins that have settings. + settingsProviders = [{obj}, num2cell(obj.Plugins)]; + hasSettings = cellfun(@(h) isprop(h, 'settings'), settingsProviders); + settingsProviders = settingsProviders(hasSettings); + + if numel(settingsProviders) > 1 + titleStr = sprintf('Preferences for %s', class(obj)); + settingsStructs = cellfun(@(h) h.settings, settingsProviders, 'uni', 0); + names = cellfun(@(h) class(h), settingsProviders, 'uni', 0); + callbacks = cellfun(@(h) @(name, value) h.changeSettings(name, value), settingsProviders, 'uni', 0); + + settingsStructs = tools.editStruct(settingsStructs, nan, titleStr, ... + 'Callback', callbacks, 'Name', names); + + for i = 1:numel(settingsProviders) + settingsProviders{i}.settings = settingsStructs{i}; + settingsProviders{i}.saveSettings() + end + else + obj.editSettings@applify.mixin.UserSettings() + end % Why? Is this if the cancel button is hit? % obj.uiaxes.imdisplay.CLim = obj.settings.ImageDisplay.imageBrightnessLimits; @@ -4722,8 +4733,19 @@ function stopDraw(obj) end end +methods (Access = private) % PointerManager lifecycle + + function initialisePointerManager(obj) + % initialisePointerManager - Create the pointer manager and assign it to obj.PointerManager + pointerManager = uim.interface.pointerManager(obj.Figure, obj.uiaxes.imdisplay, {'zoomIn', 'zoomOut', 'pan'}); + pointerManager.pointers.pan.buttonMotionCallback = @obj.moveImage; + obj.PointerManager = pointerManager; + end + +end + methods (Access = private) % Housekeeping and internal - + function turnOffModernAxesToolbar(obj, hAxes) % Disable newer matlab axes interactivity... @@ -4963,10 +4985,8 @@ function onSliderChanged(obj, ~, evtData, channelNumber) end function onMouseEnteredImage(obj, ~, ~) - isMatch = contains({obj.plugins.pluginName}, 'pointerManager'); - if any(isMatch) - pifHandle = obj.plugins(isMatch).pluginHandle; - pifHandle.updatePointerSymbol() + if ~isempty(obj.PointerManager) + obj.PointerManager.updatePointerSymbol() end end @@ -5099,7 +5119,7 @@ function onMouseMotion(obj, ~, ~) obj.isDrag = true; % Abort if mousemode is set - if ~isempty(obj.plugins(1).pluginHandle.currentPointerTool) + if ~isempty(obj.PointerManager) && ~isempty(obj.PointerManager.currentPointerTool) return end @@ -5220,23 +5240,16 @@ function onKeyPressed(obj, ~, event, forceKey) if ~obj.isMouseInApp && ~forceKey; return; end - % Todo: This only holds the pointermanager. Should make it into an - % interface.., not a plugin. - if ~isempty(obj.plugins) - for i = 1:numel(obj.plugins) - try - wasCaptured = obj.plugins(i).pluginHandle.onKeyPress([], event); - if wasCaptured; return; end - catch ME - fprintf( [ME.message, '\n'] ) - % something went wrong, but that's fine? - end + % Route key press to the pointer manager first. + if ~isempty(obj.PointerManager) + try + wasCaptured = obj.PointerManager.onKeyPress([], event); + if wasCaptured; return; end + catch ME + fprintf( [ME.message, '\n'] ) end end - - % Note: This should eventually take over for the if block above, - % pointermanager is currently saved in the 'plugins' property, and - % this method works on 'Plugins' property. Todo: consolidate + wasCaptured = obj.sendKeyEventToPlugins([], event); if wasCaptured; return; end @@ -5509,22 +5522,19 @@ function onKeyPressed(obj, ~, event, forceKey) function onKeyReleased(obj, ~, event) % onKeyReleased - Callback handler for key release events - % Check if any plugins are active, and invoke their keyRelease - % callback handler. If no plugins are active, or no plugins - % captures the key, let this app handle the key event. - - if ~isempty(obj.plugins) - for i = 1:numel(obj.plugins) - try - wasCaptured = obj.plugins(i).pluginHandle.onKeyRelease([], event); - if wasCaptured; return; end - catch ME - fprintf( [ME.message, '\n'] ) - % something went wrong, but that's fine? - end + % Route key release to the pointer manager first. + if ~isempty(obj.PointerManager) + try + wasCaptured = obj.PointerManager.onKeyRelease([], event); + if wasCaptured; return; end + catch ME + fprintf( [ME.message, '\n'] ) end end + wasCaptured = obj.sendKeyReleaseEventToPlugins([], event); + if wasCaptured; return; end + switch event.Key case 'shift' obj.Figure.SelectionType = 'normal'; @@ -5851,20 +5861,13 @@ function onImageStackSet(obj) end function configureSpatialDownsampling(obj) - + if isempty(obj.PointerManager); return; end if isa(obj.ImageStack, 'nansen.stack.HighResolutionImage') - - ind = find(contains( {obj.plugins.pluginName}, 'pointerManager')); - pif = obj.plugins(ind).pluginHandle; - - pif.pointers.zoomIn.zoomFinishedCallback = @(s,e) obj.onDisplayLimitsChanged; - pif.pointers.zoomOut.zoomFinishedCallback = @(s,e) obj.onDisplayLimitsChanged; - + obj.PointerManager.pointers.zoomIn.zoomFinishedCallback = @(s,e) obj.onDisplayLimitsChanged; + obj.PointerManager.pointers.zoomOut.zoomFinishedCallback = @(s,e) obj.onDisplayLimitsChanged; else - ind = find(contains( {obj.plugins.pluginName}, 'pointerManager')); - pif = obj.plugins(ind).pluginHandle; - pif.pointers.zoomIn.zoomFinishedCallback = []; - pif.pointers.zoomOut.zoomFinishedCallback = []; + obj.PointerManager.pointers.zoomIn.zoomFinishedCallback = []; + obj.PointerManager.pointers.zoomOut.zoomFinishedCallback = []; end end diff --git a/code/apps/+imviewer/@ImviewerPlugin/ImviewerPlugin.m b/code/apps/+imviewer/@ImviewerPlugin/ImviewerPlugin.m index 62b286bc..ae8eaf14 100644 --- a/code/apps/+imviewer/@ImviewerPlugin/ImviewerPlugin.m +++ b/code/apps/+imviewer/@ImviewerPlugin/ImviewerPlugin.m @@ -1,12 +1,8 @@ classdef ImviewerPlugin < applify.mixin.AppPlugin -%imviewer.AppPlugin Superclass for plugins to the imviewer app +%imviewer.ImviewerPlugin App-scoped plugin base for imviewer % Abstract class providing properties and methods that gives plugin % functionality for imviewer. - - properties - PrimaryAppName = 'imviewer' % Name of primary app - end properties (Dependent) ImviewerObj % Alias for PrimaryApp % Rename to ImviewerApp @@ -14,11 +10,9 @@ properties (GetAccess = protected, SetAccess = private) Axes % Axes for plotting into - %PointerManager end properties (Dependent, SetAccess = private) - NumFrames NumChannels NumPlanes end @@ -32,23 +26,10 @@ methods % Constructor function obj = ImviewerPlugin(varargin) - % Make sure the given handle is an instance of imviewer.App [h, varargin] = imviewer.ImviewerPlugin.checkForImviewerInArgList(varargin); obj@applify.mixin.AppPlugin(h, varargin{:}) - - if isempty(h); return; end - - % Assign property values. - obj.PrimaryApp = h; - obj.Axes = h.Axes; - - obj.onImviewerSet() - - obj.assignDataIoModel() % todo: superclass? Should belong to a - % data method class, not a plugin. - % So a plugin that runs a method should inherit the imviewer - % plugin and the datamethod... + % onImviewerSet is called via onPluginActivated during activatePlugin above. end end @@ -65,14 +46,6 @@ methods - function assignDataIoModel(obj) - return % Under construction. Todo: Move to another class - if isempty(obj.DataIoModel) - folderPath = fileparts( obj.ImviewerObj.ImageStack.FileName ); - obj.DataIoModel = nansen.dataio.DataIoModel(folderPath); - end - end - function imviewerObj = get.ImviewerObj(obj) imviewerObj = obj.PrimaryApp; end diff --git a/code/graphics/+applify/+mixin/AppBridgePlugin.m b/code/graphics/+applify/+mixin/AppBridgePlugin.m new file mode 100644 index 00000000..eb5bbd20 --- /dev/null +++ b/code/graphics/+applify/+mixin/AppBridgePlugin.m @@ -0,0 +1,7 @@ +classdef AppBridgePlugin < applify.mixin.AppPlugin +%AppBridgePlugin Base for plugins that bridge to another app/controller +% +% Bridge plugins coordinate another app or controller, but do not own a +% method-options workflow and do not need any extra base behavior beyond +% the generic AppPlugin contract. +end diff --git a/code/graphics/+applify/+mixin/AppPlugin.m b/code/graphics/+applify/+mixin/AppPlugin.m index 604f2dd1..38c20409 100644 --- a/code/graphics/+applify/+mixin/AppPlugin.m +++ b/code/graphics/+applify/+mixin/AppPlugin.m @@ -1,4 +1,4 @@ -classdef AppPlugin < applify.mixin.UserSettings & matlab.mixin.Heterogeneous & uiw.mixin.AssignPVPairs +classdef AppPlugin < matlab.mixin.Heterogeneous & uiw.mixin.AssignPVPairs %AppPlugin Abstract superclass for an app plugin % % Syntax: @@ -18,313 +18,163 @@ % property, value pairs to be set on construction % - % Not quite sure yet what to add here. + % Plugins can: + % - implement mouse/keyboard callbacks invoked by the host app + % - add items to the app menu + % - hold method options via OptionsManager (algorithm parameters) % - % Provide properties and methods for other classes to act as plugins to - % apps. - % - % On construction of the plugin, it is added to the apps plugin - % list. If a plugin of the same type is already in the list, the - % handle of that is returned instead of creating a new one... - % - % Plugins can implement mouse/keyboard callbacks that are called - % whenever the apps corresponding callback is invoked - % - % The plugin gets access to some of the parent class properties - % and methods. - % - % The plugin can add items to the apps menu. - % - % App takes plugin's settings into account. - - % Todo: - % [ ] Inherit from nansen.mixin.HasOptions instead of - % applify.mixin.UserSettings? - % The original idea was that a plugin has some additional - % settings that should be combined with the apps own settings - % when the plugin is active. However, all the plugins that are - % implemented is a method/algorithm with parameters, and these - % are managed using the Options/OptionsManager instead... - + % matlab.mixin.Heterogeneous allows AppWithPlugin.Plugins to hold an + % array of mixed AppPlugin subclass instances. + properties (Abstract, Constant) Name end - - properties (Abstract) - PrimaryAppName % What is this used for exactly? Maybe remove? - end - + properties (SetAccess = protected) PartialConstruction = false % Do a partial construction, i.e skip the opening of options editor on construction end - + properties - RunMethodOnFinish = true % Should we run method when settings/options are "saved"? - DestroyOnFinish = true % Destroy plugin when control panel is closed - Modal = true % Is figure modal or not - DataIoModel % Store a data i/o model object if it is provided. - OptionsManager % Store optionsmanager handle if plugin is provided with an optionsmanager on construction + DataIoModel % Store a data i/o model object if it is provided. + OptionsManager % Store optionsmanager handle if plugin is provided with an optionsmanager on construction end - + properties - PrimaryApp % App which is primary "owner" of the plugin. find better propname? + PrimaryApp % App which is primary "owner" of the plugin. MenuItem struct % Struct for storing menu handles - Icon end - + properties (Access = protected) IsActivated = false; end - - methods (Abstract, Static) % Should it be a property or part of settings? - %getPluginIcon() - end - -% % methods (Abstract, Access = protected) % todo: Is this abstract?? -% % onPluginActivated % Todo: find better name.. -% % end methods % Constructor - + function obj = AppPlugin(hApp, varargin) - + [options, varargin] = applify.mixin.AppPlugin.optionsCheck(varargin); - + if nargin > 2 obj.parseVarargin(varargin{1:end}) end - + if ~nargin || isempty(hApp); return; end - + obj.validateAppHandle(hApp) - - % Check if plugin is already open/active + + % If this plugin is already active, return the existing instance + % rather than creating a duplicate. if hApp.isPluginActive(obj) + warning('applify:AppPlugin:alreadyActive', ... + 'Plugin "%s" is already active. Returning existing instance.', obj.Name) obj = hApp.getPluginHandle(obj.Name); + return end - + % Assign options from input if provided if nargin >= 2 && ~isempty(options) obj.assignOptions(options) else obj.assignDefaultOptions() end - - if ~hApp.isPluginActive(obj) - obj.activatePlugin(hApp) - end - + + obj.activatePlugin(hApp); + if ~nargout; clear obj; end end - - function delete(obj) + function delete(obj) % Delete menu items if ~isempty(obj.MenuItem) structfun(@delete, obj.MenuItem) end end end - + methods (Access = public) - - function openControlPanel(obj) - obj.editSettings() - end - - function run(obj) + + function run(obj) %#ok % Subclasses may override end end - + % Methods for mouse and keyboard interactive callbacks - methods (Access = {?applify.mixin.AppPlugin, ?applify.AppWithPlugin} ) - - function tf = keyPressHandler(obj, src, evt) % Subclass can override - % todo: rename to onKeyPressed - tf = false; % Key press event was not captured by plugin + methods (Access = {?applify.mixin.AppPlugin, ?applify.AppWithPlugin}) + function tf = keyPressHandler(obj, src, evt) %#ok Subclass can override + tf = false; end - - function tf = keyReleasedHandler(obj, src, evt) % Subclass can override - tf = false; % Key released event was not captured by plugin + + function tf = keyReleasedHandler(obj, src, evt) %#ok Subclass can override + tf = false; end - %tf = mousePressHandler(src, evt) % Subclass can override - end - - methods % Access? - + + methods + function activatePlugin(obj, appHandle) obj.PrimaryApp = appHandle; obj.PrimaryApp.addPlugin(obj) - obj.onPluginActivated() obj.IsActivated = true; end - - function sEditor = openSettingsEditor(obj) - %openSettingsEditor Open ui dialog for editing plugin options. - - titleStr = sprintf('Options Editor (%s)', obj.Name); - - if ~isempty(obj.OptionsManager) - sEditor = obj.OptionsManager.openOptionsEditor(); - sEditor.Title = titleStr; - sEditor.Callback = @obj.onSettingsChanged; - else - sEditor = structeditor(obj.settings, 'Title', titleStr, ... - 'Callback', @obj.onSettingsChanged ); - end - - obj.relocatePrimaryApp(sEditor) % To prevent figures covering each other - obj.hSettingsEditor = sEditor; - - addlistener(obj, 'ObjectBeingDestroyed', @(s,e)delete(sEditor)); - - end - - function editSettings(obj) - %editSettings Open and wait for user to edit settings. - - sEditor = obj.openSettingsEditor(); - - if obj.Modal - sEditor.waitfor() - obj.onSettingsEditorResumed() - else - % Todo: implement above in callback. - addlistener(sEditor, 'AppDestroyed', ... - @(s, e) obj.onSettingsEditorResumed); - end - end - - function place(obj, varargin) - obj.hSettingsEditor.place(varargin{:}) - end end - + methods (Access = protected) - + function onPluginActivated(obj) %onPluginActivated Run subroutines when plugin is activated. obj.createSubMenu() end - + function parseVarargin(obj, varargin) %parseVarargin Parser for varargin that are passed on construction - - % Look for flag of whether to open plugin's options panel on - % construction + % Look for flag of whether to open plugin's options panel on construction if ischar(varargin{1}) && isequal(varargin{1}, '-p') obj.PartialConstruction = true; varargin(1) = []; end - obj.assignPVPairs(varargin{:}) end - + function assignOptions(obj, options) %assignOptions Assign non default options for plugin % - % obj.assignOptions(options) assigns non-default settings to - % this object. options can be a struct or an OptionsManager - % object - + % options can be a struct or an OptionsManager object if isa(options, 'struct') - obj.settings = options; + obj.assignOptionsStruct(options) elseif isa(options, 'nansen.manage.OptionsManager') obj.OptionsManager = options; - obj.settings = obj.OptionsManager.Options; + obj.assignOptionsStruct(obj.OptionsManager.Options) end end - - function assignDefaultOptions(obj) - %assignDefaultOptions Assign default options - % Subclasses may override. - end - - function createSubMenu(obj) - % Subclasses may override - end - - function onSettingsEditorResumed(obj) - %onSettingsEditorResumed Callback for when settings editor is resumed - - % Todo: What if obj is invalid - - % Abort if sEditor is invalid (improper exit) - if ~isvalid(obj.hSettingsEditor) - obj.hSettingsEditor = []; - return; - end - if ~obj.hSettingsEditor.wasCanceled - obj.settings_ = obj.hSettingsEditor.dataEdit; - end - - obj.wasAborted = obj.hSettingsEditor.wasCanceled; - delete(obj.hSettingsEditor) - obj.hSettingsEditor = []; - obj.onSettingsEditorClosed() - - if ~obj.wasAborted && obj.RunMethodOnFinish - obj.run(); - end - - if obj.DestroyOnFinish - delete(obj) - end + function assignDefaultOptions(obj) %#ok + %assignDefaultOptions Assign default options. Subclasses may override. end - - function onSettingsEditorClosed(obj) + + function createSubMenu(obj) %#ok % Subclasses may override end - - function relocatePrimaryApp(obj, hPlugin, direction) - %relocatePrimaryApp Relocate windows to prevent coverup. - - % TODO: What if plugin figure is in same window.. i.e roimanager - % TODO: Improve so that windows are not moved off-screen - - if nargin < 3 - direction = 'horizontal'; - end - - figPos(1,:) = getpixelposition(hPlugin.Figure); - figPos(2,:) = getpixelposition(obj.PrimaryApp.Figure); - hFig = [hPlugin.Figure, obj.PrimaryApp.Figure]; - - switch direction - case 'horizontal' - figPos_ = figPos(:, [1,3]); dim = 1; - case 'vertical' - figPos_ = figPos(:, [2,4]); dim = 2; - end - - screenPos = uim.utility.getCurrentScreenSize( obj.PrimaryApp.Figure ); - - [~, idx] = sort(figPos_(:, 1)); - figPos = figPos(idx, :); - hFig = hFig(idx); - - [x, ~] = uim.utility.layout.subdividePosition(min(figPos(:,1)), ... - screenPos(dim+2), figPos_(:,2), 20); - - for i = 1:2 - hFig(i).Position(dim) = x(i); + + function assignOptionsStruct(obj, options) + if isprop(obj, 'Options') + obj.Options = options; + else + error('AppPlugin:MissingOptionsTarget', ... + 'Plugin "%s" does not expose Options storage.', obj.Name) end end + end - + methods (Access = private) - + function validateAppHandle(obj, hApp) %validateAppHandle Check validity of app handle - if ~isa(hApp, 'applify.AppWithPlugin') error('Can not add plugin "%s" to app of type %s', ... obj.Name, class(hApp)) @@ -333,15 +183,12 @@ function validateAppHandle(obj, hApp) end methods (Static) - + function [opts, cellOfArgs] = optionsCheck(cellOfArgs) - opts = []; - if numel(cellOfArgs) >= 1 containsOpts = isa(cellOfArgs{1}, 'struct') || ... isa(cellOfArgs{1}, 'nansen.manage.OptionsManager'); - if containsOpts opts = cellOfArgs{1}; cellOfArgs(1) = []; end diff --git a/code/graphics/+applify/+mixin/HasOptionsManager.m b/code/graphics/+applify/+mixin/HasOptionsManager.m new file mode 100644 index 00000000..daf408c3 --- /dev/null +++ b/code/graphics/+applify/+mixin/HasOptionsManager.m @@ -0,0 +1,83 @@ +classdef HasOptionsManager < handle +%HasOptionsManager Mixin that gives a class an instance-level OptionsManager +% +% Provides a clean separation between user settings (persistent +% preferences, saved to file via UserSettings) and method options +% (algorithm parameters, managed via OptionsManager). +% +% Intended for use by AppPlugin subclasses that represent an algorithm +% with parameters. Mix this in alongside AppPlugin to get a dedicated +% options path that does not go through obj.settings. +% +% USAGE +% classdef MyPlugin < applify.mixin.AppPlugin & applify.mixin.HasOptionsManager +% +% % In assignDefaultOptions: +% function assignDefaultOptions(obj) +% obj.OptionsManager = nansen.manage.OptionsManager('myFunction'); +% end +% +% % Elsewhere: read options +% opts = obj.Options; +% +% % Open editor and wait +% [opts, wasAborted] = obj.editOptions(); + + properties + OptionsManager nansen.manage.OptionsManager + end + + properties (Dependent) + Options + end + + properties (Access = private) + Options_ + end + + methods + + function set.Options(obj, options) + obj.Options_ = options; + end + + function options = get.Options(obj) + if ~isempty(obj.Options_) + options = obj.Options_; + elseif ~isempty(obj.OptionsManager) + options = obj.OptionsManager.Options; + else + options = struct.empty; + end + end + + end + + methods (Access = public) + + function [options, wasAborted] = editOptions(obj) + %editOptions Open the options editor and wait for user input + [~, options, wasAborted] = obj.OptionsManager.editOptions(); + if ~wasAborted + obj.Options_ = options; + obj.onOptionsChanged(); + end + if ~nargout + clear options wasAborted + elseif nargout == 1 + clear wasAborted + end + end + + end + + methods (Access = protected) + + function onOptionsChanged(obj) %#ok + %onOptionsChanged Called after options are changed via editOptions + % Subclasses may override to react to option changes immediately. + end + + end + +end diff --git a/code/graphics/+applify/+mixin/ModalMethodPreviewController.m b/code/graphics/+applify/+mixin/ModalMethodPreviewController.m new file mode 100644 index 00000000..6c9dcc6a --- /dev/null +++ b/code/graphics/+applify/+mixin/ModalMethodPreviewController.m @@ -0,0 +1,172 @@ +classdef ModalMethodPreviewController < handle +%ModalMethodPreviewController Behavioral mixin for modal edit/preview/run workflows +% +% Provides the lifecycle for plugins that: +% 1. Open a modal options editor (edit) +% 2. Show a live preview while the editor is open (preview) +% 3. Run the method when the editor is confirmed (run) +% 4. Destroy themselves when finished (destroy) +% +% Subclasses should override: +% - assignDefaultOptions - set the default method options +% - openControlPanel — set up preview state, then call editOptions +% - run — execute the method +% - onOptionsChanged - react to live option changes in the editor +% - onOptionsEditorClosed — clean up preview state when editor closes +% +% This class is a behavioral mixin, not the primary plugin base class. +% Concrete plugins should combine it with a single AppPlugin-derived +% superclass such as imviewer.ImviewerPlugin. + + properties + RunMethodOnFinish (1,1) logical = true % Run method when editor is confirmed + DestroyOnFinish (1,1) logical = true % Destroy plugin after run + Modal (1,1) logical = true % Block execution while editor is open + end + + properties (Dependent) + Options + end + + properties (Access = protected) + Options_ struct = struct.empty + hOptionsEditor + end + properties + wasAborted = false + end + + methods (Access = public) + + function openControlPanel(obj) + %openControlPanel Open the options editor. Subclasses may override + % to set up preview state before calling editOptions. + obj.editOptions() + end + + function editOptions(obj) + %editOptions Open the options editor and wait (if modal). + optionsEditor = obj.openOptionsEditor(); + if obj.Modal + optionsEditor.waitfor() + obj.onOptionsEditorResumed() + else + addlistener(optionsEditor, 'AppDestroyed', ... + @(s, e) obj.onOptionsEditorResumed); + end + end + + function optionsEditor = openOptionsEditor(obj) + %openOptionsEditor Open the ui dialog for editing method options. + titleStr = sprintf('Options Editor (%s)', obj.Name); + if ~isempty(obj.OptionsManager) + optionsEditor = obj.OptionsManager.openOptionsEditor([], obj.getCurrentOptions()); + optionsEditor.Title = titleStr; + optionsEditor.Callback = @obj.onOptionsChanged; + else + optionsEditor = structeditor(obj.getCurrentOptions(), ... + 'Title', titleStr, 'Callback', @obj.onOptionsChanged); + end + obj.relocatePrimaryApp(optionsEditor) + obj.hOptionsEditor = optionsEditor; + addlistener(obj, 'ObjectBeingDestroyed', @(s,e) delete(optionsEditor)); + end + + function place(obj, varargin) + obj.hOptionsEditor.place(varargin{:}) + end + + end + + methods (Access = protected) + + function onOptionsEditorResumed(obj) + %onOptionsEditorResumed Called when the editor closes or is confirmed. + if ~isvalid(obj.hOptionsEditor) + obj.hOptionsEditor = []; + return + end + if ~obj.hOptionsEditor.wasCanceled + obj.setCurrentOptions(obj.hOptionsEditor.dataEdit); + end + obj.wasAborted = obj.hOptionsEditor.wasCanceled; + delete(obj.hOptionsEditor) + obj.hOptionsEditor = []; + obj.onOptionsEditorClosed() + if ~obj.wasAborted && obj.RunMethodOnFinish + obj.run() + end + if obj.DestroyOnFinish + delete(obj) + end + end + + function onOptionsEditorClosed(obj) %#ok + %onOptionsEditorClosed Called just before run/destroy. Subclasses + % override to clean up preview state (e.g. delete grid overlays). + end + + function onOptionsChanged(obj, name, value) + %onOptionsChanged Called when the options editor changes a value. + obj.updateOptionValue(name, value) + end + + function relocatePrimaryApp(obj, hPlugin, direction) + %relocatePrimaryApp Shift app and plugin windows so they do not overlap. + if nargin < 3; direction = 'horizontal'; end + figPos(1,:) = getpixelposition(hPlugin.Figure); + figPos(2,:) = getpixelposition(obj.PrimaryApp.Figure); + hFig = [hPlugin.Figure, obj.PrimaryApp.Figure]; + switch direction + case 'horizontal'; figPos_ = figPos(:, [1,3]); dim = 1; + case 'vertical'; figPos_ = figPos(:, [2,4]); dim = 2; + end + screenPos = uim.utility.getCurrentScreenSize(obj.PrimaryApp.Figure); + [~, idx] = sort(figPos_(:, 1)); + figPos = figPos(idx, :); + hFig = hFig(idx); + [x, ~] = uim.utility.layout.subdividePosition(min(figPos(:,1)), ... + screenPos(dim+2), figPos_(:,2), 20); + for i = 1:2 + hFig(i).Position(dim) = x(i); + end + end + + function options = getCurrentOptions(obj) + %getCurrentOptions Return the current method options. + options = obj.Options; + end + + function setCurrentOptions(obj, options) + %setCurrentOptions Assign the current method options. + obj.Options = options; + end + + function updateOptionValue(obj, name, value) + superFields = fieldnames(obj.Options); + + for i = 1:numel(superFields) + thisField = superFields{i}; + if isfield(obj.Options.(thisField), name) + obj.Options.(thisField).(name) = value; + end + end + end + end + + methods + function set.Options(obj, options) + obj.Options_ = options; + end + + function options = get.Options(obj) + if ~isempty(obj.Options_) + options = obj.Options_; + elseif ~isempty(obj.OptionsManager) + options = obj.OptionsManager.Options; + else + options = struct.empty; + end + end + end +end diff --git a/code/graphics/+applify/+mixin/UserSettings.m b/code/graphics/+applify/+mixin/UserSettings.m index 48716363..6102e09d 100644 --- a/code/graphics/+applify/+mixin/UserSettings.m +++ b/code/graphics/+applify/+mixin/UserSettings.m @@ -192,65 +192,27 @@ function editSettings(obj) %editSettings Open gui for editing fields of settings. titleStr = sprintf('Preferences for %s', class(obj)); - doDefault = true; % backward compatibility... - + if ~isempty(obj.hSettingsEditor) figure(obj.hSettingsEditor.Figure) return end - - % Todo: If obj has plugins, grab all settings from plugin - % classes as well. - if isprop(obj, 'plugins') - - hObjects = [{obj}, {obj.plugins.pluginHandle}]; - hasSettings = cellfun(@(h) isprop(h, 'settings'), hObjects); - - if sum(hasSettings) > 1 - - hObjects = hObjects(hasSettings); - settingsStruct = cellfun(@(h) h.settings, hObjects, 'uni', 0); - names = cellfun(@(h) class(h), hObjects, 'uni', 0); - callbacks = cellfun(@(h) @(name, value) h.changeSettings(name, value), hObjects, 'uni', 0); - - settingsStruct = tools.editStruct(settingsStruct, nan, titleStr, ... - 'Callback', callbacks, 'Name', names); - - obj.settings = settingsStruct{1}; - obj.saveSettings() - for i = 1:numel(hObjects) - hObjects{i}.settings = settingsStruct{i}; - hObjects{i}.saveSettings() - end - doDefault = false; - else - doDefault = true; - end - end - - if doDefault - try - obj.hSettingsEditor = structeditor.App(obj.settings, ... - 'Title', titleStr, 'Callback', @obj.onSettingsChanged); - - addlistener(obj.hSettingsEditor, 'AppDestroyed', ... - @(s, e) obj.onSettingsEditorClosed); - catch ME - - switch ME.identifier - case 'MATLAB:class:InvalidSuperClass' - - if contains(ME.message, 'uiw.mixin.AssignPVPairs') - msg = 'Settings window requires the Widgets Toolbox to be installed'; - errordlg(msg) - error(msg) - end - - otherwise - rethrow(ME) - - end + try + obj.hSettingsEditor = structeditor.App(obj.settings, ... + 'Title', titleStr, 'Callback', @obj.onSettingsChanged); + addlistener(obj.hSettingsEditor, 'AppDestroyed', ... + @(s, e) obj.onSettingsEditorClosed); + catch ME + switch ME.identifier + case 'MATLAB:class:InvalidSuperClass' + if contains(ME.message, 'uiw.mixin.AssignPVPairs') + msg = 'Settings window requires the Widgets Toolbox to be installed'; + errordlg(msg) + error(msg) + end + otherwise + rethrow(ME) end end end diff --git a/code/graphics/+applify/AppWithPlugin.m b/code/graphics/+applify/AppWithPlugin.m index 373e7df6..61a8bab5 100644 --- a/code/graphics/+applify/AppWithPlugin.m +++ b/code/graphics/+applify/AppWithPlugin.m @@ -67,14 +67,14 @@ function deletePlugin(obj, pluginName) end function wasCaptured = sendKeyEventToPlugins(obj, ~, evt) - %sendKeyEventToPlugins Send a key event to plugins + %sendKeyEventToPlugins Send a key press event to plugins % % The key event is sent to the plugins in the order they have % been added. If a plugin captures the key event, this method % returns before sending the event to the remaining plugins. - + wasCaptured = false; - + for i = 1:numel(obj.Plugins) try wasCaptured = obj.Plugins(i).keyPressHandler([], evt); @@ -84,5 +84,24 @@ function deletePlugin(obj, pluginName) end end end + + function wasCaptured = sendKeyReleaseEventToPlugins(obj, ~, evt) + %sendKeyReleaseEventToPlugins Send a key release event to plugins + % + % The key release event is sent to the plugins in the order they + % have been added. If a plugin captures the event, this method + % returns before sending the event to the remaining plugins. + + wasCaptured = false; + + for i = 1:numel(obj.Plugins) + try + wasCaptured = obj.Plugins(i).keyReleasedHandler([], evt); + if wasCaptured; return; end + catch ME + warning( [ME.message, '\n'] ) + end + end + end end end diff --git a/code/wrappers/+nansen/+plugin/+imviewer/EXTRACT.m b/code/wrappers/+nansen/+plugin/+imviewer/EXTRACT.m index e40aef0b..0d147f18 100644 --- a/code/wrappers/+nansen/+plugin/+imviewer/EXTRACT.m +++ b/code/wrappers/+nansen/+plugin/+imviewer/EXTRACT.m @@ -1,4 +1,4 @@ -classdef EXTRACT < imviewer.ImviewerPlugin +classdef EXTRACT < imviewer.ImviewerPlugin & applify.mixin.ModalMethodPreviewController %EXTRACT Imviewer plugin for EXTRACT method % % SYNTAX: @@ -6,11 +6,6 @@ % % extractPlugin = EXTRACT(imviewerObj, optionsManagerObj) - properties (Constant, Hidden = true) - USE_DEFAULT_SETTINGS = false % Ignore settings file - DEFAULT_SETTINGS = [] % This class uses an optionsmanager - end - properties (Constant) Name = 'EXTRACT' end @@ -62,18 +57,11 @@ function delete(obj) function openControlPanel(obj, mode) obj.plotGrid() - obj.editSettings() - end - - function loadSettings(~) - % This class does not have to load settings - end - function saveSettings(~) - % This class does not have to save settings + obj.editOptions() end - function changeSetting(obj, name, value) - obj.onSettingsChanged(name, value) + function changeOption(obj, name, value) + obj.onOptionsChanged(name, value) end function showTip(obj, message) @@ -90,39 +78,39 @@ function onPluginActivated(obj) end - function onSettingsChanged(obj, name, value) + function onOptionsChanged(obj, name, value) switch name case {'num_partitions_x', 'num_partitions_y'} - obj.settings.Main.(name) = value; + obj.Options.Main.(name) = value; obj.plotGrid() obj.checkGridSize() case 'use_gpu' - obj.settings.Main.(name) = value; + obj.Options.Main.(name) = value; if value && ismac obj.showTip('Note: GPU acceleration with Parallel Computing Toolbox is not supported on macOS versions 10.14 (Mojave) and above. Support for earlier macOS versions will be removed in a future MATLAB release.') end case 'avg_cell_radius' - obj.settings.Main.(name) = value; + obj.Options.Main.(name) = value; obj.plotCellTemplates(value) case 'temporal_denoising' - obj.settings.Preprocess.(name) = value; + obj.Options.Preprocess.(name) = value; if value obj.showTip('Note: This might increase processing time considerably for long movies') end case 'reestimate_S_if_downsampled' - obj.settings.Downsample.(name) = value; + obj.Options.Downsample.(name) = value; if value obj.showTip('This is not recommended as precise shape of cell images are typically not essential, and processing will take longer') end case 'trace_output_option' - obj.settings.Main.(name) = value; + obj.Options.Main.(name) = value; if strcmp(value, 'raw') obj.showTip('Please check EXTRACT''s FAQ before using this options') @@ -131,12 +119,6 @@ function onSettingsChanged(obj, name, value) end end - methods (Static) % Inherited... - function getPluginIcon() - - end - end - methods (Access = private) function plotGrid(obj) @@ -147,8 +129,8 @@ function plotGrid(obj) end % Plot grid lines - numRows = obj.settings.Main.num_partitions_y; - numCols = obj.settings.Main.num_partitions_x; + numRows = obj.Options.Main.num_partitions_y; + numCols = obj.Options.Main.num_partitions_x; hLine = imviewer.plot.plotGridLines(obj.PrimaryApp, numRows, numCols); @@ -206,8 +188,8 @@ function plotCellTemplates(obj, radius) function checkGridSize(obj) - numRows = obj.settings.Main.num_partitions_y; - numCols = obj.settings.Main.num_partitions_x; + numRows = obj.Options.Main.num_partitions_y; + numCols = obj.Options.Main.num_partitions_x; sizeX = obj.PrimaryApp.imWidth ./ numRows; sizeY = obj.PrimaryApp.imHeight ./ numCols; diff --git a/code/wrappers/+nansen/+plugin/+imviewer/FlowRegistration.m b/code/wrappers/+nansen/+plugin/+imviewer/FlowRegistration.m index 0bbee4b7..b5ed486c 100644 --- a/code/wrappers/+nansen/+plugin/+imviewer/FlowRegistration.m +++ b/code/wrappers/+nansen/+plugin/+imviewer/FlowRegistration.m @@ -1,4 +1,4 @@ -classdef FlowRegistration < imviewer.ImviewerPlugin & nansen.processing.MotionCorrectionPreview +classdef FlowRegistration < imviewer.ImviewerPlugin & applify.mixin.ModalMethodPreviewController & nansen.processing.MotionCorrectionPreview %FlowRegistration Imviewer plugin for FlowRegistration method % % SYNTAX: @@ -8,11 +8,6 @@ % Todo: Use methods of flowreg processor to run prealigning? - properties (Constant, Hidden = true) - USE_DEFAULT_SETTINGS = false % Ignore settings file - DEFAULT_SETTINGS = struct.empty; - end - properties (Constant) Name = 'Flow Registration' % Name of plugin end @@ -50,20 +45,20 @@ function delete(obj) methods - function sEditor = openSettingsEditor(obj) - %openSettingsEditor Open editor for method options. + function sEditor = openOptionsEditor(obj) + %openOptionsEditor Open editor for method options. - % Update folder- and filename in settings. + % Update folder- and filename in options. [folderPath, fileName] = fileparts( obj.ImviewerObj.ImageStack.FileName ); folderPath = fullfile(folderPath, obj.TargetFolderName); % Prepare default filename fileName = obj.buildFilenameWithExtension(fileName); - obj.settings_.Export.SaveDirectory = folderPath; - obj.settings_.Export.FileName = fileName; + obj.Options_.Export.SaveDirectory = folderPath; + obj.Options_.Export.FileName = fileName; - sEditor = openSettingsEditor@imviewer.ImviewerPlugin(obj); + sEditor = openOptionsEditor@applify.mixin.ModalMethodPreviewController(obj); % Need a better solution for this: idx = strcmp(sEditor.Name, 'Export'); @@ -76,7 +71,7 @@ function delete(obj) function openControlPanel(obj) obj.initializeGaussianFilter() - obj.editSettings() + obj.editOptions() end function run(obj) @@ -86,10 +81,10 @@ function run(obj) function runTestAlign(obj) % Check if saveResult or showResults is selected - obj.assertPreviewSettingsValid() + obj.assertPreviewOptionsValid() % Prepare save directory - if obj.settings.Preview.saveResults + if obj.Options.Preview.saveResults [saveFolder, datePrefix] = obj.prepareSaveFolder(); if isempty(saveFolder); return; end end @@ -101,7 +96,7 @@ function runTestAlign(obj) %stackSize = size(Y); import nansen.wrapper.flowreg.* - options = Options.convert(obj.settings); + options = Options.convert(obj.Options); if ~isa(Y, 'single') || ~isa(Y, 'double') Y = single(Y); @@ -123,13 +118,13 @@ function runTestAlign(obj) obj.ImviewerObj.clearMessage; % Show results from test aligning: - if obj.settings.Preview.showResults + if obj.Options.Preview.showResults h = imviewer(M); h.stackname = sprintf('%s - %s', obj.ImviewerObj.stackname, 'Flowreg Test Correction'); end % Save results from test aligning: - if obj.settings.Preview.saveResults + if obj.Options.Preview.saveResults getSavepath = @(name) fullfile(saveFolder, ... sprintf('%s_%s', datePrefix, name ) ); @@ -146,7 +141,7 @@ function runAlign(obj) dataSet = obj.prepareTargetDataset(); nansen.wrapper.flowreg.Processor(obj.ImviewerObj.ImageStack, ... - obj.settings, 'DataIoModel', dataSet) + obj.Options, 'DataIoModel', dataSet) end end @@ -157,8 +152,8 @@ function createSubMenu(obj) % Nothing here yet end - function onSettingsEditorClosed(obj) - %onSettingsEditorClosed "Callback" for when settings editor exits + function onOptionsEditorClosed(obj) + %onOptionsEditorClosed "Callback" for when options editor exits obj.resetGaussianFilter() end @@ -166,7 +161,7 @@ function assignDefaultOptions(obj) functionName = 'nansen.wrapper.flowreg.Processor'; obj.OptionsManager = nansen.manage.OptionsManager(functionName); - obj.settings = obj.OptionsManager.getOptions; + obj.Options = obj.OptionsManager.getOptions; end end @@ -174,7 +169,7 @@ function assignDefaultOptions(obj) function initializeGaussianFilter(obj) obj.ImviewerObj.imageDisplayMode.filter = 'gauss3d'; - obj.ImviewerObj.imageDisplayMode.filterParam = struct('sigma', obj.settings.General.sigmaX); + obj.ImviewerObj.imageDisplayMode.filterParam = struct('sigma', obj.Options.General.sigmaX); obj.ImviewerObj.updateImage(); obj.ImviewerObj.updateImageDisplay(); end @@ -287,39 +282,39 @@ function updateResults(obj) % Todo: Combine these into one method - function onSettingsChanged(obj, name, value) + function onOptionsChanged(obj, name, value) - % Call superclass method to deal with settings that are - % general motion correction settings. - onSettingsChanged@nansen.processing.MotionCorrectionPreview(obj, name, value) + % Call superclass method to deal with options that are + % general motion correction options. + onOptionsChanged@nansen.processing.MotionCorrectionPreview(obj, name, value) switch name case 'symmetricKernel' - obj.settings.General.symmetricKernel = value; + obj.Options.General.symmetricKernel = value; case {'sigmaX', 'sigmaY'} - if obj.settings.General.symmetricKernel - obj.settings.General.sigmaX = value; - obj.settings.General.sigmaY = value; + if obj.Options.General.symmetricKernel + obj.Options.General.sigmaX = value; + obj.Options.General.sigmaY = value; else - obj.settings.General.(name) = value; + obj.Options.General.(name) = value; end - obj.settings.Model.sigma = [obj.settings.General.sigmaY, obj.settings.General.sigmaX, obj.settings.General.sigmaZ]; - obj.ImviewerObj.imageDisplayMode.filterParam = struct('sigma', obj.settings.Model.sigma); + obj.Options.Model.sigma = [obj.Options.General.sigmaY, obj.Options.General.sigmaX, obj.Options.General.sigmaZ]; + obj.ImviewerObj.imageDisplayMode.filterParam = struct('sigma', obj.Options.Model.sigma); obj.ImviewerObj.updateImage(); obj.ImviewerObj.updateImageDisplay(); case 'sigmaZ' - obj.settings.sigma = [obj.settings.General.sigmaX, obj.settings.General.sigmaY, obj.settings.General.sigmaZ]; - obj.ImviewerObj.imageDisplayMode.filterParam = struct('sigma', obj.settings.Model.sigma); + obj.Options.sigma = [obj.Options.General.sigmaX, obj.Options.General.sigmaY, obj.Options.General.sigmaZ]; + obj.ImviewerObj.imageDisplayMode.filterParam = struct('sigma', obj.Options.Model.sigma); obj.ImviewerObj.updateImage(); obj.ImviewerObj.updateImageDisplay(); case 'FileName' - obj.settings.Export.FileName = value; - %obj.settings_.Export.FileName = value; + obj.Options.Export.FileName = value; + %obj.Options_.Export.FileName = value; end end end diff --git a/code/wrappers/+nansen/+plugin/+imviewer/FluFinder.m b/code/wrappers/+nansen/+plugin/+imviewer/FluFinder.m index a865991a..b1a926e6 100644 --- a/code/wrappers/+nansen/+plugin/+imviewer/FluFinder.m +++ b/code/wrappers/+nansen/+plugin/+imviewer/FluFinder.m @@ -1,4 +1,4 @@ -classdef FluFinder < imviewer.ImviewerPlugin +classdef FluFinder < imviewer.ImviewerPlugin & applify.mixin.ModalMethodPreviewController %FluFinder Imviewer plugin for FluFinder autosegmentation method % % SYNTAX: @@ -6,11 +6,6 @@ % % flufinderPlugin = FluFinder(imviewerObj, optionsManagerObj) - properties (Constant, Hidden = true) - USE_DEFAULT_SETTINGS = false % Ignore settings file - DEFAULT_SETTINGS = [] % This class uses an optionsmanager - end - properties (Constant) Name = 'FluFinder' end @@ -71,18 +66,11 @@ function delete(obj) methods function openControlPanel(obj, mode) - obj.editSettings() - end - - function loadSettings(~) - % This class does not have to load settings - end - function saveSettings(~) - % This class does not have to save settings + obj.editOptions() end - function changeSetting(obj, name, value) - obj.onSettingsChanged(name, value) + function changeOption(obj, name, value) + obj.onOptionsChanged(name, value) end function showTip(obj, message) @@ -101,7 +89,7 @@ function addPreviewOptions(obj) S.Show = 'Preprocessed'; S.Show_ = {'Preprocessed', 'Binarized'}; - obj.settings_.Preview = S; + obj.Options_.Preview = S; end @@ -109,51 +97,45 @@ function onPluginActivated(obj) end - function onSettingsChanged(obj, name, value) + function onOptionsChanged(obj, name, value) switch name case 'RoiDiameter' - obj.settings.General.RoiDiameter = value; + obj.Options.General.RoiDiameter = value; obj.plotCellTemplates(value/2) case 'Show' - obj.settings.Preview.Show = value; + obj.Options.Preview.Show = value; obj.changeImageToDisplay(); case 'PrctileForBinarization' - obj.settings.Preprocessing.PrctileForBinarization = value; + obj.Options.Preprocessing.PrctileForBinarization = value; - if strcmp(obj.settings.Preview.Show, 'Binarized') + if strcmp(obj.Options.Preview.Show, 'Binarized') obj.updateImviewerDisplay() end case 'PrctileForBaseline' - obj.settings.Preprocessing.PrctileForBaseline = value; + obj.Options.Preprocessing.PrctileForBaseline = value; obj.updateBackgroundImage() obj.BackgroundOffset = 0; - if strcmp(obj.settings.Preview.Show, 'Static Background') + if strcmp(obj.Options.Preview.Show, 'Static Background') obj.showImageInImviewer(obj.StaticBackground, 'Static Background') - elseif strcmp(obj.settings.Preview.Show, 'Preprocessed') + elseif strcmp(obj.Options.Preview.Show, 'Preprocessed') obj.updateImviewerDisplay() end case 'SmoothingSigma' - obj.settings.Preprocessing.SmoothingSigma = value; + obj.Options.Preprocessing.SmoothingSigma = value; obj.BackgroundOffset = 0; - if strcmp(obj.settings.Preview.Show, 'Preprocessed') + if strcmp(obj.Options.Preview.Show, 'Preprocessed') obj.updateImviewerDisplay() end end end end - methods (Static) % Inherited... - function getPluginIcon() - - end - end - methods (Access = private) function changeImageToDisplay(obj) @@ -161,7 +143,7 @@ function changeImageToDisplay(obj) hRoimanager = obj.PrimaryApp.getPluginHandle('Roimanager'); imArray = hRoimanager.prepareImagedata(); - switch obj.settings.Preview.Show + switch obj.Options.Preview.Show case 'Preprocessed' updateFcn = @obj.getPreprocessedImage; @@ -186,7 +168,7 @@ function changeImageToDisplay(obj) function imArray = getPreprocessedImageArray(obj, imArray) import nansen.wrapper.abstract.OptionsAdapter - opts = OptionsAdapter.ungroupOptions(obj.settings); + opts = OptionsAdapter.ungroupOptions(obj.Options); if isempty(obj.CachePreprocessed) imArray = flufinder.module.preprocessImages(imArray, opts); @@ -199,7 +181,7 @@ function changeImageToDisplay(obj) function imArray = getBinarizedImageArray(obj, imArray) import nansen.wrapper.abstract.OptionsAdapter - opts = OptionsAdapter.ungroupOptions(obj.settings); + opts = OptionsAdapter.ungroupOptions(obj.Options); if isempty(obj.CacheBinarized) imArray = obj.getPreprocessedImageArray(imArray); @@ -212,7 +194,7 @@ function changeImageToDisplay(obj) end function image = getPreprocessedImage(obj, image) - opts = obj.getUngroupedSettings(); + opts = obj.getUngroupedOptions(); % Preprocess (subtract dynamic background) optsNames = {'SpatialFilterType', 'SmoothingSigma'}; @@ -238,7 +220,7 @@ function changeImageToDisplay(obj) function image = getBinarizedImage(obj, image) - opts = obj.getUngroupedSettings(); + opts = obj.getUngroupedOptions(); imageType = class(image); image = obj.getPreprocessedImage(image); @@ -262,7 +244,7 @@ function updateBackgroundImage(obj) import flufinder.preprocess.computeStaticBackgroundImage imageArray = obj.getImageArray(); - opts = obj.getUngroupedSettings(); + opts = obj.getUngroupedOptions(); bgImage = computeStaticBackgroundImage(imageArray, opts); @@ -333,9 +315,9 @@ function plotCellTemplates(obj, radius) imArray = hRoimanager.prepareImagedata(); end - function opts = getUngroupedSettings(obj) + function opts = getUngroupedOptions(obj) import nansen.wrapper.abstract.OptionsAdapter - opts = OptionsAdapter.ungroupOptions(obj.settings); + opts = OptionsAdapter.ungroupOptions(obj.Options); end end end diff --git a/code/wrappers/+nansen/+plugin/+imviewer/NoRMCorre.m b/code/wrappers/+nansen/+plugin/+imviewer/NoRMCorre.m index 442b837b..99993509 100644 --- a/code/wrappers/+nansen/+plugin/+imviewer/NoRMCorre.m +++ b/code/wrappers/+nansen/+plugin/+imviewer/NoRMCorre.m @@ -1,4 +1,4 @@ -classdef NoRMCorre < imviewer.ImviewerPlugin & nansen.processing.MotionCorrectionPreview +classdef NoRMCorre < imviewer.ImviewerPlugin & applify.mixin.ModalMethodPreviewController & nansen.processing.MotionCorrectionPreview %NoRMCorre Imviewer plugin for NoRMCorre method % % SYNTAX: @@ -9,22 +9,14 @@ % INHERITANCE: % |- imviewer.ImviewerPlugin % |- applify.mixin.AppPlugin -% |- applify.mixin.UserSettings % |- matlab.mixin.Heterogeneous % |- uiw.mixin.AssignPVPairs % TODO: -% [v] Subclass from imviewer plugin class. % [ ] migrate plugin to new instance if results open in new window -% [v] Implement options based on OptionsManager & normcorre options. % [ ] Should it have a DataIoModel property? Then its easy to plug in % whatever model (i.e) a session model and save data consistently. - properties (Constant, Hidden = true) - USE_DEFAULT_SETTINGS = false % Ignore settings file - DEFAULT_SETTINGS = [] % This class uses an optionsmanager - end - properties (Constant) % Implementation of AppPlugin property Name = 'NoRMCorre' end @@ -51,7 +43,7 @@ obj@imviewer.ImviewerPlugin(varargin{:}) - if ~ obj.PartialConstruction && isempty(obj.hSettingsEditor) + if ~ obj.PartialConstruction && isempty(obj.hOptionsEditor) obj.openControlPanel() end @@ -72,12 +64,6 @@ function delete(obj) end end - function loadSettings(~) % override to do nothing - % This class does not have to load settings - end - function saveSettings(~) % override to do nothing - % This class does not have to save settings - end end methods (Access = protected) % Plugin derived methods @@ -91,8 +77,8 @@ function createSubMenu(obj) obj.MenuItem(1).PlotShifts.Callback = @obj.plotResults; end - function onSettingsEditorClosed(obj) - %onSettingsEditorClosed "Callback" for when settings editor exits + function onOptionsEditorClosed(obj) + %onOptionsEditorClosed "Callback" for when options editor exits delete(obj.hGridLines) delete(obj.hGridOverlaps) end @@ -100,26 +86,26 @@ function onSettingsEditorClosed(obj) function assignDefaultOptions(obj) functionName = 'nansen.wrapper.normcorre.Processor'; obj.OptionsManager = nansen.manage.OptionsManager(functionName); - obj.settings = obj.OptionsManager.getOptions; + obj.Options = obj.OptionsManager.getOptions; end end methods % Methods for running normcorre motion correction - function sEditor = openSettingsEditor(obj) - %openSettingsEditor Open editor for method options. + function sEditor = openOptionsEditor(obj) + %openOptionsEditor Open editor for method options. - % Update folder- and filename in settings. + % Update folder- and filename in options. [folderPath, fileName] = fileparts( obj.ImviewerObj.ImageStack.FileName ); folderPath = fullfile(folderPath, obj.TargetFolderName); % Prepare default filename fileName = obj.buildFilenameWithExtension(fileName); - obj.settings_.Export.SaveDirectory = folderPath; - obj.settings_.Export.FileName = fileName; + obj.Options_.Export.SaveDirectory = folderPath; + obj.Options_.Export.FileName = fileName; - sEditor = openSettingsEditor@imviewer.ImviewerPlugin(obj); + sEditor = openOptionsEditor@applify.mixin.ModalMethodPreviewController(obj); % Need a better solution for this: idx = strcmp(sEditor.Name, 'Export'); @@ -132,7 +118,7 @@ function assignDefaultOptions(obj) function openControlPanel(obj) obj.plotGrid() - obj.editSettings() + obj.editOptions() end function run(obj) @@ -146,10 +132,10 @@ function runTestAlign(obj) % Run a motion correction processor on frames instead? % Check if saveResult or showResults is selected - obj.assertPreviewSettingsValid() + obj.assertPreviewOptionsValid() % Prepare save directory - if obj.settings.Preview.saveResults + if obj.Options.Preview.saveResults [saveFolder, datePrefix] = obj.prepareSaveFolder(); if isempty(saveFolder); return; end end @@ -161,7 +147,7 @@ function runTestAlign(obj) stackSize = size(Y); import nansen.wrapper.normcorre.* - ncOptions = Options.convert(obj.settings, stackSize); + ncOptions = Options.convert(obj.Options, stackSize); if ~isa(Y, 'single') || ~isa(Y, 'double') Y = single(Y); @@ -172,7 +158,7 @@ function runTestAlign(obj) obj.ImviewerObj.displayMessage('Running NoRMCorre...') warning('off', 'MATLAB:mir_warning_maybe_uninitialized_temporary') - [M, ncShifts, ref] = normcorre_batch(Y, ncOptions); + [M, ncShifts, ~] = normcorre_batch(Y, ncOptions); warning('on', 'MATLAB:mir_warning_maybe_uninitialized_temporary') obj.TestResults(end+1).Shifts = ncShifts; @@ -185,13 +171,13 @@ function runTestAlign(obj) obj.ImviewerObj.clearMessage; % Show results from test aligning: - if obj.settings.Preview.showResults + if obj.Options.Preview.showResults h = imviewer(M); h.stackname = sprintf('%s - %s', obj.ImviewerObj.stackname, 'NoRMCorre Test Correction'); end % Save results from test aligning: - if obj.settings.Preview.saveResults + if obj.Options.Preview.saveResults getSavepath = @(name) fullfile(saveFolder, ... sprintf('%s_%s', datePrefix, name ) ); @@ -209,35 +195,35 @@ function runAlign(obj) dataSet = obj.prepareTargetDataset(); nansen.wrapper.normcorre.Processor(obj.ImviewerObj.ImageStack,... - obj.settings, 'DataIoModel', dataSet) + obj.Options, 'DataIoModel', dataSet) end end methods (Access = protected) - function onSettingsChanged(obj, name, value) + function onOptionsChanged(obj, name, value) - % Call superclass method to deal with settings that are - % general motion correction settings. - onSettingsChanged@nansen.processing.MotionCorrectionPreview(obj, name, value) + % Call superclass method to deal with options that are + % general motion correction options. + onOptionsChanged@nansen.processing.MotionCorrectionPreview(obj, name, value) - patchesFields = fieldnames(obj.settings.Configuration); - templateFields = fieldnames(obj.settings.Template); + patchesFields = fieldnames(obj.Options.Configuration); + templateFields = fieldnames(obj.Options.Template); switch name % Note: this needs to go before the patchesfield! case {'numRows', 'numCols', 'patchOverlap'} - obj.settings.Configuration.(name) = value; + obj.Options.Configuration.(name) = value; obj.plotGrid() case patchesFields - obj.settings.Configuration.(name) = value; + obj.Options.Configuration.(name) = value; case templateFields - obj.settings.Template.(name) = value; + obj.Options.Template.(name) = value; case {'firstFrame', 'numFrames', 'saveResults', 'showResults'} - obj.settings.Preview.(name) = value; + obj.Options.Preview.(name) = value; case 'runAlign' obj.runAlign() @@ -252,8 +238,8 @@ function plotGrid(obj) xLim = [1,obj.ImviewerObj.imWidth]; yLim = [1,obj.ImviewerObj.imHeight]; - numRows = obj.settings.Configuration.numRows; - numCols = obj.settings.Configuration.numCols; + numRows = obj.Options.Configuration.numRows; + numCols = obj.Options.Configuration.numCols; xPoints = linspace(xLim(1),xLim(2), numCols+1); yPoints = linspace(yLim(1),yLim(2), numRows+1); @@ -277,8 +263,8 @@ function plotGrid(obj) set(obj.hGridLines, 'HitTest', 'off', 'Tag', 'NorRMCorre Gridlines'); xDataVert = cat(1, xDataVert, xDataVert); - xDataVert(1:2, :) = xDataVert(1:2, :) - obj.settings.Configuration.patchOverlap(2)/2; - xDataVert(3:4, :) = xDataVert(3:4, :) + obj.settings.Configuration.patchOverlap(2)/2; + xDataVert(1:2, :) = xDataVert(1:2, :) - obj.Options.Configuration.patchOverlap(2)/2; + xDataVert(3:4, :) = xDataVert(3:4, :) + obj.Options.Configuration.patchOverlap(2)/2; yDataVert = cat(1, yDataVert, flipud(yDataVert)); h2 = patch(obj.ImviewerObj.Axes, xDataVert, yDataVert, 'w'); @@ -286,8 +272,8 @@ function plotGrid(obj) xDataHorz = cat(1, xDataHorz, flipud(xDataHorz)); yDataHorz = cat(1, yDataHorz, yDataHorz); - yDataHorz(1:2, :) = yDataHorz(1:2, :) - obj.settings.Configuration.patchOverlap(1)/2; - yDataHorz(3:4, :) = yDataHorz(3:4, :) + obj.settings.Configuration.patchOverlap(1)/2; + yDataHorz(1:2, :) = yDataHorz(1:2, :) - obj.Options.Configuration.patchOverlap(1)/2; + yDataHorz(3:4, :) = yDataHorz(3:4, :) + obj.Options.Configuration.patchOverlap(1)/2; h3 = patch(obj.ImviewerObj.Axes, xDataHorz, yDataHorz, 'w'); set(h3, 'FaceAlpha', 0.15, 'HitTest', 'off') diff --git a/code/wrappers/+nansen/+plugin/+signalviewer/CaimanDeconvolution.m b/code/wrappers/+nansen/+plugin/+signalviewer/CaimanDeconvolution.m index 01f43e14..f215ae43 100644 --- a/code/wrappers/+nansen/+plugin/+signalviewer/CaimanDeconvolution.m +++ b/code/wrappers/+nansen/+plugin/+signalviewer/CaimanDeconvolution.m @@ -1,18 +1,9 @@ -classdef CaimanDeconvolution < applify.mixin.AppPlugin % signalviewer plugin - - properties (Constant, Hidden = true) - USE_DEFAULT_SETTINGS = false % Ignore settings file - DEFAULT_SETTINGS = [] % This class uses an optionsmanager - end +classdef CaimanDeconvolution < applify.mixin.AppPlugin & applify.mixin.HasOptionsManager % signalviewer plugin properties (Constant) % Implementation of AppPlugin property Name = 'CaImAn Deconvolution' end - properties - PrimaryAppName = 'Roi Signal Explorer' - end - properties (Access = protected) RoiSignalArray end @@ -34,7 +25,7 @@ obj.Mode = 'UpdateAll'; - obj.editSettings() + obj.editOptions() end @@ -56,24 +47,20 @@ function createSubMenu(obj) % Todo: Open? Close? Toggle? obj.MenuItem(1).ExploreDff = uimenu(m, 'Text', 'Deconvolve...', 'Enable', 'off'); - obj.MenuItem(1).PlotShifts.Callback = @obj.editSettings; + obj.MenuItem(1).PlotShifts.Callback = @obj.editOptions; end function assignDefaultOptions(obj) - %functionName = 'ophys.twophoton.process.deconvolution.Caiman'; functionName = 'nansen.twophoton.roisignals.deconvolveDff'; obj.OptionsManager = nansen.manage.OptionsManager(functionName); - obj.settings = obj.OptionsManager.getOptions; end end - + methods (Access = protected) - - function onSettingsChanged(obj, name, value) - - obj.settings_.(name) = value; - obj.RoiSignalArray.DeconvolutionOptions = obj.settings; + + function onOptionsChanged(obj) + obj.RoiSignalArray.DeconvolutionOptions = obj.Options; obj.PrimaryApp.displayMessage('Updating Deconvolved Signal...') @@ -104,7 +91,7 @@ function updateInternal(obj) dff_ = dff(isVisible); xData_ = xData(isVisible); - [dec, den, ~] = deconvolveDff(dff_, obj.settings);%, options) + [dec, den, ~] = deconvolveDff(dff_, obj.Options); if isempty(obj.hLineDeconvolved) yyaxis(hAx, 'right') diff --git a/code/wrappers/+nansen/+plugin/+signalviewer/DffExplorer.m b/code/wrappers/+nansen/+plugin/+signalviewer/DffExplorer.m index 4ddd4a55..ba5f12c7 100644 --- a/code/wrappers/+nansen/+plugin/+signalviewer/DffExplorer.m +++ b/code/wrappers/+nansen/+plugin/+signalviewer/DffExplorer.m @@ -1,18 +1,9 @@ -classdef DffExplorer < applify.mixin.AppPlugin % signalviewer plugin - - properties (Constant, Hidden = true) - USE_DEFAULT_SETTINGS = false % Ignore settings file - DEFAULT_SETTINGS = [] % This class uses an optionsmanager - end +classdef DffExplorer < applify.mixin.AppPlugin & applify.mixin.HasOptionsManager % signalviewer plugin properties (Constant) % Implementation of AppPlugin property Name = 'DFF Explorer' end - properties - PrimaryAppName = 'Roi Signal Explorer' - end - properties (Access = protected) RoiSignalArray end @@ -26,7 +17,7 @@ obj.PrimaryApp.Figure.Name = 'DFF Explorer'; obj.RoiSignalArray = obj.PrimaryApp.RoiSignalArray; - obj.editSettings() + obj.editOptions() end @@ -48,28 +39,21 @@ function createSubMenu(obj) % Todo: Open? Close? Toggle? obj.MenuItem(1).ExploreDff = uimenu(m, 'Text', 'Explore DFF', 'Enable', 'off'); - obj.MenuItem(1).PlotShifts.Callback = @obj.editSettings; + obj.MenuItem(1).PlotShifts.Callback = @obj.editOptions; end function assignDefaultOptions(obj) functionName = 'nansen.twophoton.roisignals.computeDff'; obj.OptionsManager = nansen.manage.OptionsManager(functionName); - obj.settings = obj.OptionsManager.getOptions; end end - + methods (Access = protected) - - function onSettingsChanged(obj, name, value) - - obj.settings_.(name) = value; - - obj.RoiSignalArray.DffOptions = obj.settings; + + function onOptionsChanged(obj) + obj.RoiSignalArray.DffOptions = obj.Options; obj.RoiSignalArray.resetSignals('all', {'dff'}) - - %obj.updateSignalPlot(obj.DisplayedRoiIndices, 'replace', {'dff'}, true); - end end end