diff --git a/+ndi/+daq/+system/mfdaq.m b/+ndi/+daq/+system/mfdaq.m index 886f40555..3ad7e6d74 100755 --- a/+ndi/+daq/+system/mfdaq.m +++ b/+ndi/+daq/+system/mfdaq.m @@ -312,7 +312,6 @@ channeltype, channel, ndi_daqsystem_mfdaq_obj.session()); end; end; - end; % methods methods (Static), % functions that don't need the object diff --git a/+ndi/+daq/templates/daq_reader.mtemplate b/+ndi/+daq/templates/daq_reader.mtemplate new file mode 100644 index 000000000..4b0b240e9 --- /dev/null +++ b/+ndi/+daq/templates/daq_reader.mtemplate @@ -0,0 +1,5 @@ +classdef {{ReaderClassName}} < {{ReaderSuperClass}} + + + +end \ No newline at end of file diff --git a/+ndi/+daq/templates/daq_reader_variables.json b/+ndi/+daq/templates/daq_reader_variables.json new file mode 100644 index 000000000..ba7aed104 --- /dev/null +++ b/+ndi/+daq/templates/daq_reader_variables.json @@ -0,0 +1,4 @@ +{ + "ReaderClassName": "", + "ReaderSuperClass": "" +} \ No newline at end of file diff --git a/+ndi/+database/+app/+dataset_viewer/DatasetViewer.mlapp b/+ndi/+database/+app/+dataset_viewer/DatasetViewer.mlapp index 242c2fd3b..227f5d931 100644 Binary files a/+ndi/+database/+app/+dataset_viewer/DatasetViewer.mlapp and b/+ndi/+database/+app/+dataset_viewer/DatasetViewer.mlapp differ diff --git a/+ndi/+database/+metadata_ds_core/+conversion/+internal/createInstance.m b/+ndi/+database/+metadata_ds_core/+conversion/+internal/createInstance.m new file mode 100644 index 000000000..c3169680f --- /dev/null +++ b/+ndi/+database/+metadata_ds_core/+conversion/+internal/createInstance.m @@ -0,0 +1,49 @@ +function openMindsInstance = createInstance(dataStruct, openMindsType) + + arguments + dataStruct (1,1) struct + openMindsType (1,1) string + end + + try + conversionFunctionMap = getConcreteConversionMap(openMindsType); + catch + conversionFunctionMap = struct; + end + + openMindsInstance = feval( openMindsType ); + dataFields = fieldnames(dataStruct); + + for i = 1:numel(dataFields) + [fieldName, propName] = deal( dataFields{i} ); + propName(1) = lower(propName(1)); + + value = dataStruct.(fieldName); + if isempty(value); continue; end % Skip conversion for empty values + + if isa(value, 'char'); value = string(value); end + + if isfield( conversionFunctionMap, propName ) + + conversionFcn = conversionFunctionMap.(propName); + + if iscell(value) + value = cellfun(@(s) conversionFcn(s), value); + + elseif numel(value) > 1 % array conversion + value = arrayfun(@(s) conversionFcn(s), value); + + else + value = conversionFcn(value); + end + else + % Insert value directly + end + + try + openMindsInstance.(propName) = value; + catch ME + % warning(ME.message) + end + end +end diff --git a/+ndi/+database/+metadata_ds_core/+conversion/convertStrains.m b/+ndi/+database/+metadata_ds_core/+conversion/convertStrains.m new file mode 100644 index 000000000..29c003310 --- /dev/null +++ b/+ndi/+database/+metadata_ds_core/+conversion/convertStrains.m @@ -0,0 +1,28 @@ +function [strainInstanceMap] = convertStrains(items) + + import ndi.database.metadata_ds_core.conversion.internal.createInstance + + strainInstanceMap = containers.Map(); + + % Convert items without background strains + for i = 1:numel(items) + thisItem = items(i); + thisItem = rmfield(thisItem, 'backgroundStrain'); + + thisInstance = createInstance(thisItem, 'openminds.core.Strain'); + + strainInstanceMap(thisItem.name) = thisInstance; + end + + % "Recursively" link together background strains + for i = 1:numel(items) + thisItem = items(i); + thisInstance = strainInstanceMap(thisItem.name); + + for j = 1:numel(thisItem.backgroundStrain) + bgStrainName = thisItem.backgroundStrain(j); + bgInstance = strainInstanceMap(bgStrainName); + thisInstance.backgroundStrain(j) = bgInstance; + end + end +end diff --git a/+ndi/+database/+metadata_ds_core/+conversion/getAllStrains.m b/+ndi/+database/+metadata_ds_core/+conversion/getAllStrains.m new file mode 100644 index 000000000..87dd75cb6 --- /dev/null +++ b/+ndi/+database/+metadata_ds_core/+conversion/getAllStrains.m @@ -0,0 +1,10 @@ +function strainInstanceMap = getAllStrains() + + import ndi.database.metadata_app.fun.loadUserInstanceCatalog + import ndi.database.metadata_ds_core.conversion.convertStrains + + strainCatalog = loadUserInstanceCatalog('Strain'); + + % Todo: Adapt conversion to also convert custom species. + strainInstanceMap = convertStrains(strainCatalog.getAll() ); +end diff --git a/+ndi/+dataset/+gui/+enum/FolderLevelType.m b/+ndi/+dataset/+gui/+enum/FolderLevelType.m new file mode 100644 index 000000000..298c0ba40 --- /dev/null +++ b/+ndi/+dataset/+gui/+enum/FolderLevelType.m @@ -0,0 +1,28 @@ +classdef FolderLevelType + + enumeration + Undefined('Undefined') + Subject('Subject') + Session('Session') + Trial('Trial') + Date('Date') + Epoch('Epoch') + end + + properties + Name + DefaultFolderPrefix + end + + methods + function obj = FolderLevelType(name) + obj.Name = name; + switch obj.Name + case 'Subject' + obj.DefaultFolderPrefix = 'subject'; + case 'Session' + obj.DefaultFolderPrefix = 'session'; + end + end + end +end \ No newline at end of file diff --git a/+ndi/+dataset/+gui/+event/UserActionEventData.m b/+ndi/+dataset/+gui/+event/UserActionEventData.m new file mode 100644 index 000000000..c83d64533 --- /dev/null +++ b/+ndi/+dataset/+gui/+event/UserActionEventData.m @@ -0,0 +1,11 @@ +classdef UserActionEventData < event.EventData + properties + UserAction + end + + methods + function obj = UserActionEventData(userAction) + obj.UserAction = userAction; + end + end +end diff --git a/+ndi/+dataset/+gui/+folderorganization/+widget/FolderOrganizationTableToolbar.mlapp b/+ndi/+dataset/+gui/+folderorganization/+widget/FolderOrganizationTableToolbar.mlapp new file mode 100644 index 000000000..d16e27ba2 Binary files /dev/null and b/+ndi/+dataset/+gui/+folderorganization/+widget/FolderOrganizationTableToolbar.mlapp differ diff --git a/+ndi/+dataset/+gui/+folderorganization/FolderOrganizationModel.m b/+ndi/+dataset/+gui/+folderorganization/FolderOrganizationModel.m new file mode 100644 index 000000000..e69de29bb diff --git a/+ndi/+dataset/+gui/+folderorganization/FolderOrganizationPage.m b/+ndi/+dataset/+gui/+folderorganization/FolderOrganizationPage.m new file mode 100644 index 000000000..d3df32221 --- /dev/null +++ b/+ndi/+dataset/+gui/+folderorganization/FolderOrganizationPage.m @@ -0,0 +1,294 @@ +classdef FolderOrganizationPage < ndi.gui.window.wizard.abstract.Page + +% Todo: +% Pass button through the subfolder preview functions? + + properties (Constant) + Name = "Subfolders" + Title = "Folder Organization" + Description = "Specify how subfolders are organized" + end + + properties % UI Data % TODO: move to model? + RootDirectory + PresetFolderModels (1,1) dictionary + end + + properties % Model - View - Controller (MVC pattern) + DataModel (1,1) ndi.dataset.gui.models.FolderOrganizationModel + View (1,1) ndi.dataset.gui.folderorganization.FolderOrganizationTableView + Controller (1,1) ndi.dataset.gui.folderorganization.FolderOrganizationTableController + end + + properties (Access = private) % Custom UI Components + UITable %ndi.dataset.gui.pages.component.FolderOrganizationTable + UIToolbar ndi.dataset.gui.pages.component.FolderOrganizationToolbar % move to controller... + TableGridLayout + end + + properties (Access = private) % Generic UI Components + ContextMenu + SaveMenu + LoadMenu + end + + properties (Access = private) + FolderListViewer + FolderListViewerActive = false + FolderOrganizationFilterListener + end + + methods + function obj = FolderOrganizationPage( ) + obj.DoCreatePanel = true; + end + end + + methods + function set.DataModel(obj, dataModel) + obj.DataModel = dataModel; + if obj.IsInitialized + % Todo: update differently.... + obj.View.update() %#ok + end + end + + function set.RootDirectory(obj, rootDirectory) + obj.RootDirectory = rootDirectory; + if obj.IsInitialized + % Todo(?) Update data model root directory + end + end + + function set.PresetFolderModels(obj, value) + obj.PresetFolderModels = value; + obj.postSetPresetFolderModels() + end + end + + methods (Access = private) + function postSetPresetFolderModels(obj) + if ~isempty( obj.UIToolbar ) + obj.UIToolbar.PresetFolderModels = keys(obj.PresetFolderModels); + end + end + end + + methods (Access = protected) + + function onVisiblePropertyValueSet(obj) + onVisiblePropertyValueSet@ndi.gui.window.wizard.abstract.Page(obj) + + if ~isempty(obj.UIToolbar) + obj.UIToolbar.Visible = obj.Visible; + end + end + + function onPageEntered(obj) + % No action needed + end + + function onPageExited(obj) + % Todo: Close folder viewer if it is open. + end + + function createComponents(obj) + % createComponents - Create components for page. + + defaultTableData = obj.DataModel.getDefaultFolderLevelTable(); + + obj.TableGridLayout = uigridlayout(obj.UIPanel); + obj.TableGridLayout.ColumnWidth={'1x'}; + obj.TableGridLayout.RowHeight={'1x'}; + obj.TableGridLayout.Padding = [20,0,20,0]; + obj.TableGridLayout.BackgroundColor = 'w'; + + parent = obj.TableGridLayout; + %parent = obj.UIPanel; + + obj.UITable = WidgetTable(parent, 'ItemName', 'Folder Level'); + obj.UITable.HeaderBackgroundColor = "#FFFFFF"; + obj.UITable.HeaderForegroundColor = "#002054"; + obj.UITable.BackgroundColor = 'w'; + obj.UITable.ColumnNames = defaultTableData.Properties.CustomProperties.VariableTitle; + obj.UITable.ColumnWidth = {185, 175, 150, 150}; + %obj.UITable.ColumnWidget = {'', '', '', customColumnFcn}; + obj.UITable.TableBorderType = 'none'; + obj.UITable.MinimumColumnWidth = 120; + obj.UITable.ColumnHeaderHelpFcn = @ndi.dataset.gui.getTooltipMessage; + + obj.UITable.setDefaultRowData(defaultTableData) + + obj.View = ndi.dataset.gui.folderorganization.FolderOrganizationTableView(obj.DataModel, obj.UITable); + obj.Controller = ndi.dataset.gui.folderorganization.FolderOrganizationTableController(obj.DataModel, obj.UITable); + obj.View.update() + + obj.UIToolbar = ndi.dataset.gui.pages.component.FolderOrganizationToolbar(obj.ParentApp.BodyGridLayout); + obj.UIToolbar.Layout.Row = 2; + obj.UIToolbar.Layout.Column = 1; + obj.UIToolbar.Theme = obj.ParentApp.Theme; + + % Set callback functions + obj.UIToolbar.MenuButtonPushedFcn = @obj.onMenuButtonPushed; + obj.UIToolbar.ShowFiltersButtonPushedFcn = @obj.onShowFiltersButtonPushed; + obj.UIToolbar.PreviewButtonPushedFcn = @obj.onFolderPreviewButtonClicked; + obj.UIToolbar.SelectTemplateDropDownValueChangedFcn = @obj.onPresetTemplateSelected; + + % Todo: (allow saving a model as a template?) + % Note: This could be a menu on a button next to the template + % dropdown + obj.ContextMenu = uicontextmenu(obj.ParentApp.UIFigure); + + obj.SaveMenu = uimenu(obj.ContextMenu); + obj.SaveMenu.Text = 'Save to template file'; + + obj.LoadMenu = uimenu(obj.ContextMenu); + obj.LoadMenu.Text = 'Load from template file'; + + % Do this last. + obj.loadPresetFolderModels() + end + + function onMenuButtonPushed(obj, event) + pos = getpixelposition(event.Source, true); + obj.ContextMenu.open(pos(1)+12, pos(2)-5) + end + + function onShowFiltersButtonPushed(obj, event) + if event.Value + obj.View.showAdvancedOptions(); + %obj.UITable.showAdvancedOptions() + else + obj.View.hideAdvancedOptions(); + %obj.UITable.hideAdvancedOptions() + end + end + + function onFolderPreviewButtonClicked(obj, event) + %onFolderPreviewButtonClicked Button callback + % + % This callback toggles visibility a figure that displays all the + % folders that are detected using current configuration + + if ~isempty(obj.FolderListViewer) && isvalid(obj.FolderListViewer) + if strcmp(obj.FolderListViewer.Visible, 'on') + obj.FolderListViewerActive = false; + obj.hideFolderListViewer() + event.Source.FontWeight = 'normal'; + elseif strcmp(obj.FolderListViewer.Visible, 'off') + obj.FolderListViewerActive = true; + obj.showFolderListViewer() + event.Source.FontWeight = 'bold'; + end + else + obj.FolderListViewerActive = true; + obj.showFolderListViewer() + event.Source.FontWeight = 'bold'; + end + end + + function onPresetTemplateSelected(obj, event) + filePath = obj.PresetFolderModels( event.Value ); + templateFolderModel = ndi.dataset.gui.models.FolderOrganizationModel.fromJson(filePath); + + oldFolderLevels = obj.DataModel.FolderLevels; + newFolderLevels = templateFolderModel.FolderLevels; + + % Update names based on current selections + for i = 1:numel(newFolderLevels) + if i <= numel(oldFolderLevels) + newFolderLevels(i).Name = oldFolderLevels(i).Name; + end + end + + obj.DataModel.FolderLevels = newFolderLevels; + + obj.View.update() + end + end + + % % % % Methods for the folder listing figure and table + methods (Access = private) + + function createFolderListViewer(obj) + + obj.FolderListViewer = nansen.config.dloc.FolderPathViewer(obj.ParentApp.UIFigure); + obj.FolderListViewer.Theme = obj.ParentApp.Theme; + + addlistener(obj.FolderListViewer, 'ObjectBeingDestroyed', ... + @(s, e) obj.onFolderListViewerDeleted); + + obj.FolderOrganizationFilterListener = listener(obj.DataModel, ... + 'FolderModelChanged', @(s,e) obj.updateFolderList); + + % Give focus to the app figure + figure(obj.ParentApp.UIFigure) + end + + function showFolderListViewer(obj) + + [screenSize, ~] = ndi.gui.utility.getCurrentScreenSize(obj.ParentApp.UIFigure); + obj.ParentApp.UIFigure.Position(1) = screenSize(1) + 10; + + if isempty(obj.FolderListViewer) || ~isvalid(obj.FolderListViewer) + obj.createFolderListViewer() + obj.updateFolderList() + end + + %obj.PreviewButton.ImageSource = nansen.internal.getIconPathName('look3.png'); + obj.FolderListViewer.Visible = 'on'; + pause(0.01) + + if ~isempty(obj.ParentApp.UIFigure) + figure(obj.ParentApp.UIFigure) + end + end + + function hideFolderListViewer(obj) + + if ~isempty(obj.FolderListViewer) && isvalid(obj.FolderListViewer) + %obj.PreviewButton.ImageSource = nansen.internal.getIconPathName('look2.png'); + obj.FolderListViewer.Visible = 'off'; + + if ~isempty(obj.ParentApp.UIFigure) + ndi.gui.utility.centerFigureOnScreen(obj.ParentApp.UIFigure) + end + end + end + + function onFolderListViewerDeleted(obj) + if ~isvalid(obj); return; end + + obj.FolderListViewerActive = false; + %obj.PreviewButton.ImageSource = nansen.internal.getIconPathName('look2.png'); + obj.FolderListViewer = []; + end + + function updateFolderList(obj) + + if isempty(obj.FolderListViewer) || ~isvalid(obj.FolderListViewer) + return + end + + folderPath = obj.DataModel.listAllFolders(); + + if isempty(folderPath); folderPath = {''}; end + + % Remove the root path from the displayed paths. + folderPath = strrep(folderPath, obj.RootDirectory, sprintf('')); + + % Update table data in folderlist viewer + obj.FolderListViewer.Data = folderPath'; + end + + function loadPresetFolderModels(obj) + rootDirectory = fileparts( fileparts (mfilename('fullpath') ) ); + L = dir( fullfile(rootDirectory, 'resources', 'preset', '*.json') ); + + for i = 1:numel(L) + [~, name] = fileparts(L(i).name); + obj.PresetFolderModels(name) = fullfile(L(i).folder, L(i).name); + end + end + end +end \ No newline at end of file diff --git a/+ndi/+dataset/+gui/+folderorganization/FolderOrganizationTableController.m b/+ndi/+dataset/+gui/+folderorganization/FolderOrganizationTableController.m new file mode 100644 index 000000000..e214c8d08 --- /dev/null +++ b/+ndi/+dataset/+gui/+folderorganization/FolderOrganizationTableController.m @@ -0,0 +1,106 @@ +classdef FolderOrganizationTableController < handle +% FolderOrganizationTableController - Controller class for folder organization table +% +% This class should respond to a user's actions in the FolderOrganization +% page and update the FolderOrganization model accordingly. + +% Todo: +% [ ] Create the table toolbar and also assign its callbacks. +%  [ ] Store the view handle as a controller property + + + properties (Access = private) + % UITable - Handle object of the UITable representing the folder levels + UITable + end + + properties (Access = private) + % DataModel - Handle of a FolderOrganizationModel object. + DataModel + end + + methods % Constructor + function obj = FolderOrganizationTableController(dataModel, uiTable) + if ~nargin; return; end + + % Assign controller dependencies + obj.DataModel = dataModel; + obj.UITable = uiTable; + + % Assign controller callbacks + obj.UITable.CellEditedFcn = @obj.onTableCellValueChanged; + obj.UITable.AddRowFcn = @obj.onAddSubfolderLevelButtonPushed; + obj.UITable.RowRemovedFcn = @obj.onSubfolderLevelRemoved; + end + end + + methods (Access = private) + % Callback handler for value change in FolderLevel table. + function onTableCellValueChanged(obj, ~, evt) + rowInd = evt.Indices(1); + varName = evt.ColumnName; + newValue = evt.NewData; + + % Process input + switch varName + case 'Expression' + newValue = numbersign2expression(evt.NewData); + + case 'IgnoreList' + newValue = strtrim( strsplit(newValue, ',') ); + newValue = newValue(~cellfun('isempty', newValue)); + newValue = string(newValue); + + case 'Name' % Make sure "placeholder" value does not end up in model + if strcmp(newValue, ''}, subFolderOptions]; %#ok + + if isempty(char(S(i).Name)) + S(i).Name = subFolderOptions{1}; + elseif ~any(strcmp(S(i).Name, subFolderOptions)) + S(i).Name = subFolderOptions{1}; + end + S(i).Name = categorical({char(S(i).Name)}, subFolderOptions); + + % Update ignorelist to comma separated char + S(i).IgnoreList = strjoin( S(i).IgnoreList, ', '); + end + + if ~obj.ShowAdvancedOptions + %S = rmfield(S, {'Expression', 'IgnoreList'}); + end + + % Assign the struct array as the tables data... + obj.UITable.updateData(S) + + % Only allow removal of the last folder level. + numFolderLevels = numel(obj.DataModel.FolderLevels); + obj.UITable.disableRemoveRowButton(1:numFolderLevels-1) + obj.UITable.enableRemoveRowButton(numFolderLevels) + end + end +end \ No newline at end of file diff --git a/+ndi/+dataset/+gui/+models/DaqSystemCollection.m b/+ndi/+dataset/+gui/+models/DaqSystemCollection.m new file mode 100644 index 000000000..e1822d469 --- /dev/null +++ b/+ndi/+dataset/+gui/+models/DaqSystemCollection.m @@ -0,0 +1,41 @@ +classdef DaqSystemCollection < ndi.internal.mixin.JsonSerializable + + properties (Constant) + VERSION = "1.0.0" + DESCRIPTION = "NDI DAQ System Configuration Collection" % Catalog? + end + + properties + DaqSystems (1,:) ndi.setup.DaqSystemConfiguration + end + + methods % Catalog-like methods... + function addDaqSystem(obj) + obj.DaqSystems(end+1) = ndi.setup.DaqSystemConfiguration(); + + end + + function removeDaqSystem(obj, index) + obj.DaqSystems(index) = []; + end + end + + methods (Access = protected) + % function fromStruct(obj, S) + % + % end + + function tf = isInitialized(obj) + % isInitialized - Is data initialized? + tf = ~isempty(obj.DaqSystems); + end + + end + + methods (Static) + function obj = fromJson(jsonStr) + className = mfilename('class'); + obj = fromJson@ndi.internal.mixin.JsonSerializable(jsonStr, className); + end + end +end \ No newline at end of file diff --git a/+ndi/+dataset/+gui/+models/DatasetInfo.m b/+ndi/+dataset/+gui/+models/DatasetInfo.m new file mode 100644 index 000000000..12734ebe1 --- /dev/null +++ b/+ndi/+dataset/+gui/+models/DatasetInfo.m @@ -0,0 +1,23 @@ +classdef DatasetInfo < handle + + properties + DatasetTitle (1,1) string + DatasetRootPathLog (1,:) string + end + + properties (SetObservable) + DatasetRootPath (1,1) string + end + + methods + function tf = isClean(obj, filePath) + if isfile(filePath) + S = load(filePath); + tf = strcmp(S.DatasetInformation.DatasetTitle, obj.DatasetTitle); + else + tf = false; + end + end + end + +end \ No newline at end of file diff --git a/+ndi/+dataset/+gui/+models/FolderLevel.m b/+ndi/+dataset/+gui/+models/FolderLevel.m new file mode 100644 index 000000000..bc8cfab32 --- /dev/null +++ b/+ndi/+dataset/+gui/+models/FolderLevel.m @@ -0,0 +1,34 @@ +classdef FolderLevel < matlab.mixin.SetGet + + properties + Name (1,1) string = "" + Type (1,1) ndi.dataset.gui.enum.FolderLevelType + IgnoreList (1,:) string + Expression (1,1) string + FolderNamePrefix (1,1) string = "" + end + + methods % Constructor + function obj = FolderLevel(propertyValues) + arguments + propertyValues.?ndi.dataset.gui.models.FolderLevel + end + obj.set(propertyValues) + end + end + + methods + function getFolderPath(obj, rootPathName, id) + % Get path of subfolder given a root directory path and a folder id + end + + function createFolder(obj, rootPathName, id) + % Create a subfolder in a root directory with a given id + end + + function listFolders(obj, rootPathName) + % List all folders in the rootDirectory + end + end + +end \ No newline at end of file diff --git a/+ndi/+dataset/+gui/+models/FolderOrganizationModel.m b/+ndi/+dataset/+gui/+models/FolderOrganizationModel.m new file mode 100644 index 000000000..7f29a8c46 --- /dev/null +++ b/+ndi/+dataset/+gui/+models/FolderOrganizationModel.m @@ -0,0 +1,249 @@ +classdef FolderOrganizationModel < ndi.internal.mixin.JsonSerializable & matlab.mixin.SetGet + +% Todo: Rename to FolderTreeModel + + properties (Constant) % ndi.internal.mixin.JsonSerializable + VERSION = "1.0.0" + DESCRIPTION = "NDI Folder Organization Model" + end + + properties (Transient) + RootDirectory + end + + properties (Transient, Dependent) % or setaccess immutable... + %PresetFolderModels + end + + properties + % FolderLevels - A list of FolderLevels objects + FolderLevels (1,:) ndi.dataset.gui.models.FolderLevel + + % SubFolders - A list of names (string) for subfolders located + % within a session / data folder. Currently no functional role. + SubFolders (1,:) string + end + + events + FolderLevelChanged + FolderLevelAdded + FolderLevelRemoved + + % OR: + FolderModelChanged + end + + methods % Constructor + function obj = FolderOrganizationModel(propertyValues) + arguments + propertyValues.?ndi.dataset.gui.models.FolderOrganizationModel + end + + obj.set(propertyValues) + end + end + + methods % public methods + function assertExistSubfolders(obj) + numFolderLevels = numel(obj.FolderLevels); + subFolderOptions = obj.listFoldersAtDepth(numFolderLevels+1); + + assert( ~isempty(subFolderOptions), 'No subfolders exist at the given folder level' ) + end + + function folderRelativePath = getExampleFolder(obj) + folderRelativePath = fullfile(obj.FolderLevels.Name); + end + + function [folderLevel, folderLevelIdx] = getFolderLevel(obj, folderLevelType) + arguments + obj + folderLevelType (1,1) ndi.dataset.gui.enum.FolderLevelType + end + + folderLevelIdx = find( [obj.FolderLevels.Type] == folderLevelType); + folderLevel = obj.FolderLevels(folderLevelIdx); + end + + function value = getFolderLevelType(obj) + value = [obj.FolderLevels.Type]; + end + + function setFolderLevelType(obj, value) + for i = 1:numel(obj.FolderLevels) + obj.FolderLevels(i).Type = value(i); + end + end + + function value = getSubfolders(obj, options) + arguments + obj + options.Type (1,1) ndi.dataset.gui.enum.FolderLevelType + options.Id (1,:) string = missing + end + + %value = unique(obj.SubFolders); + end + + function addSubFolderLevel(obj, propertyValues) + arguments + obj + propertyValues.?ndi.dataset.gui.models.FolderLevel %#ok + end + + nvPairs=namedargs2cell(propertyValues); + obj.FolderLevels(end+1) = ndi.dataset.gui.models.FolderLevel(nvPairs{:}); + obj.notify('FolderModelChanged') + end + + function removeSubfolderLevel(obj, folderLevelIdx) + obj.FolderLevels(folderLevelIdx) = []; + obj.notify('FolderModelChanged') + end + + function updateFolderLevel(obj, folderLevelIdx, propertyName, newValue) + + obj.FolderLevels(folderLevelIdx).(propertyName) = newValue; + + % Todo: eventdata?? + + obj.notify('FolderModelChanged') + end + + function addSubfolders(obj, subFolderNames) + subFolderNames = reshape(subFolderNames, 1, []); + obj.SubFolders = [obj.SubFolders, subFolderNames]; + end + end + + methods + function S = getFolderLevelStruct(obj) + + S = struct.empty; + + % Todo: Assign fieldnames and loop... + warnState = warning('off', 'MATLAB:structOnObject'); + for i = 1:numel(obj.FolderLevels) + if i == 1 + S = struct(obj.FolderLevels(i)); + else + S(i) = struct(obj.FolderLevels(i)); + end + end + warning(warnState) + if ~isempty(S) + S = rmfield(S, 'FolderNamePrefix'); + end + end + + function updateFolderLevelFromStruct(obj, S) + obj.FolderLevels(:) = []; % Delete existing + + for i = 1:numel(S) + nvPairs = namedargs2cell(S(i)); + obj.addSubFolderLevel(nvPairs{:}) + end + end + + function folderPath = listAllFolders(obj) + % listAllFolders - List all folders in a root directory based on model + folderPath = string.empty; + + S = obj.getFolderLevelStruct(); + if isempty(S); return; end + + folderPath = cellstr(obj.RootDirectory); + + for i = 1:numel(S) + + % Look for subfolders in the folderpath + folderPath = recursiveDir(folderPath, ... + 'Expression', S(i).Expression, ... + 'Ignore', S(i).IgnoreList, ... + 'Type', 'folder', ... + 'RecursionDepth', 1, ... + 'OutputType', 'FilePath'); + end + end + end + + + % Methods of folder model + methods (Access = private) + function folderPath = getFolderPathAtDepth(obj, subfolderDepth) + % getFolderAtDepth - Get path name for the subfolder at given depth + + rootDirectoryPath = obj.RootDirectory; + numFolderLevels = numel(obj.FolderLevels); + + % Todo: Assert subfolderDepth is not deeper than number of subfo + assert(subfolderDepth <= numFolderLevels, ... + 'Subfolder depth must be less than or equal to %d', numFolderLevels) + + if subfolderDepth >= 0 && ~isempty(rootDirectoryPath) + folderPath = rootDirectoryPath; + + for iLevel = 1:subfolderDepth % Get folderpath from data struct... + if isempty( char(obj.FolderLevels(iLevel).Name) ) + error('NDI:FolderModel:FolderNameIsEmpty', 'Folder name is not specified for folder level %d', iLevel) + end + folderPath = fullfile(folderPath, obj.FolderLevels(iLevel).Name); + end + else + folderPath = ''; + end + end + end + + methods (Access = public) + function folderNames = listFoldersAtDepth(obj, subfolderDepth) + % listFoldersAtDepth - List all folders at a given depth which pass filters + S = obj.getFolderLevelStruct(); + + rootFolderPath = obj.getFolderPathAtDepth(subfolderDepth-1); + + if subfolderDepth <= numel(obj.FolderLevels) + expression = S(subfolderDepth).Expression; + ignoreList = S(subfolderDepth).IgnoreList; + else + expression = ""; + ignoreList = string.empty; + end + + % Look for subfolders in the folderpath + L = recursiveDir(rootFolderPath, ... + 'Expression', expression, ... + 'Ignore', ignoreList, ... + 'Type', 'folder', ... + 'RecursionDepth', 1); + + folderNames = {L.name}; + end + end + + methods (Access = protected) + % function fromStruct(obj, S) + % + % end + end + + methods (Static) + function T = getDefaultFolderLevelTable() + folderLevel = ndi.dataset.gui.models.FolderLevel(); + warnState = warning('off', 'MATLAB:structOnObject'); + S = struct(folderLevel); + warning(warnState) + S = rmfield(S, 'FolderNamePrefix'); + T = struct2table(S, "AsArray", true); + T = addprop(T, 'VariableTitle', 'variable'); + T.Properties.CustomProperties.VariableTitle = {'Select subfolder example', 'Set subfolder type', 'Exclusion list', 'Inclusion list'}; + end + end + + methods (Static) + function obj = fromJson(jsonStr) + className = mfilename('class'); + obj = fromJson@ndi.internal.mixin.JsonSerializable(jsonStr, className); + end + end +end \ No newline at end of file diff --git a/+ndi/+dataset/+gui/+pages/+component/DAQSystemEditButtons.mlapp b/+ndi/+dataset/+gui/+pages/+component/DAQSystemEditButtons.mlapp new file mode 100644 index 000000000..869aadaa9 Binary files /dev/null and b/+ndi/+dataset/+gui/+pages/+component/DAQSystemEditButtons.mlapp differ diff --git a/+ndi/+dataset/+gui/+pages/+component/DaqSystemsTable.m b/+ndi/+dataset/+gui/+pages/+component/DaqSystemsTable.m new file mode 100644 index 000000000..ffadcbc55 --- /dev/null +++ b/+ndi/+dataset/+gui/+pages/+component/DaqSystemsTable.m @@ -0,0 +1,334 @@ +classdef DaqSystemsTable < applify.apptable +% Class interface for editing/configuring DAQ System in a uifigure + + % properties / app states depending on outside values: + + properties (Constant) + COLUMN_NAMES = {'', 'Name', 'Select Data Source', 'Select Data Reader', 'File Parameters', '', ''} + COLUMN_WIDTHS = [22, 100, 175, 130, 120, 22, 22] + end + + properties + EditDaqSystemButtonPushedFcn + end + + properties + DaqSystemSpec % stuct/table/object representation of whats in table.. + end + + properties + AppFigure % Todo: make sure this is set... + end + + properties + IsDirty = false % Flag to show if data has changed. + IsAdvancedView = true + IsUpdating = false % Flag to disable event notification when table is being updated. + end + + methods % Structors + function obj = DaqSystemsTable(daqSystemConfiguration, varargin) + %FolderOrganizationTable Construct a FolderOrganizationTable instance + warning('off', 'MATLAB:structOnObject') + data = struct( daqSystemConfiguration ); + warning('on', 'MATLAB:structOnObject') + varargin = [{'Data', data}, varargin]; + obj@applify.apptable(varargin{:}) + + obj.AppFigure = ancestor(obj.Parent, 'figure'); + end + + function delete(obj) + end + + end + + methods (Access = protected) % Implementation of superclass methods + + function assignDefaultTablePropertyValues(obj) + obj.ColumnNames = obj.COLUMN_NAMES; + obj.ColumnWidths = obj.COLUMN_WIDTHS; + obj.ColumnHeaderHelpFcn = @ndi.dataset.gui.getTooltipMessage; + obj.RowSpacing = 20; + end + + function hRow = createTableRowComponents(obj, rowData, rowNum) + + hRow = struct(... + 'RemoveImage', obj.createRemoveRowButton(rowNum, 1), ... + 'NameEditField', obj.createDaqSystemNameEditfield(rowNum, 2), ... + 'DaqSystemDropdown', obj.createDaqSystemSelectionDropdown(rowNum, 3), ... + 'DaqReaderDropdown', obj.createDaqReaderSelectionDropdown(rowNum, 4), ... + 'FileParametersEditfield', obj.createFileParametersEditfield(rowNum, 5), ... + 'EditButton', obj.createEditDaqSystemButton(rowNum, 6), ... + 'AddImage', obj.createAddRowButton(rowNum, 7) ... + ); + + if obj.NumRows == 0; hRow.RemoveImage.Enable = 'off'; end + obj.updateRowComponentValues(hRow, rowData, rowNum) + end + + function updateRowComponentValues(obj, rowComponents, rowData, rowNum) + % updateRowComponentValues - Update component values from data + + if isempty(rowData.DaqReaderClass) + rowComponents.DaqReaderDropdown.Value = ''}, ndi.setup.daq.listDaqReaders()]; + hDropdown.Items = [{'', ''}, daqSystemNames]; + + dependentData = categorical(names(1), names); + src.updateCellValue(evt.Indices(1), 2, dependentData) + + case 2 % DAQ system name + idx = evt.Indices(1); + + if strcmp(char(evt.NewData), '') + [fileName, folder] = uigetfile('*.json'); + if fileName==0 + newValue = '') + %pass. Todo: reset + + else + obj.PageData.DaqSystems(idx) = ... + ndi.setup.DaqSystemConfiguration.fromDeviceName(evt.NewData); + end + + + % Load daq system from file... + + % Todo: Update daqreader? + end + end + + function onDAQSystemAdded(obj, srv, evt) + obj.PageData.DaqSystems(evt.RowIndex) = ndi.setup.DaqSystemConfiguration(); + end + + function onDAQSystemRemoved(obj, srv, evt) + obj.PageData.DaqSystems(evt.RowIndex) = []; + end + + function postSetPageData(obj) + end + end + + methods (Static) + function daqData = getDefaultDAQConfigTable() + + daqData = struct(... + 'DAQSystemType', "EPhys", ... + 'Name', "", ... + 'DAQReader', "n-trode", ... + 'Edit', true); + + %todo + daqSystemTypes = {... + '', ''}, daqSystemNames]; + + daqData.Name = categorical(names(1), names); + daqData.DAQSystemType = categorical({'' + daqSystemNames = ... + ndi.setup.daq.system.listDaqSystemNames(); + + case '2-Photon-Microscope' + daqSystemNames = {'ScanImage', 'PrairieView'}; + + case 'Multifunction DAQ' + daqSystemNames = {}; + + case 'Electrophysiology' + daqSystemNames = ... + ndi.setup.daq.system.listDaqSystemNames(); + end + end + + function tbl = convertDaqSystemObjectToTable(daqSystemConfig) + tbl = ndi.dataset.gui.pages.DaqSystemsPage.getDefaultDAQConfigTable(); + + numItems = numel(daqSystemConfig); + tbl = repmat(tbl, numItems, 1); + + [daqReaderTypes, ~, fcnNames] = ndi.setup.daq.listDaqReaders(); + + for i = 1:numItems + if daqSystemConfig(i).Name == "" + tbl{i, "Name"} = "