diff --git a/code/+nansen/+manage/OptionsManager.m b/code/+nansen/+manage/OptionsManager.m index 60af97321..369e651a5 100644 --- a/code/+nansen/+manage/OptionsManager.m +++ b/code/+nansen/+manage/OptionsManager.m @@ -384,12 +384,22 @@ function setOptions(obj, optionsName, options) sEditor = structeditor(obj.Options, 'OptionsManager', obj, 'Title', name); sEditor.waitfor() - wasAborted = sEditor.wasCanceled; - - if wasAborted - obj.Options = sEditor.dataOrig; + + if isprop(sEditor, 'FinishState') + wasAborted = sEditor.FinishState ~= "Finished"; + if wasAborted + obj.Options = sEditor.OriginalData; + else + obj.Options = sEditor.Data; + end else - obj.Options = sEditor.dataEdit; + wasAborted = sEditor.wasCanceled; + + if wasAborted + obj.Options = sEditor.dataOrig; + else + obj.Options = sEditor.dataEdit; + end end delete(sEditor) @@ -420,8 +430,13 @@ function setOptions(obj, optionsName, options) 'OptionsManager', obj, ... 'Title', titleStr, ... 'Prompt', promptStr ); - - hOptionsEditor.changeOptionsSelectionDropdownValue(optionsName); + + if ismethod(hOptionsEditor, 'changeOptionsSelectionDropdownValue') + hOptionsEditor.changeOptionsSelectionDropdownValue(optionsName); + else + plugin = hOptionsEditor.getPlugin("nansen.manage.OptionsManagerPlugin"); + plugin.setCurrentOptionsName(optionsName); + end end @@ -449,14 +464,25 @@ function setOptions(obj, optionsName, options) sEditor = obj.openOptionsEditor(optsName, optsStruct); sEditor.waitfor() - - wasAborted = sEditor.wasCanceled; - if sEditor.wasCanceled - optsStruct = sEditor.dataOrig; + if isprop(sEditor, 'FinishState') + wasAborted = sEditor.FinishState ~= "Finished"; + plugin = sEditor.getPlugin("nansen.manage.OptionsManagerPlugin"); + if wasAborted + optsStruct = sEditor.OriginalData; + else + optsStruct = sEditor.Data; + optsName = plugin.CurrentOptionsName; + end else - optsStruct = sEditor.dataEdit; - optsName = sEditor.currentOptionsName; + wasAborted = sEditor.wasCanceled; + + if sEditor.wasCanceled + optsStruct = sEditor.dataOrig; + else + optsStruct = sEditor.dataEdit; + optsName = sEditor.currentOptionsName; + end end % Clear modified options! diff --git a/code/+nansen/+manage/OptionsManagerPlugin.m b/code/+nansen/+manage/OptionsManagerPlugin.m new file mode 100644 index 000000000..f5b4811d8 --- /dev/null +++ b/code/+nansen/+manage/OptionsManagerPlugin.m @@ -0,0 +1,212 @@ +classdef OptionsManagerPlugin < handle + + properties (SetAccess = private) + OptionsManager + CurrentOptionsName char = '' + end + + properties (Access = private) + Editor + HeaderGrid matlab.ui.container.GridLayout + OptionsDropDown matlab.ui.control.DropDown + SaveButton matlab.ui.control.Button + DefaultButton matlab.ui.control.Button + IsUpdating (1,1) logical = false + end + + methods + function obj = OptionsManagerPlugin(optionsManager, optionsName) + arguments + optionsManager + optionsName char = '' + end + + obj.OptionsManager = optionsManager; + if isempty(optionsName) + optionsName = optionsManager.OptionsName; + end + if isempty(optionsName) + optionsName = optionsManager.getPreferredOptionsName(); + end + obj.CurrentOptionsName = char(optionsName); + end + + function attach(obj, editor) + obj.Editor = editor; + parent = editor.getAttachmentContainer("header"); + + obj.HeaderGrid = uigridlayout(parent); + obj.HeaderGrid.ColumnWidth = {'1x', 82, 96}; + obj.HeaderGrid.RowHeight = {'1x'}; + obj.HeaderGrid.Padding = [0 0 0 0]; + obj.HeaderGrid.ColumnSpacing = 6; + + obj.OptionsDropDown = uidropdown(obj.HeaderGrid); + obj.OptionsDropDown.Layout.Row = 1; + obj.OptionsDropDown.Layout.Column = 1; + obj.OptionsDropDown.ValueChangedFcn = @obj.onOptionsSetChanged; + + obj.SaveButton = uibutton(obj.HeaderGrid, ... + "Text", "Save Options", ... + "ButtonPushedFcn", @obj.onSaveOptionsPushed); + obj.SaveButton.Layout.Row = 1; + obj.SaveButton.Layout.Column = 2; + + obj.DefaultButton = uibutton(obj.HeaderGrid, ... + "Text", "Make Default", ... + "ButtonPushedFcn", @obj.onMakeDefaultPushed); + obj.DefaultButton.Layout.Row = 1; + obj.DefaultButton.Layout.Column = 3; + + obj.refreshOptionsDropdown(); + end + + function setCurrentOptionsName(obj, optionsName) + obj.CurrentOptionsName = char(optionsName); + obj.refreshOptionsDropdown(); + end + + function onDataChanged(obj, ~, ~) + if obj.IsUpdating || isempty(obj.OptionsDropDown) + return + end + + opts = obj.Editor.Data; + currentName = obj.CurrentOptionsName; + + if contains(currentName, 'Modified') + obj.OptionsManager.appendModifiedOptions(opts, currentName) + else + obj.CurrentOptionsName = sprintf('%s (Modified)', currentName); + obj.OptionsManager.appendModifiedOptions(opts, obj.CurrentOptionsName) + end + + obj.refreshOptionsDropdown(); + end + + function onFinishStateChanged(~, ~, ~) + % Hook kept for the generic plugin lifecycle. OptionsManager + % state is read by callers after editor.waitfor returns. + end + + function control = getTestControl(obj, controlName) + control = obj.(controlName); + end + end + + methods (Access = private) + function onOptionsSetChanged(obj, src, ~) + newName = char(src.Value); + oldName = obj.CurrentOptionsName; + + if strcmp(newName, oldName) + return + end + + if contains(oldName, 'Modified') + obj.OptionsManager.appendModifiedOptions(obj.Editor.Data, oldName) + end + + if obj.OptionsManager.isModified(newName) + newOptions = obj.OptionsManager.getModifiedOptions(newName); + else + newOptions = obj.OptionsManager.getOptions(newName); + end + + obj.IsUpdating = true; + cleanupObj = onCleanup(@() obj.clearUpdatingFlag()); + obj.Editor.replaceData(newOptions); + obj.CurrentOptionsName = newName; + obj.refreshOptionsDropdown(); + delete(cleanupObj) + end + + function onSaveOptionsPushed(obj, ~, ~) + currentName = obj.CurrentOptionsName; + givenName = obj.OptionsManager.saveCustomOptions(obj.Editor.Data); + if isempty(givenName) + return + end + + if contains(currentName, 'Modified') + obj.OptionsManager.removeModifiedOptions(currentName) + end + + obj.CurrentOptionsName = givenName; + obj.refreshOptionsDropdown(); + end + + function onMakeDefaultPushed(obj, ~, ~) + if isempty(obj.CurrentOptionsName) || contains(obj.CurrentOptionsName, 'Modified') + return + end + + obj.OptionsManager.setDefault(obj.CurrentOptionsName); + obj.refreshOptionsDropdown(); + end + + function refreshOptionsDropdown(obj) + if isempty(obj.OptionsDropDown) || ~isvalid(obj.OptionsDropDown) + return + end + + [displayNames, optionNames] = obj.getDropdownNames(); + obj.OptionsDropDown.Items = displayNames; + obj.OptionsDropDown.ItemsData = optionNames; + + if any(strcmp(optionNames, obj.CurrentOptionsName)) + obj.OptionsDropDown.Value = obj.CurrentOptionsName; + elseif ~isempty(optionNames) + obj.CurrentOptionsName = optionNames{1}; + obj.OptionsDropDown.Value = obj.CurrentOptionsName; + end + + obj.updateButtonStates(); + end + + function [displayNames, optionNames] = getDropdownNames(obj) + presetNames = obj.OptionsManager.PresetOptionNames; + customNames = obj.OptionsManager.CustomOptionNames; + editedNames = obj.OptionsManager.EditedOptionNames; + optionNames = [presetNames, customNames, editedNames]; + + displayNames = [ ... + obj.OptionsManager.formatPresetNames(presetNames), ... + customNames, ... + obj.OptionsManager.formatEditedNames(editedNames)]; + + defaultName = obj.OptionsManager.getPreferredOptionsName(); + isDefault = strcmp(optionNames, defaultName); + displayNames(isDefault) = obj.OptionsManager.formatDefaultName(displayNames(isDefault)); + end + + function updateButtonStates(obj) + if isempty(obj.SaveButton) || ~isvalid(obj.SaveButton) + return + end + + isModified = contains(obj.CurrentOptionsName, 'Modified'); + obj.SaveButton.Enable = obj.onOff(isModified); + + defaultName = obj.OptionsManager.getPreferredOptionsName(); + canMakeDefault = ~isempty(obj.CurrentOptionsName) ... + && ~isModified ... + && ~strcmp(obj.CurrentOptionsName, defaultName); + obj.DefaultButton.Enable = obj.onOff(canMakeDefault); + end + + function clearUpdatingFlag(obj) + obj.IsUpdating = false; + end + end + + methods (Static, Access = private) + function value = onOff(tf) + if tf + value = 'on'; + else + value = 'off'; + end + end + end +end diff --git a/code/apps/+imviewer/+plugin/@RoiSignalVideo/RoiSignalVideo.m b/code/apps/+imviewer/+plugin/@RoiSignalVideo/RoiSignalVideo.m index f3d5252c0..2a8bba514 100644 --- a/code/apps/+imviewer/+plugin/@RoiSignalVideo/RoiSignalVideo.m +++ b/code/apps/+imviewer/+plugin/@RoiSignalVideo/RoiSignalVideo.m @@ -66,14 +66,22 @@ obj.loadSettings() obj.imviewerRef.displayMessage('Preparing video export') - sEditor = structeditor.App(obj.settings, 'Title', 'Set preferences for video export'); + sEditor = structeditor(obj.settings, 'Title', 'Set preferences for video export'); sEditor.waitfor() - if sEditor.wasCanceled + if isprop(sEditor, 'FinishState') + wasCanceled = sEditor.FinishState ~= "Finished"; + editedSettings = sEditor.Data; + else + wasCanceled = sEditor.wasCanceled; + editedSettings = sEditor.dataEdit; + end + + if wasCanceled obj.imviewerRef.clearMessage return else - obj.settings = sEditor.dataEdit; + obj.settings = editedSettings; obj.saveSettings() end diff --git a/code/apps/+imviewer/@App/App.m b/code/apps/+imviewer/@App/App.m index ebcdecb84..0275973bd 100755 --- a/code/apps/+imviewer/@App/App.m +++ b/code/apps/+imviewer/@App/App.m @@ -4338,18 +4338,26 @@ function uiEditStackMetadata(obj) S.PhysicalUnits = obj.ImageStack.MetaData.getPhysicalUnits(); S.SampleRate = obj.ImageStack.getSampleRate(); - h = structeditor.App(S, 'Title', 'ImageStack Properties', ... + h = structeditor(S, 'Title', 'ImageStack Properties', ... 'Prompt', 'Set ImageStack Properties:'); h.waitfor() - if ~h.wasCanceled - obj.ImageStack.MetaData.SampleRate = h.dataEdit.SampleRate; - obj.ImageStack.MetaData.SpatialPosition = h.dataEdit.SpatialPosition; - physSize = h.dataEdit.PhysicalSize; - if numel(h.dataEdit.PhysicalUnits) == 1 - physUnits = strsplit(h.dataEdit.PhysicalUnits{1}, ', '); + if isprop(h, 'FinishState') + if h.FinishState ~= "Finished"; return; end + editedData = h.Data; + else + if h.wasCanceled; return; end + editedData = h.dataEdit; + end + + if ~isempty(editedData) + obj.ImageStack.MetaData.SampleRate = editedData.SampleRate; + obj.ImageStack.MetaData.SpatialPosition = editedData.SpatialPosition; + physSize = editedData.PhysicalSize; + if numel(editedData.PhysicalUnits) == 1 + physUnits = strsplit(editedData.PhysicalUnits{1}, ', '); else - physUnits = h.dataEdit.PhysicalUnits; + physUnits = editedData.PhysicalUnits; end obj.ImageStack.MetaData.PhysicalSizeX = physSize(1); obj.ImageStack.MetaData.PhysicalSizeY = physSize(2); diff --git a/code/apps/structeditor.m b/code/apps/structeditor.m index b8093fbb2..3731a992b 100644 --- a/code/apps/structeditor.m +++ b/code/apps/structeditor.m @@ -5,13 +5,136 @@ % For more detailed information: % See also structeditor.App + [varargin, forceLegacy] = popRouterOptions(varargin); + if nargin == 0 hApp = structeditor.App(); - else + elseif useLegacyStructEditor(forceLegacy, varargin{:}) hApp = structeditor.App(varargin{:}); + else + varargin = prepareModernStructEditorInputs(varargin); + hApp = structeditor.StructEditorApp(varargin{:}); end if ~nargout clear hApp end end + +function tf = useLegacyStructEditor(forceLegacy, varargin) + + tf = forceLegacy; + if tf; return; end + + if isMATLABReleaseOlderThan("R2025a") + tf = true; + return + end + + if exist("structeditor.StructEditorApp", "class") ~= 8 + tf = true; + return + end + + if hasParentHandle(varargin{:}) + tf = true; + return + end + + if hasNameValueOption(varargin, "showPresetInHeader") && ... + ~hasNameValueOption(varargin, "OptionsManager") + tf = true; + return + end + + if hasNameValueOption(varargin, "OptionsManager") && ... + exist("nansen.manage.OptionsManagerPlugin", "class") ~= 8 + tf = true; + end +end + +function tf = hasParentHandle(varargin) + tf = ~isempty(varargin) && ... + (isa(varargin{1}, "matlab.ui.container.Panel") || ... + isa(varargin{1}, "matlab.ui.container.Tab") || ... + isa(varargin{1}, "matlab.ui.Figure")); +end + +function tf = hasNameValueOption(args, names) + tf = false; + if isempty(args); return; end + + firstNameValueIndex = 2; + if hasParentHandle(args{:}) + firstNameValueIndex = 3; + end + + for i = firstNameValueIndex:2:numel(args) + if ischar(args{i}) || isstring(args{i}) + if any(strcmpi(string(args{i}), names)) + tf = true; + return + end + end + end +end + +function [args, forceLegacy] = popRouterOptions(args) + forceLegacy = false; + removeIdx = []; + + firstNameValueIndex = 2; + if hasParentHandle(args{:}) + firstNameValueIndex = 3; + end + + for i = firstNameValueIndex:2:numel(args) + if ~(ischar(args{i}) || isstring(args{i})) + continue + end + + if any(strcmpi(string(args{i}), ["ForceLegacy", "UseLegacyStructEditor"])) + if i < numel(args) + forceLegacy = logical(args{i+1}); + removeIdx = [i, i+1]; + end + break + end + end + + args(removeIdx) = []; +end + +function args = prepareModernStructEditorInputs(args) + [args, optionsManager] = popNameValue(args, "OptionsManager"); + [args, ~] = popNameValue(args, "showPresetInHeader"); + + if ~isempty(optionsManager) + plugin = nansen.manage.OptionsManagerPlugin(optionsManager); + args = [args, {'Plugin', plugin}]; + end +end + +function [args, value] = popNameValue(args, name) + value = []; + removeIdx = []; + + firstNameValueIndex = 2; + if hasParentHandle(args{:}) + firstNameValueIndex = 3; + end + + for i = firstNameValueIndex:2:numel(args) + if ~(ischar(args{i}) || isstring(args{i})) + continue + end + + if strcmpi(string(args{i}), name) && i < numel(args) + value = args{i+1}; + removeIdx = [i, i+1]; + break + end + end + + args(removeIdx) = []; +end diff --git a/code/general/+tools/editStruct.m b/code/general/+tools/editStruct.m index 1935a1173..45f42d2a6 100644 --- a/code/general/+tools/editStruct.m +++ b/code/general/+tools/editStruct.m @@ -58,14 +58,25 @@ else sEditor = structeditor(sIn, varargin{:}); - sEditor.IsModal = true; + if isprop(sEditor, 'IsModal') + sEditor.IsModal = true; + end sEditor.waitfor() - - if sEditor.wasCanceled - sOut = sEditor.dataOrig; - wasAborted = true; + + if isprop(sEditor, 'FinishState') + if sEditor.FinishState == "Finished" + sOut = sEditor.Data; + else + sOut = sEditor.OriginalData; + wasAborted = true; + end else - sOut = sEditor.dataEdit; + if sEditor.wasCanceled + sOut = sEditor.dataOrig; + wasAborted = true; + else + sOut = sEditor.dataEdit; + end end end diff --git a/code/graphics/+applify/+mixin/AppPlugin.m b/code/graphics/+applify/+mixin/AppPlugin.m index 604f2dd14..8b68e4a14 100644 --- a/code/graphics/+applify/+mixin/AppPlugin.m +++ b/code/graphics/+applify/+mixin/AppPlugin.m @@ -262,11 +262,19 @@ function onSettingsEditorResumed(obj) return; end - if ~obj.hSettingsEditor.wasCanceled - obj.settings_ = obj.hSettingsEditor.dataEdit; + if isprop(obj.hSettingsEditor, 'FinishState') + obj.wasAborted = obj.hSettingsEditor.FinishState ~= "Finished"; + if ~obj.wasAborted + obj.settings_ = obj.hSettingsEditor.Data; + end + else + if ~obj.hSettingsEditor.wasCanceled + obj.settings_ = obj.hSettingsEditor.dataEdit; + end + + obj.wasAborted = obj.hSettingsEditor.wasCanceled; end - obj.wasAborted = obj.hSettingsEditor.wasCanceled; delete(obj.hSettingsEditor) obj.hSettingsEditor = []; obj.onSettingsEditorClosed() diff --git a/code/graphics/+applify/+mixin/UserSettings.m b/code/graphics/+applify/+mixin/UserSettings.m index 48716363c..340cc407c 100644 --- a/code/graphics/+applify/+mixin/UserSettings.m +++ b/code/graphics/+applify/+mixin/UserSettings.m @@ -417,12 +417,22 @@ function onSettingsEditorClosed(obj) if ~isvalid(obj.hSettingsEditor); return; end - if obj.hSettingsEditor.wasCanceled - updatedSettings = obj.hSettingsEditor.dataOrig; - obj.wasAborted = true; + if isprop(obj.hSettingsEditor, 'FinishState') + if obj.hSettingsEditor.FinishState == "Finished" + updatedSettings = obj.hSettingsEditor.Data; + obj.wasAborted = false; + else + updatedSettings = obj.hSettingsEditor.OriginalData; + obj.wasAborted = true; + end else - updatedSettings = obj.hSettingsEditor.dataEdit; - obj.wasAborted = false; + if obj.hSettingsEditor.wasCanceled + updatedSettings = obj.hSettingsEditor.dataOrig; + obj.wasAborted = true; + else + updatedSettings = obj.hSettingsEditor.dataEdit; + obj.wasAborted = false; + end end % Delete hSettingsEditor before assigning updated settings. diff --git a/code/modules/+nansen/+module/+general/+core/+sessionmethod/+data/+organize/changeDataLocationRoot.m b/code/modules/+nansen/+module/+general/+core/+sessionmethod/+data/+organize/changeDataLocationRoot.m index 3aeb23c73..4d4626994 100644 --- a/code/modules/+nansen/+module/+general/+core/+sessionmethod/+data/+organize/changeDataLocationRoot.m +++ b/code/modules/+nansen/+module/+general/+core/+sessionmethod/+data/+organize/changeDataLocationRoot.m @@ -80,7 +80,7 @@ S.RootPath = rootPath; S.RootPath_ = allRootPaths; - h = structeditor.App(S, 'AdjustFigureSize', true, ... + h = structeditor(S, 'AdjustFigureSize', true, ... 'Title', 'Edit Data Location Rootpaths', ... 'LabelPosition', 'over', ... 'CustomFigureSize', [700, 300], ... @@ -89,11 +89,17 @@ h.Title = sprintf('Edit Data Location Rootpath'); h.waitfor() - if h.wasCanceled - return + if isprop(h, 'FinishState') + wasCanceled = h.FinishState ~= "Finished"; + sNew = h.Data; else + wasCanceled = h.wasCanceled; sNew = h.dataEdit; - + end + + if wasCanceled + return + else for i = 1:numel(sessionObject) sessionObject(i).updateRootDirPath(dataLocName, sNew.RootPath) end diff --git a/code/modules/+nansen/+module/+general/+core/+tablevariable/+session/DataLocation.m b/code/modules/+nansen/+module/+general/+core/+tablevariable/+session/DataLocation.m index 2f5749599..2073bdc0a 100644 --- a/code/modules/+nansen/+module/+general/+core/+tablevariable/+session/DataLocation.m +++ b/code/modules/+nansen/+module/+general/+core/+tablevariable/+session/DataLocation.m @@ -250,8 +250,8 @@ function onCellDoubleClick(obj, metaObj) % todo for the future %S.(fieldName).Subfolder_ = @(x)uigetdir(rootPath); end - - h = structeditor.App(S, 'AdjustFigureSize', true, ... + + h = structeditor(S, 'AdjustFigureSize', true, ... 'Title', 'Edit Data Location Rootpaths', ... 'LabelPosition', 'over', ... 'CustomFigureSize', [700, 300], ... @@ -260,11 +260,18 @@ function onCellDoubleClick(obj, metaObj) h.Title = sprintf('Edit Data Locations for %s', metaObj.sessionID); h.waitfor() + + if isprop(h, 'FinishState') + wasCanceled = h.FinishState ~= "Finished"; + sNew = h.Data; + else + wasCanceled = h.wasCanceled; + sNew = h.dataEdit; + end - if h.wasCanceled + if wasCanceled return else - sNew = h.dataEdit; if ~isequal(sNew, S) metaObj.updateRootDir(sNew) end @@ -302,19 +309,26 @@ function onCellDoubleClick2(obj, metaObj) S.(fieldName_) = allRootPaths; end - - h = structeditor.App(S, 'AdjustFigureSize', true, ... + + h = structeditor(S, 'AdjustFigureSize', true, ... 'Title', 'Edit Data Location Rootpaths', ... 'LabelPosition', 'over', ... 'Prompt', 'Select datalocation root directories'); h.Title = sprintf('Edit Data Locations for %s', metaObj.sessionID); h.waitfor() + + if isprop(h, 'FinishState') + wasCanceled = h.FinishState ~= "Finished"; + sNew = h.Data; + else + wasCanceled = h.wasCanceled; + sNew = h.dataEdit; + end - if h.wasCanceled + if wasCanceled return else - sNew = h.dataEdit; if ~isequal(sNew, S) metaObj.updateRootDir(sNew) end diff --git a/code/wrappers/+nansen/+plugin/+imviewer/FlowRegistration.m b/code/wrappers/+nansen/+plugin/+imviewer/FlowRegistration.m index 0bbee4b70..3a921b789 100644 --- a/code/wrappers/+nansen/+plugin/+imviewer/FlowRegistration.m +++ b/code/wrappers/+nansen/+plugin/+imviewer/FlowRegistration.m @@ -65,13 +65,19 @@ function delete(obj) sEditor = openSettingsEditor@imviewer.ImviewerPlugin(obj); - % Need a better solution for this: - idx = strcmp(sEditor.Name, 'Export'); - sEditor.dataOrig{idx}.SaveDirectory = folderPath; - sEditor.dataEdit{idx}.SaveDirectory = folderPath; + if ismethod(sEditor, 'replaceData') + settings = sEditor.Data; + settings.Export.SaveDirectory = folderPath; + settings.Export.FileName = fileName; + sEditor.replaceData(settings); + else + idx = strcmp(sEditor.Name, 'Export'); + sEditor.dataOrig{idx}.SaveDirectory = folderPath; + sEditor.dataEdit{idx}.SaveDirectory = folderPath; - sEditor.dataOrig{idx}.FileName = fileName; - sEditor.dataEdit{idx}.FileName = fileName; + sEditor.dataOrig{idx}.FileName = fileName; + sEditor.dataEdit{idx}.FileName = fileName; + end end function openControlPanel(obj) diff --git a/code/wrappers/+nansen/+plugin/+imviewer/NoRMCorre.m b/code/wrappers/+nansen/+plugin/+imviewer/NoRMCorre.m index 442b837b1..745a8504f 100644 --- a/code/wrappers/+nansen/+plugin/+imviewer/NoRMCorre.m +++ b/code/wrappers/+nansen/+plugin/+imviewer/NoRMCorre.m @@ -121,13 +121,19 @@ function assignDefaultOptions(obj) sEditor = openSettingsEditor@imviewer.ImviewerPlugin(obj); - % Need a better solution for this: - idx = strcmp(sEditor.Name, 'Export'); - sEditor.dataOrig{idx}.SaveDirectory = folderPath; - sEditor.dataEdit{idx}.SaveDirectory = folderPath; + if ismethod(sEditor, 'replaceData') + settings = sEditor.Data; + settings.Export.SaveDirectory = folderPath; + settings.Export.FileName = fileName; + sEditor.replaceData(settings); + else + idx = strcmp(sEditor.Name, 'Export'); + sEditor.dataOrig{idx}.SaveDirectory = folderPath; + sEditor.dataEdit{idx}.SaveDirectory = folderPath; - sEditor.dataOrig{idx}.FileName = fileName; - sEditor.dataEdit{idx}.FileName = fileName; + sEditor.dataOrig{idx}.FileName = fileName; + sEditor.dataEdit{idx}.FileName = fileName; + end end function openControlPanel(obj) diff --git a/tests/+nansen/+unittest/+config/FakeOptionsManager.m b/tests/+nansen/+unittest/+config/FakeOptionsManager.m new file mode 100644 index 000000000..63cfe7983 --- /dev/null +++ b/tests/+nansen/+unittest/+config/FakeOptionsManager.m @@ -0,0 +1,126 @@ +classdef FakeOptionsManager < handle + + properties + OptionsName char = 'Preset A' + DefaultName char = 'Preset A' + SavedOptions + SavedName char = '' + SaveCustomOptionsCallCount (1,1) double = 0 + SetDefaultCallCount (1,1) double = 0 + end + + properties (Access = private) + PresetMap containers.Map + CustomMap containers.Map + ModifiedMap containers.Map + end + + properties (Dependent) + PresetOptionNames + CustomOptionNames + EditedOptionNames + AllOptionNames + end + + methods + function obj = FakeOptionsManager() + obj.PresetMap = containers.Map(); + obj.CustomMap = containers.Map(); + obj.ModifiedMap = containers.Map(); + + obj.PresetMap('Preset A') = struct('Value', 1); + obj.PresetMap('Preset B') = struct('Value', 2); + obj.CustomMap('Custom A') = struct('Value', 10); + end + + function names = get.PresetOptionNames(obj) + names = obj.PresetMap.keys(); + end + + function names = get.CustomOptionNames(obj) + names = obj.CustomMap.keys(); + end + + function names = get.EditedOptionNames(obj) + names = obj.ModifiedMap.keys(); + end + + function names = get.AllOptionNames(obj) + names = [obj.PresetOptionNames, obj.CustomOptionNames]; + end + + function options = getOptions(obj, optionsName) + optionsName = char(optionsName); + if obj.PresetMap.isKey(optionsName) + options = obj.PresetMap(optionsName); + elseif obj.CustomMap.isKey(optionsName) + options = obj.CustomMap(optionsName); + else + error('FakeOptionsManager:UnknownOptions', ... + 'Unknown options set "%s".', optionsName) + end + end + + function appendModifiedOptions(obj, opts, name) + obj.ModifiedMap(char(name)) = opts; + end + + function removeModifiedOptions(obj, name) + name = char(name); + if obj.ModifiedMap.isKey(name) + obj.ModifiedMap.remove(name); + end + end + + function options = getModifiedOptions(obj, name) + options = obj.ModifiedMap(char(name)); + end + + function resetModifiedOptions(obj) + obj.ModifiedMap = containers.Map(); + end + + function tf = isModified(obj, name) + tf = obj.ModifiedMap.isKey(char(name)); + end + + function givenName = saveCustomOptions(obj, opts, name) + if nargin < 3 || isempty(name) + name = 'Saved Custom'; + end + + givenName = char(name); + obj.SaveCustomOptionsCallCount = obj.SaveCustomOptionsCallCount + 1; + obj.SavedOptions = opts; + obj.SavedName = givenName; + obj.CustomMap(givenName) = opts; + end + + function setDefault(obj, optionsName) + obj.SetDefaultCallCount = obj.SetDefaultCallCount + 1; + obj.DefaultName = char(optionsName); + end + + function name = getPreferredOptionsName(obj) + name = obj.DefaultName; + end + end + + methods (Static) + function names = formatPresetNames(names) + names = cellfun(@(name) sprintf('[%s]', name), names, 'UniformOutput', false); + end + + function name = formatDefaultName(name) + if iscell(name) + name = cellfun(@(n) strcat(n, ' (Default)'), name, 'UniformOutput', false); + else + name = strcat(name, ' (Default)'); + end + end + + function names = formatEditedNames(names) + % Modified names already include their marker. + end + end +end diff --git a/tests/+nansen/+unittest/+config/OptionsManagerPluginTest.m b/tests/+nansen/+unittest/+config/OptionsManagerPluginTest.m new file mode 100644 index 000000000..cb03527f5 --- /dev/null +++ b/tests/+nansen/+unittest/+config/OptionsManagerPluginTest.m @@ -0,0 +1,101 @@ +classdef OptionsManagerPluginTest < matlab.uitest.TestCase + + methods (TestMethodSetup) + function assumeModernStructEditor(testCase) + testCase.assumeEqual(exist('structeditor.StructEditorApp', 'class'), 8, ... + 'Standalone StructEditor must be on the MATLAB path.') + end + end + + methods (Test) + function testPluginPopulatesDropdown(testCase) + manager = nansen.unittest.config.FakeOptionsManager(); + [editor, plugin] = testCase.createEditor(manager); + + testCase.verifyClass(plugin, 'nansen.manage.OptionsManagerPlugin') + dropDown = plugin.getTestControl('OptionsDropDown'); + + testCase.verifyEqual(dropDown.ItemsData, ... + {'Preset A', 'Preset B', 'Custom A'}) + testCase.verifyEqual(dropDown.Value, 'Preset A') + testCase.verifyEqual(dropDown.Items{1}, '[Preset A] (Default)') + + delete(editor) + end + + function testEditingCreatesModifiedOptions(testCase) + manager = nansen.unittest.config.FakeOptionsManager(); + [editor, plugin] = testCase.createEditor(manager); + controlContainer = editor.getComponent('UIControlContainers'); + + testCase.type(controlContainer.UIControls.Value, 5) + + testCase.verifyEqual(plugin.CurrentOptionsName, 'Preset A (Modified)') + testCase.verifyTrue(manager.isModified('Preset A (Modified)')) + testCase.verifyEqual(manager.getModifiedOptions('Preset A (Modified)').Value, 5) + + delete(editor) + end + + function testSelectingOptionsReplacesEditorData(testCase) + manager = nansen.unittest.config.FakeOptionsManager(); + [editor, plugin] = testCase.createEditor(manager); + dropDown = plugin.getTestControl('OptionsDropDown'); + + testCase.choose(dropDown, '[Preset B]') + + testCase.verifyEqual(editor.Data.Value, 2) + testCase.verifyEqual(plugin.CurrentOptionsName, 'Preset B') + + delete(editor) + end + + function testSaveOptionsPersistsModifiedData(testCase) + manager = nansen.unittest.config.FakeOptionsManager(); + [editor, plugin] = testCase.createEditor(manager); + controlContainer = editor.getComponent('UIControlContainers'); + saveButton = plugin.getTestControl('SaveButton'); + + testCase.type(controlContainer.UIControls.Value, 7) + testCase.press(saveButton) + + testCase.verifyEqual(manager.SaveCustomOptionsCallCount, 1) + testCase.verifyEqual(manager.SavedOptions.Value, 7) + testCase.verifyEqual(plugin.CurrentOptionsName, 'Saved Custom') + testCase.verifyFalse(manager.isModified('Preset A (Modified)')) + + delete(editor) + end + + function testMakeDefaultUpdatesOptionsManager(testCase) + manager = nansen.unittest.config.FakeOptionsManager(); + [editor, plugin] = testCase.createEditor(manager); + dropDown = plugin.getTestControl('OptionsDropDown'); + defaultButton = plugin.getTestControl('DefaultButton'); + + testCase.choose(dropDown, 'Custom A') + testCase.press(defaultButton) + + testCase.verifyEqual(manager.SetDefaultCallCount, 1) + testCase.verifyEqual(manager.DefaultName, 'Custom A') + + delete(editor) + end + end + + methods (Access = private) + function [editor, plugin] = createEditor(testCase, manager) + plugin = nansen.manage.OptionsManagerPlugin(manager, 'Preset A'); + editor = structeditor(manager.getOptions('Preset A'), ... + 'CloseOnExit', false, ... + 'Plugin', plugin); + testCase.addTeardown(@() deleteValid(editor)); + end + end +end + +function deleteValid(obj) + if ~isempty(obj) && isvalid(obj) + delete(obj) + end +end