From 2f03a0fc522bb064352fe86a5bf003fb0131ec2d Mon Sep 17 00:00:00 2001 From: ehennestad Date: Fri, 7 Jun 2024 20:28:53 +0200 Subject: [PATCH 01/24] Fix indentation --- +ndi/+daq/+system/mfdaq.m | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/+ndi/+daq/+system/mfdaq.m b/+ndi/+daq/+system/mfdaq.m index 24e893fce..5e45936b6 100755 --- a/+ndi/+daq/+system/mfdaq.m +++ b/+ndi/+daq/+system/mfdaq.m @@ -23,7 +23,7 @@ classdef mfdaq < ndi.daq.system - properties (GetAcces=public,SetAccess=protected) + properties (GetAccess=public,SetAccess=protected) end properties (Access=private) % potential private variables end @@ -47,13 +47,13 @@ % functions that override ndi.epoch.epochset - function ec = epochclock(ndi_daqsystem_mfdaq_obj, epoch) - % EPOCHCLOCK - return the ndi.time.clocktype objects for an epoch - % - % EC = EPOCHCLOCK(NDI_DAQSYSTEM_MFDAQ_OBJ, EPOCH) - % - % Return the clock types available for this epoch as a cell array - % of ndi.time.clocktype objects (or sub-class members). + function ec = epochclock(ndi_daqsystem_mfdaq_obj, epoch) + % EPOCHCLOCK - return the ndi.time.clocktype objects for an epoch + % + % EC = EPOCHCLOCK(NDI_DAQSYSTEM_MFDAQ_OBJ, EPOCH) + % + % Return the clock types available for this epoch as a cell array + % of ndi.time.clocktype objects (or sub-class members). % % For the generic ndi.daq.system.mfdaq, this returns a single clock % type 'dev_local'time'; @@ -292,7 +292,7 @@ end; end; % readevents_epochsamples - function sr = samplerate(ndi_daqsystem_mfdaq_obj, epoch, channeltype, channel) + function sr = samplerate(ndi_daqsystem_mfdaq_obj, epoch, channeltype, channel) % SAMPLERATE - GET THE SAMPLE RATE FOR SPECIFIC CHANNEL % % SR = SAMPLERATE(DEV, EPOCH, CHANNELTYPE, CHANNEL) From 411d1fc970a90f8f70658048a31d07ad953deac9 Mon Sep 17 00:00:00 2001 From: ehennestad Date: Mon, 17 Jun 2024 12:09:52 +0200 Subject: [PATCH 02/24] Add functions for listing DAQ system component classes --- +ndi/+setup/+daq/+system/listDaqSystemNames.m | 26 +++++++++++++---- +ndi/+setup/+daq/listDaqEpochProbemapClass.m | 3 ++ +ndi/+setup/+daq/listDaqMetadataReaders.m | 22 +++++++++++++++ +ndi/+setup/+daq/listDaqReaders.m | 28 +++++++++++++++++++ +ndi/+setup/+daq/listDaqSystemClasses.m | 16 +++++++++++ +ndi/+util/getPackageDir.m | 8 ++++++ 6 files changed, 97 insertions(+), 6 deletions(-) create mode 100644 +ndi/+setup/+daq/listDaqEpochProbemapClass.m create mode 100644 +ndi/+setup/+daq/listDaqMetadataReaders.m create mode 100644 +ndi/+setup/+daq/listDaqReaders.m create mode 100644 +ndi/+setup/+daq/listDaqSystemClasses.m create mode 100644 +ndi/+util/getPackageDir.m diff --git a/+ndi/+setup/+daq/+system/listDaqSystemNames.m b/+ndi/+setup/+daq/+system/listDaqSystemNames.m index cb711305e..208c9b6b1 100644 --- a/+ndi/+setup/+daq/+system/listDaqSystemNames.m +++ b/+ndi/+setup/+daq/+system/listDaqSystemNames.m @@ -1,14 +1,28 @@ -function daqSystemNames = listDaqSystemNames(labName) +function [daqSystemNames, daqSystemConfigFiles] = listDaqSystemNames(labName) % listDaqSystemNames - List names of pre-configured DAQ systems for a lab - ndi.globals; - importDir = fullfile(ndi_globals.path.commonpath, 'daq_systems', labName); + arguments + labName (1,1) string = missing + end + + ndi.globals; + rootPath = fullfile(ndi_globals.path.commonpath, 'daq_systems'); + + if ~ismissing(labName) + importDir = fullfile(ndi_globals.path.commonpath, 'daq_systems', labName); - if ~isfolder(importDir) - error('No DAQ systems were found for "%s"', labName) + if ~isfolder(importDir) + error('No DAQ systems were found for "%s"', labName) + end + L = dir(fullfile(importDir, '*.json')); + else + L = recursiveDir(rootPath, 'FileType', 'json'); end - L = dir(fullfile(importDir, '*.json')); [~, daqSystemNames] = fileparts({L.name}); + + if nargout == 2 + daqSystemConfigFiles = arrayfun(@(s) fullfile(s.folder, s.name), L, 'uni', 0); + end end diff --git a/+ndi/+setup/+daq/listDaqEpochProbemapClass.m b/+ndi/+setup/+daq/listDaqEpochProbemapClass.m new file mode 100644 index 000000000..ad1d4f5b1 --- /dev/null +++ b/+ndi/+setup/+daq/listDaqEpochProbemapClass.m @@ -0,0 +1,3 @@ +function names = listDaqEpochProbemapClass() + names = {'epochprobemap_daqsystem'}; +end \ No newline at end of file diff --git a/+ndi/+setup/+daq/listDaqMetadataReaders.m b/+ndi/+setup/+daq/listDaqMetadataReaders.m new file mode 100644 index 000000000..5b2772f20 --- /dev/null +++ b/+ndi/+setup/+daq/listDaqMetadataReaders.m @@ -0,0 +1,22 @@ +function [names, absPaths] = listDaqMetadataReaders() + + % Collect paths of all root folders containing daq mdreaders in a cell array + rootPath = {}; + rootPath{1} = ndi.util.getPackageDir('ndi.daq.metadatareader'); + rootPath{2} = ndi.util.getPackageDir('ndi.setup.daq.metadatareader'); + + % Find all m-files in these root folders. + fileExtension = '.m'; + fileList = recursiveDir(rootPath, 'Type', 'file', 'FileType', fileExtension); + + % Add generic metadata reader: + pathStr = which('ndi.daq.metadatareader'); + fileList = [dir(pathStr); fileList]; + + names = {fileList.name}; + names = strrep(names, fileExtension, ''); + + if nargout >= 2 + absPaths = abspath(fileList); % Todo: Add dependency + end +end diff --git a/+ndi/+setup/+daq/listDaqReaders.m b/+ndi/+setup/+daq/listDaqReaders.m new file mode 100644 index 000000000..aebf5096e --- /dev/null +++ b/+ndi/+setup/+daq/listDaqReaders.m @@ -0,0 +1,28 @@ +function [names, absPaths, functionNames] = listDaqReaders() + + % Collect paths of all root folders containing daq readers in a cell array + rootPath = {}; + rootPath{1} = ndi.util.getPackageDir('ndi.daq.reader'); + rootPath{2} = ndi.util.getPackageDir('ndi.setup.daq.reader'); + + % Find all m-files in these root folders. + fileExtension = '.m'; + fileList = recursiveDir(rootPath, 'Type', 'file', 'FileType', fileExtension); + + absPaths = abspath(fileList); + names = {fileList.name}; + names = strrep(names, fileExtension, ''); + + if nargout < 2 + clear absPath + end + + if nargout == 3 + functionNames = utility.path.abspath2funcname(absPaths); + end + + % Todo: Return a struct, with following fields: + % full path + % package/full function name + % category, i.e mfdaq, tplsm, ephys etc... +end \ No newline at end of file diff --git a/+ndi/+setup/+daq/listDaqSystemClasses.m b/+ndi/+setup/+daq/listDaqSystemClasses.m new file mode 100644 index 000000000..732aaa353 --- /dev/null +++ b/+ndi/+setup/+daq/listDaqSystemClasses.m @@ -0,0 +1,16 @@ +function [names, absPaths] = listDaqSystemClasses() + + rootPath = fileparts( which('ndi.version') ); + rootPath = fullfile(rootPath, '+daq', '+system'); + + fileExtension = '.m'; + fileList = recursiveDir(rootPath, 'Type', 'file', 'FileType', fileExtension); + + absPaths = abspath(fileList); + names = {fileList.name}; + names = strrep(names, fileExtension, ''); + + if nargout < 2 + clear absPath + end +end \ No newline at end of file diff --git a/+ndi/+util/getPackageDir.m b/+ndi/+util/getPackageDir.m new file mode 100644 index 000000000..ebd7cb4ab --- /dev/null +++ b/+ndi/+util/getPackageDir.m @@ -0,0 +1,8 @@ +function pathName = getPackageDir(packageName) + s = what(strrep(packageName, '.', filesep)); + if isempty(s) + error('No path was found for package "%s"', packageName) + else + pathName = s.path; + end +end \ No newline at end of file From ef07d4eaa93fdaa0c5e63a8115c4e454a5b0a9d1 Mon Sep 17 00:00:00 2001 From: ehennestad Date: Mon, 17 Jun 2024 14:02:29 +0200 Subject: [PATCH 03/24] Add dependencies as utility functions and use correct function calls --- +ndi/+setup/+daq/listDaqMetadataReaders.m | 2 +- +ndi/+setup/+daq/listDaqReaders.m | 4 +- +ndi/+setup/+daq/listDaqSystemClasses.m | 2 +- +ndi/+util/abspath2funcname.m | 59 +++++++++++++++++++++++ +ndi/+util/dir2abspath.m | 9 ++++ 5 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 +ndi/+util/abspath2funcname.m create mode 100644 +ndi/+util/dir2abspath.m diff --git a/+ndi/+setup/+daq/listDaqMetadataReaders.m b/+ndi/+setup/+daq/listDaqMetadataReaders.m index 5b2772f20..83c6d5c64 100644 --- a/+ndi/+setup/+daq/listDaqMetadataReaders.m +++ b/+ndi/+setup/+daq/listDaqMetadataReaders.m @@ -17,6 +17,6 @@ names = strrep(names, fileExtension, ''); if nargout >= 2 - absPaths = abspath(fileList); % Todo: Add dependency + absPaths = ndi.util.dir2abspath(fileList); end end diff --git a/+ndi/+setup/+daq/listDaqReaders.m b/+ndi/+setup/+daq/listDaqReaders.m index aebf5096e..087d6bdf4 100644 --- a/+ndi/+setup/+daq/listDaqReaders.m +++ b/+ndi/+setup/+daq/listDaqReaders.m @@ -9,7 +9,7 @@ fileExtension = '.m'; fileList = recursiveDir(rootPath, 'Type', 'file', 'FileType', fileExtension); - absPaths = abspath(fileList); + absPaths = ndi.util.dir2abspath(fileList); names = {fileList.name}; names = strrep(names, fileExtension, ''); @@ -18,7 +18,7 @@ end if nargout == 3 - functionNames = utility.path.abspath2funcname(absPaths); + functionNames = ndi.util.abspath2funcname(absPaths); end % Todo: Return a struct, with following fields: diff --git a/+ndi/+setup/+daq/listDaqSystemClasses.m b/+ndi/+setup/+daq/listDaqSystemClasses.m index 732aaa353..20d23f292 100644 --- a/+ndi/+setup/+daq/listDaqSystemClasses.m +++ b/+ndi/+setup/+daq/listDaqSystemClasses.m @@ -6,7 +6,7 @@ fileExtension = '.m'; fileList = recursiveDir(rootPath, 'Type', 'file', 'FileType', fileExtension); - absPaths = abspath(fileList); + absPaths = ndi.util.dir2abspath(fileList); names = {fileList.name}; names = strrep(names, fileExtension, ''); diff --git a/+ndi/+util/abspath2funcname.m b/+ndi/+util/abspath2funcname.m new file mode 100644 index 000000000..8b75c21b7 --- /dev/null +++ b/+ndi/+util/abspath2funcname.m @@ -0,0 +1,59 @@ +function functionName = abspath2funcname(pathStr) +%abspath2funcname Get function name for .m file given as absolute pathstring +% +% Returns package-prefixed function name given the absolute path of a .m +% file. pathStr can be a character vector, a cell array of character +% vectors or a string array. If the input is an array, the output will be +% a cell array +% +% Syntax: +% functionName = ndi.util.abspath2funcname(pathStr) + + if isa(pathStr, 'cell') + functionName = cellfun(@(c) ndi.util.abspath2funcname(c), ... + pathStr, 'UniformOutput', false); + return + elseif isa(pathStr, 'string') && numel(pathStr) > 1 + functionName = arrayfun(@(str) ndi.util.abspath2funcname(str), ... + pathStr, 'UniformOutput', false); + return + end + + % Get function name, taking package into account + [folderPath, functionName, ext] = fileparts(pathStr); + + assert(strcmp(ext, '.m'), 'pathStr must point to a .m (function) file') + + packageName = pathstr2packagename(folderPath); + functionName = strcat(packageName, '.', functionName); +end + +function packageName = pathstr2packagename(pathStr) +%pathstr2packagename Convert a path string to a string with name of package +% +% packageName = pathstr2packagename(pathStr) +% +% EXAMPLE: +% +% pathStr = +% '/Users/username/Documents/MATLAB/NDI-matlab/+ndi/+session' +% +% packageName = pathstr2packagename(pathStr) +% +% packageName = +% 'ndi.session' + + assert(isfolder(pathStr), 'Path must point to a folder.') + + % Split pathstr by foldernames + splitFolderNames = strsplit(pathStr, filesep); + + % Find all folders that are a package + isPackage = cellfun(@(str) strncmp(str, '+', 1), splitFolderNames ); + + % Create output string + packageFolderNames = splitFolderNames(isPackage); + packageFolderNames = strrep(packageFolderNames, '+', ''); + + packageName = strjoin(packageFolderNames, '.'); +end diff --git a/+ndi/+util/dir2abspath.m b/+ndi/+util/dir2abspath.m new file mode 100644 index 000000000..ac6b5c538 --- /dev/null +++ b/+ndi/+util/dir2abspath.m @@ -0,0 +1,9 @@ +function absolutePathList = abspath(folderContentList) +%abspath Combine folder and name for each element in a folderContent struct array + + absolutePathList = cell(size(folderContentList)); + for i = 1:numel(folderContentList) + absolutePathList{i} = fullfile(folderContentList(i).folder, ... + folderContentList(i).name); + end +end \ No newline at end of file From 07b15a485001066fd7251ac5e61955d3ca36c5ff Mon Sep 17 00:00:00 2001 From: ehennestad Date: Mon, 17 Jun 2024 15:27:34 +0200 Subject: [PATCH 04/24] Update DaqSystemConfiguration.m Change default value of "Name" from missing to blank string (missing does not serialize to json well) Add method to get daq system using only device name --- .../@DaqSystemConfiguration/DaqSystemConfiguration.m | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/+ndi/+setup/@DaqSystemConfiguration/DaqSystemConfiguration.m b/+ndi/+setup/@DaqSystemConfiguration/DaqSystemConfiguration.m index f54294585..d1051ad7a 100644 --- a/+ndi/+setup/@DaqSystemConfiguration/DaqSystemConfiguration.m +++ b/+ndi/+setup/@DaqSystemConfiguration/DaqSystemConfiguration.m @@ -3,7 +3,7 @@ properties % Name - A name for the DAQ system device - Name (1,1) string = missing + Name (1,1) string = "" % DaqSystemClass - Full class name for the class to use for % creating an NDI DAQ system object @@ -45,7 +45,7 @@ methods function obj = DaqSystemConfiguration(name, propertyValues) arguments - name + name (1,1) string = "" propertyValues.?ndi.setup.DaqSystemConfiguration end @@ -161,6 +161,14 @@ function export(obj, configFileName) configFilePath = fullfile(importDir, [deviceName, '.json']); daqSystemConfiguration = ndi.setup.DaqSystemConfiguration.fromConfigFile(configFilePath); end + + function daqSystemConfiguration = fromDeviceName(deviceName) + ndi.globals; + importDir = fullfile(ndi_globals.path.commonpath, 'daq_systems'); + configFilePath = recursiveDir(importDir, 'FileType', '.json', 'Expression', deviceName, 'OutputType', 'FilePath'); + assert(iscell(configFilePath) && numel(configFilePath)==1) + daqSystemConfiguration = ndi.setup.DaqSystemConfiguration.fromConfigFile(configFilePath{1}); + end end end From 0de251b882be008b0b7b3fb3284a4ab04a7a5096 Mon Sep 17 00:00:00 2001 From: ehennestad Date: Mon, 17 Jun 2024 22:50:29 +0200 Subject: [PATCH 05/24] Add DAQSystemConfigurator app and dependencies --- +ndi/+daq/templates/daq_reader.mtemplate | 5 + +ndi/+daq/templates/daq_reader_variables.json | 4 + .../+internal/FileParameterSpecification.m | 42 + +ndi/+file/temp_name.m | 4 +- +ndi/+file/tempsave.m | 19 + apps/DAQSystemConfigurator.m | 1143 +++++++++++++++++ apps/private/getNdiAppIcon.m | 0 apps/private/treeDir.m | 120 ++ apps/resources/icons/confirm.png | Bin 0 -> 7536 bytes apps/resources/icons/export.png | Bin 0 -> 8758 bytes apps/resources/icons/import.png | Bin 0 -> 8786 bytes apps/resources/ndi_logo.png | Bin 0 -> 16957 bytes 12 files changed, 1335 insertions(+), 2 deletions(-) create mode 100644 +ndi/+daq/templates/daq_reader.mtemplate create mode 100644 +ndi/+daq/templates/daq_reader_variables.json create mode 100644 +ndi/+file/+internal/FileParameterSpecification.m create mode 100644 +ndi/+file/tempsave.m create mode 100644 apps/DAQSystemConfigurator.m create mode 100644 apps/private/getNdiAppIcon.m create mode 100644 apps/private/treeDir.m create mode 100644 apps/resources/icons/confirm.png create mode 100644 apps/resources/icons/export.png create mode 100644 apps/resources/icons/import.png create mode 100644 apps/resources/ndi_logo.png 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/+file/+internal/FileParameterSpecification.m b/+ndi/+file/+internal/FileParameterSpecification.m new file mode 100644 index 000000000..817617bcb --- /dev/null +++ b/+ndi/+file/+internal/FileParameterSpecification.m @@ -0,0 +1,42 @@ +classdef FileParameterSpecification +%FileParameterSpecification Specification for a DAQ System file parameter +% +% This specification is used by the DAQ System Configurator for +% interactively picking and adding file parameters to a DAQ system +% configuration template + + properties + % OriginalFilename - The original filename used to select a file parameter. + % This is the exact filename of a file belonging to a specific + % session and epoch. + OriginalFilename (1,1) string = missing + + % ClassType - The type of class to use for reading file + % This should be one of "DAQ Reader", "Metadata Reader" or "Epoch Probe Map" + ClassType (1,1) string {mustBeMember(ClassType, ["DAQ Reader", "Metadata Reader", "Epoch Probe Map"])} = "DAQ Reader" + + % ClassName - The full name of the matlab class to use for reading file + ClassName (1,1) string = missing + + % UseRegularExpression - Whether to use a regular expression to find file + % If this is true, a regular expression is used to find the file, + % otherwise the full name in OriginalFilename is used. + UseRegularExpression (1,1) logical = false + + % RegularExpression - A regular expression for finding file. + % This should be non-missing if UseRegularExpression is true + RegularExpression (1,1) string = missing + end + + methods + function obj = FileParameterSpecification(propertyValues) + arguments + propertyValues.?ndi.file.internal.FileParameterSpecification + end + + for fieldName = string(fieldnames(propertyValues)') + obj.(fieldName) = propertyValues.(fieldName); + end + end + end +end diff --git a/+ndi/+file/temp_name.m b/+ndi/+file/temp_name.m index f53140fa8..16e05867c 100644 --- a/+ndi/+file/temp_name.m +++ b/+ndi/+file/temp_name.m @@ -1,7 +1,7 @@ -function fname = tempname() +function fname = temp_name() % TEMPNAME - return a unique temporary file name % -% FNAME = ndi.file.tempname() +% FNAME = ndi.file.temp_name() % % Return the full path of a unique temporary file name that % can be used by NDI programs. diff --git a/+ndi/+file/tempsave.m b/+ndi/+file/tempsave.m new file mode 100644 index 000000000..9ed92e42f --- /dev/null +++ b/+ndi/+file/tempsave.m @@ -0,0 +1,19 @@ +function [filePath, cleanupObj] = tempsave(fileUrl, fileName) +% tempsave - Save file from the web to temporary location +% +% File is automatically deleted when the cleanupObj is deleted or cleared +% from the workspace + + if nargin < 2 + [~, fileName, fileExtension] = fileparts( char(fileUrl) ); + end + + filePath = websave(fullfile(tempdir, [fileName, fileExtension] ), fileUrl ); + cleanupObj = onCleanup(@(filename) deleteTempFile(filePath)); +end + +function deleteTempFile(filePath) + if isfile(filePath) + delete(filePath) + end +end diff --git a/apps/DAQSystemConfigurator.m b/apps/DAQSystemConfigurator.m new file mode 100644 index 000000000..1133b0da8 --- /dev/null +++ b/apps/DAQSystemConfigurator.m @@ -0,0 +1,1143 @@ +classdef DAQSystemConfigurator < matlab.apps.AppBase + + % Properties that correspond to app components + properties (Access = public) + UIFigure matlab.ui.Figure + MainGridLayout matlab.ui.container.GridLayout + FooterGridLayout matlab.ui.container.GridLayout + ExportDaqSystemButton matlab.ui.control.Button + ImportDAQSystemButton matlab.ui.control.Button + TabGroup matlab.ui.container.TabGroup + CreateDAQSystemTab matlab.ui.container.Tab + DaqSystemPageLayout matlab.ui.container.GridLayout + DAQSystemNameLabel matlab.ui.control.Label + DaqSystemNameEditField matlab.ui.control.EditField + HelpButton_DS matlab.ui.control.Button + HelpButton_DR matlab.ui.control.Button + HelpButton_MDR matlab.ui.control.Button + HelpButton_PM matlab.ui.control.Button + CreateNewButton_DS matlab.ui.control.Button + CreateNewButton_DR matlab.ui.control.Button + CreateNewButton_MDR matlab.ui.control.Button + CreateNewButton_PM matlab.ui.control.Button + LoadProbesFromFileSwitch matlab.ui.control.Switch + LoadProbesFromFileLabel matlab.ui.control.Label + SelectDAQEpochProbemapClassDropDown matlab.ui.control.DropDown + SelectDAQEpochProbemapClassDropDownLabel matlab.ui.control.Label + SelectDAQMetadataReaderDropDown matlab.ui.control.DropDown + SelectDAQMetadataReaderDropDownLabel matlab.ui.control.Label + SelectDAQReaderDropDown matlab.ui.control.DropDown + SelectDAQReaderDropDownLabel matlab.ui.control.Label + DAQSystemBaseDropDown matlab.ui.control.DropDown + LogoImage matlab.ui.control.Image + DAQSystemBaseDropDownLabel matlab.ui.control.Label + LinkFilesTab matlab.ui.container.Tab + LinkedFilePageGridLayout matlab.ui.container.GridLayout + SelectReaderTypeforLinkingFilesLabel matlab.ui.control.Label + FileSelectionGridLayout matlab.ui.container.GridLayout + FileTree matlab.ui.container.CheckBoxTree + FileTreeFilterEditField matlab.ui.control.EditField + DAQReaderButton matlab.ui.control.StateButton + MetadataReaderButton matlab.ui.control.StateButton + EpochProbeMapButton matlab.ui.control.StateButton + RegularExpressionEditField matlab.ui.control.EditField + RegularExpressionSwitch matlab.ui.control.Switch + UseRegularExpressionforSelectedFileLabel matlab.ui.control.Label + ProbesTab matlab.ui.container.Tab + ProbePageGridLayout matlab.ui.container.GridLayout + ProbeTableToolbarGridLayout matlab.ui.container.GridLayout + CustomizeprobesperepochCheckBox matlab.ui.control.CheckBox + SelectEpochDropDown matlab.ui.control.DropDown + SelectEpochDropDownLabel matlab.ui.control.Label + ProbeTablePanel matlab.ui.container.Panel + ProbeTableGridLayout matlab.ui.container.GridLayout + HiddenTabGroupLabel matlab.ui.control.Label + HiddenTabGroup matlab.ui.container.TabGroup + Tab matlab.ui.container.Tab + HiddenTreeLabel matlab.ui.control.Label + HiddenTree matlab.ui.container.CheckBoxTree + end + + % Todo / Tests: + % [ ] Import / export + % [ ] Update each individual field + + properties (SetAccess = private) + % DaqSystemConfiguration - DAQ System Configuration object. + % This object will be updated based on user input in this app and + % can be exported to file. + DaqSystemConfiguration ndi.setup.DaqSystemConfiguration + + % FinishState - State of app pending user input. + FinishState = "Incomplete" + end + + properties (Access = private) % Private data properties + % RootDirectory - Root directory of a dataset + RootDirectory (1,1) string = missing + + % EpochFolder - Pathname of an epoch folder + EpochFolder (1,1) string = missing + + % EpochOrganization - Describes how epochs are organized in folders. + % Value is "flat" if files from different epochs exist in the same + % folder and "nested" if files from different epohchs exists in + % individual subfolders + EpochOrganization (1,1) string ... + {mustBeMember(EpochOrganization, ["flat", "nested"])} = "flat" + + % FileParameters - A list of file parameters for a DAQ system. + % Representetd by a FileParameterSpecification object. + FileParameters (1,:) ndi.file.internal.FileParameterSpecification + + % CurrentFileParameterSelectionIndex - Index of currently selected file. + CurrentFileParameterSelectionIndex = [] + end + + properties (Access = private) % Custom UI Components + % UIForm - A struct for storing handles of form / dialog windows + % that can be opened from this app + UIForm (1,1) struct = struct + + % ProbeTable - UITable for probes. Users can interactively add and + % remove probes as well as modifying existing probes + ProbeTable + end + + properties (Dependent, Access = private) + % CheckedNodes - Private store for checked nodes of the FileTree + CheckedNodes + % CheckedNodes - Private store for selected nodes of the FileTree + SelectedNodes + end + + properties (Access = private) + % Todo: Define/describe this... + Theme = ndi.ui.dataset.WizardTheme %ndi.graphics.theme.NDITheme + + % WaitFor - Boolean flag indicating if MATLAB execution is blocked + % by this app. + WaitFor (1,1) logical = false + end + + methods % Public methods + function uiwait(app) + % uiwait - Blocks program execution until users clicks "Save Changes" + + % Change appearance of import button to. Button is renamed to + % "Save Changes" and program execution is block until button is + % pushed. + app.ImportDAQSystemButton.Text = "Save Changes"; + pathToMLAPP = fileparts(mfilename('fullpath')); + app.ImportDAQSystemButton.Icon = fullfile(pathToMLAPP, 'resources', 'icons', 'save.png'); + app.ImportDAQSystemButton.ButtonPushedFcn = createCallbackFcn(app, @SaveChangesButtonPushed, true); + + app.FinishState = "Incomplete"; + app.WaitFor = true; + uiwait(app.UIFigure) %#ok + end + end + + % Property set methods + methods + function set.DaqSystemConfiguration(app, value) + app.DaqSystemConfiguration = value; + app.postSetDaqSystemConfiguration() + end + + function value = get.CheckedNodes(app) + value = [app.FileTree.CheckedNodes; app.HiddenTree.CheckedNodes]; + end + end + + % Property post set methods + methods (Access = private) + function postSetDaqSystemConfiguration(app) + + app.DaqSystemNameEditField.Value = app.DaqSystemConfiguration.Name; + + daqReaderSplit = strsplit(app.DaqSystemConfiguration.DaqReaderClass, '.'); + app.SelectDAQReaderDropDown.Value = daqReaderSplit{end}; + + if ~isempty( app.DaqSystemConfiguration.MetadataReaderClass ) + mdReaderSplit = strsplit(app.DaqSystemConfiguration.MetadataReaderClass, '.'); + app.SelectDAQMetadataReaderDropDown.Value = mdReaderSplit{end}; + else + app.SelectDAQMetadataReaderDropDown.Value = app.SelectDAQMetadataReaderDropDown.Items{1}; + end + end + end + + methods (Access = private) + function probeData = initializeProbeData(app) + % Todo: Initialize probe data based on dataset... + probeData = struct(... + 'Name', "ctx", ... + 'Reference', uint8(1), ... + 'Type', "n-trode", ... + 'DeviceString', "ced_daqsystem:ai11", ... + 'Subject', categorical("treeshrew_12345@mylab.org") ); + + % List supported probe types in NDI: + ndi.globals + probe_type_file = fullfile(ndi_globals.path.commonpath, 'probe', 'probetype2object.json'); + probeTable = jsondecode(fileread(probe_type_file)); + probeTypes = string( {probeTable.type} ); + + % Create a categorical value + probeData.Type = categorical({'n-trode'}, probeTypes); + + % Convert to table. + probeData = struct2table(probeData, "AsArray", true); + end + end + + % Methods for populating and updating uicontrols + methods (Access = private) + function initializeProbeTable(app) + % initializeProbeTable - Initialize the probe table component + probeData = app.initializeProbeData(); + + %app.ProbeTable = ndi.gui.control.WidgetTable(app.ProbeTableGridLayout); + % Create the WidgetTable component. + app.ProbeTable = WidgetTable(app.ProbeTableGridLayout); + app.ProbeTable.ShowColumnHeaderHelp = 'off'; + app.ProbeTable.ColumnWidth = {100, 90, 80, '1x', 150}; + app.ProbeTable.Data = probeData; + drawnow + app.ProbeTable.HeaderBackgroundColor = "#002054"; + app.ProbeTable.HeaderForegroundColor = '#FDF7FA'; + app.ProbeTable.HeaderTextColor = '#FDF7FA'; + app.ProbeTable.BackgroundColor = "white"; + app.ProbeTable.MinimumColumnWidth = 120; + %app.ProbeTable.redraw() + %app.ProbeTable.TableBorderType = 'none'; + end + + function populateDaqSystemClassOptions(app) + names = ndi.setup.daq.listDaqSystemClasses(); + app.DAQSystemBaseDropDown.Items = names; + app.DAQSystemBaseDropDown.Value = names{1}; + end + + function populateDaqReaderOptions(app) + names = ndi.setup.daq.listDaqReaders(); + names = [{'', ''}, names]; + app.SelectDAQMetadataReaderDropDown.Items = names; + app.SelectDAQMetadataReaderDropDown.Value = names{1}; + end + + function populateEpochProbeMapClass(app) + names = ndi.setup.daq.listDaqEpochProbemapClass(); + names = [{'', '') + newValue = ''; + end + end + + obj.DataModel.updateFolderLevel(rowInd, varName, newValue) + end + + % Callback handler for button to add more subfolder levels. + function onAddSubfolderLevelButtonPushed(obj, ~, ~) + try + obj.DataModel.assertExistSubfolders() + obj.DataModel.addSubFolderLevel() + catch ME + switch ME.identifier + case 'NDI:FolderModel:FolderNameIsEmpty' + error('Please select a subfolder for each folder level before adding a new folder level') + otherwise + rethrow(ME) + end + end + end + + % Callback handler for when subfolder is removed. + function onSubfolderLevelRemoved(obj, ~, evt) + obj.DataModel.removeSubfolderLevel(evt.RowIndex) + end + end +end + +function str = numbersign2expression(str) +% numbersign2expression - Convert number signs (#) to regexp expression +% +% Example: +% str = test###; +% expr = numbersign2expression(str) +% +% expr = +% +% test\d{3} + + assert(ischar(str), 'Input must be a character vector') + + numChars = numel(str); + for i = numChars:-1:1 + + searchStr = repmat('#', 1, i); + replaceStr = ['\', sprintf('d{%d}', i)]; + + str = strrep(str, searchStr, replaceStr); + end +end diff --git a/+ndi/+dataset/+gui/+folderorganization/FolderOrganizationTableView.m b/+ndi/+dataset/+gui/+folderorganization/FolderOrganizationTableView.m new file mode 100644 index 000000000..65f1acd74 --- /dev/null +++ b/+ndi/+dataset/+gui/+folderorganization/FolderOrganizationTableView.m @@ -0,0 +1,101 @@ +classdef FolderOrganizationTableView < handle + + properties (Access = private) + UITable + end + + properties (Access = private) + DataModel + end + + properties (Access = private) + FolderModelChangedListener + ShowAdvancedOptions = true % Todo: Change default to false... + end + + methods % Constructor + function obj = FolderOrganizationTableView(dataModel, uiTable) + if ~nargin; return; end + obj.DataModel = dataModel; + obj.UITable = uiTable; + + % Add listeners for model events + obj.FolderModelChangedListener = listener(obj.DataModel, ... + 'FolderModelChanged', @obj.onFolderModelChanged); + end + end + + methods + function update(obj) + obj.updateTableView() + end + + function showAdvancedOptions(obj) + obj.ShowAdvancedOptions = true; + + obj.UITable.reset() + drawnow + + obj.UITable.ColumnNames = {'Select subfolder example', 'Set subfolder type', 'Exclusion list', 'Inclusion list'}; + obj.UITable.ColumnWidth = {185, 175, 150, 150}; + obj.UITable.MaximumColumnWidth = [300, 140, 300, 300]; + + obj.updateTableView() + drawnow + obj.UITable.redraw() + end + + function hideAdvancedOptions(obj) + obj.ShowAdvancedOptions = false; + + obj.UITable.reset() + drawnow + + obj.UITable.ColumnNames = {'Select subfolder example', 'Set subfolder type'}; + obj.UITable.ColumnWidth = {'2x', '1x'}; + obj.UITable.MaximumColumnWidth = [300, 140]; + + obj.updateTableView() + drawnow + obj.UITable.redraw() + end + end + + % Callback functions handling folder model events + methods (Access = private) + function onFolderModelChanged(obj, src, evt) + obj.updateTableView() + end + end + + methods (Access = private) + function updateTableView(obj) + S = obj.DataModel.getFolderLevelStruct(); + if isempty(S); return; end + + for i = 1:numel(S) + subFolderOptions = obj.DataModel.listFoldersAtDepth(i); + subFolderOptions = [{''; + obj.Data(rowNum).Type = ''; + else + reader = strsplit(rowData.DaqReaderClass, '.'); + rowComponents.DaqReaderDropdown.Value = reader{end}; + end + + if ~isempty(rowData.FileParameters) + rowComponents.DynamicRegexp.Value = strjoin( rowData.FileParameters, ','); + end + end + + end + + methods (Access = private) + + function onRootDirectorySet(obj) + obj.onCurrentDataLocationSet() + end + + function onCurrentDataLocationSet(obj) + %onCurrentDataLocationSet Update controls based on current DataLoc + + if ~obj.IsConstructed; return; end + + obj.IsUpdating = true; + + obj.resetTable() + + obj.Data = obj.CurrentDataLocation.SubfolderStructure; + + % Recreate rows. + for i = 1:numel(obj.Data) + rowData = obj.getRowData(i); + obj.createTableRow(rowData, i) + + obj.updateSubfolderItems(i); % Semicolon, this fcn has output. + if ~obj.IsAdvancedView + obj.setRowDisplayMode(i, false) + end + end + + obj.IsUpdating = false; + end + + end + + methods % Callbacks for row components + + function notify(obj, eventName, eventData) + %notify Disable event notification when table is being updated + % + % Note: Some methods that notify about events are being invoked + % during table update. The method ensures that events are not + % triggered during table update. + + if obj.IsUpdating + return; + else + notify@handle(obj, eventName, eventData) + end + end + + function showAdvancedOptions(obj) + + % Relocate / show header elements + obj.setColumnHeaderDisplayMode(true) + + % Relocate / show column elements + for i = 1:numel(obj.RowControls) + obj.setRowDisplayMode(i, true) + end + + obj.IsAdvancedView = true; + drawnow + end + + function hideAdvancedOptions(obj) + + % Relocate / show header elements + obj.setColumnHeaderDisplayMode(false) + + % Relocate / show column elements + for i = 1:numel(obj.RowControls) + obj.setRowDisplayMode(i, false) + end + + obj.IsAdvancedView = false; + drawnow + end + + function setColumnHeaderDisplayMode(obj, showAdvanced) + + xOffset = sum(obj.ColumnWidths(4:5)) + obj.ColumnSpacing; + visibility = 'off'; + + if showAdvanced + xOffset = -1 * xOffset; + visibility = 'on'; + end + + % Relocate / show header elements + %obj.ColumnHeaderLabels{2}.Position(3) = obj.ColumnHeaderLabels{2}.Position(3) + xOffset; + %obj.ColumnLabelHelpButton{2}.Position(1) = obj.ColumnLabelHelpButton{2}.Position(1) + xOffset; + + obj.ColumnHeaderLabels{3}.Position(1) = obj.ColumnHeaderLabels{3}.Position(1) + xOffset; + obj.ColumnLabelHelpButton{3}.Position(1) = obj.ColumnLabelHelpButton{3}.Position(1) + xOffset; + + obj.ColumnHeaderLabels{4}.Visible = visibility; + obj.ColumnLabelHelpButton{4}.Visible = visibility; + + obj.ColumnHeaderLabels{5}.Visible = visibility; + obj.ColumnLabelHelpButton{5}.Visible = visibility; + end + + function setRowDisplayMode(obj, rowNum, showAdvanced) + + xOffset = sum(obj.ColumnWidths(4:5)) + obj.ColumnSpacing; + visibility = 'off'; + + if showAdvanced + xOffset = -1 * xOffset; + visibility = 'on'; + end + + hRow = obj.RowControls(rowNum); + hRow.SubfolderDropdown.Position(3) = hRow.SubfolderDropdown.Position(3) + xOffset; + hRow.SubfolderTypeDropdown.Position(1) = hRow.SubfolderTypeDropdown.Position(1) + xOffset; + hRow.DynamicRegexp.Visible = visibility; + hRow.IgnoreList.Visible = visibility; + end + + function markClean(obj) + obj.IsDirty = false; + end + + function markDirty(obj) + obj.IsDirty = true; + end + + function onRemoveRowButtonPushed(obj, src, ~) + if nargin < 2 % Remove last row if no input is given. + i = obj.NumRows; + elseif isnumeric(src) + i = src; + else + i = obj.getComponentRowNumber(src); + end + + obj.removeRow(i) + end + + function onDAQSystemNameChanged(obj, src, evt) + end + + function onDaqSystemSelectionChanged(obj, src, evt) + end + + function onDaqReaderSelectionChanged(obj, src, evt) + % Todo: Need to expand full classname... + end + + function onFileParametersValueChanged(obj, src, evt) + + end + + function onEditDaqSystemButtonPushed(obj, src, ~) + if ~isempty(obj.EditDaqSystemButtonPushedFcn) + hProgress = uiprogressdlg(obj.AppFigure, "Indeterminate", "on", "Message", "Opening DAQ System Editor"); + + % Todo: Get DAQ System + daqSystem = []; + obj.EditDaqSystemButtonPushedFcn(daqSystem) + + delete(hProgress) + end + end + + function onAddDaqSystemButtonPushed(obj, src, evt) + end + end + + methods (Access = private) % Create individual components + function hButton = createRemoveRowButton(obj, rowIdx, columnIdx) + % createRemoveRowButton - Button with minus icon for removing row + [xi, y, wi, h] = obj.getCellPosition(rowIdx, columnIdx); + hButton = uibutton(obj.TablePanel); + hButton.Position = [xi y wi h]; + hButton.Text = ''; + hButton.Icon = nansen.internal.getIconPathName('minus.png'); + hButton.ButtonPushedFcn = @obj.onRemoveRowButtonPushed; + end + + function hEditfield = createDaqSystemNameEditfield(obj, rowIdx, columnIdx) + % createDaqSystemNameEditfield - Create editfield for entering a name + + [xi, y, wi, h] = obj.getCellPosition(rowIdx, columnIdx); + hEditfield = uieditfield(obj.TablePanel, 'text'); + hEditfield.Position = [xi y wi h]; + hEditfield.FontName = 'Segoe UI'; + hEditfield.BackgroundColor = [1 1 1]; + hEditfield.ValueChangedFcn = @obj.onDAQSystemNameChanged; + end + + function hDropdown = createDaqSystemSelectionDropdown(obj, rowIdx, columnIdx) + % createSubfolderSelectionDropdown - Create dropdown for selecting subfolder name + [xi, y, wi, h] = obj.getCellPosition(rowIdx, columnIdx); + hDropdown = uidropdown(obj.TablePanel); + hDropdown.Position = [xi y wi h]; + hDropdown.FontName = 'Segoe UI'; + hDropdown.BackgroundColor = [1 1 1]; + hDropdown.ValueChangedFcn = @obj.onDaqSystemSelectionChanged; + hDropdown.Items = {'Multi-function DAQ'}; + hDropdown.Value = 'Multi-function DAQ'; + end + + function hDropdown = createDaqReaderSelectionDropdown(obj, rowIdx, columnIdx) + % createSubfolderTypeSelectionDropdown - Create dropdown for selecting subfolder type + [xi, y, wi, h] = obj.getCellPosition(rowIdx, columnIdx); + hDropdown = uidropdown(obj.TablePanel); + hDropdown.Position = [xi y wi h]; + hDropdown.ValueChangedFcn = @obj.onDaqReaderSelectionChanged; + %hDropdown.Items = [{''}, ndi.setup.daq.listDaqReaders()]; + end + + function hEditfield = createFileParametersEditfield(obj, rowIdx, columnIdx) + % createFileParametersEditfield - Create editfield for entering a + % file name expressinons + + [xi, y, wi, h] = obj.getCellPosition(rowIdx, columnIdx); + hEditfield = uieditfield(obj.TablePanel, 'text'); + hEditfield.Position = [xi y wi h]; + hEditfield.FontName = 'Segoe UI'; + hEditfield.BackgroundColor = [1 1 1]; + hEditfield.ValueChangedFcn = @obj.onFileParametersValueChanged; + end + + function hButton = createEditDaqSystemButton(obj, rowIdx, columnIdx) + % createEditDaqSystemButton - Create button for editing DAQ system + [xi, y, wi, h] = obj.getCellPosition(rowIdx, columnIdx); + hButton = uibutton(obj.TablePanel); + hButton.Position = [xi y wi h]; + hButton.Text = ''; + hButton.Icon = nansen.internal.getIconPathName('ellipsis.png'); + hButton.ButtonPushedFcn = @obj.onEditDaqSystemButtonPushed; + end + + function bButton = createAddRowButton(obj, rowIdx, columnIdx) + % createSubfolderTypeSelectionDropdown - Create dropdown for selecting subfolder type + [xi, y, wi, h] = obj.getCellPosition(rowIdx, columnIdx); + bButton = uibutton(obj.TablePanel); + bButton.Position = [xi y wi h]; + bButton.Text = ''; + bButton.Icon = nansen.internal.getIconPathName('plus.png'); + bButton.ButtonPushedFcn = @obj.onAddDaqSystemButtonPushed; + bButton.Enable = 'off'; + end + end +end \ No newline at end of file diff --git a/+ndi/+dataset/+gui/+pages/+component/DatasetComponent.mlapp b/+ndi/+dataset/+gui/+pages/+component/DatasetComponent.mlapp new file mode 100644 index 0000000000000000000000000000000000000000..806450bad29fcd5681722eb68068b87c2d76d969 GIT binary patch literal 21153 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�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 MkKfiUcYnS82aOh?>;M1& literal 0 HcmV?d00001 diff --git a/+ndi/+dataset/+gui/+pages/+component/FolderOrganizationTable.m b/+ndi/+dataset/+gui/+pages/+component/FolderOrganizationTable.m new file mode 100644 index 000000000..93d06914c --- /dev/null +++ b/+ndi/+dataset/+gui/+pages/+component/FolderOrganizationTable.m @@ -0,0 +1,651 @@ +classdef FolderOrganizationTable < applify.apptable +% FolderOrganizationTable - A table for uifigures to specify folder organization + +% Note: this class is a mess when it comes to updating the data and values. +% Needs work in order to instantly update the datalocation model on +% changes. The methods for updating are misused, so that whenever the +% subfolder example selection is changed, add row and remove row is called, +% although this does not mean the model is changed. Need to separate +% methods better... + +% Methods that use updateSubfolderItems +% - addrow +% - subfolderChanged +% - onIgnoreExpressionValueChanged +% - onFilterExpressionValueChanged +% - onRootDirectorySet +% +% should separate beteen whether 1) names of existing subfolder levels are +% changed, 2) subfolders levels are added or removed and 3) datalocation +% is set/changed. + + % updateRowData + % updateDirectoryTree + + properties (Constant) + COLUMN_NAMES = {'', 'Select subfolder example', 'Set subfolder type', 'Exclusion list', 'Inclusion list', ''} + COLUMN_WIDTHS = [22, 175, 130, 90, 125, 22] + end + + % properties / app states depending on outside values: + properties + RootDirectory + + % Data... Should be an object with callbacks and events? + + % stuct/table/object representation of whats in table.. + SubfolderStructure + CurrentDataLocation + end + + properties + AppFigure % Todo: make sure this is set... Needed??? + 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 + + properties (Access = private) + FolderOrganizationFilterListener + end + + events + FilterChanged + ModelChanged + end + + methods % Structors + function obj = FolderOrganizationTable(folderOrganizationModel, varargin) + %FolderOrganizationTable - Construct a FolderOrganizationTable instance + + % Todo: + % 2 inputs: + % 1) Root directory + % 2) Folder organization model... + + if ~isempty(folderOrganizationModel) + varargin = [varargin, {'Data', folderOrganizationModel.FolderLevels}]; + end + + obj@applify.apptable(varargin{:}) + + if ~isempty(folderOrganizationModel) + obj.SubfolderStructure = folderOrganizationModel.FolderLevels; + end + + obj.IsUpdating = true; + for i = 1:obj.NumRows + obj.updateSubfolderItems(i); + end + obj.IsDirty = false; + obj.IsUpdating = false; + obj.AppFigure = ancestor(obj.Parent, 'figure'); + end + + function delete(obj) + isDeletable = @(h) ~isempty(h) && isvalid(h); + + if isDeletable(obj.FolderOrganizationFilterListener) + delete(obj.FolderOrganizationFilterListener) + end + end + end + + methods % Set/get + function set.RootDirectory(obj, newValue) + obj.RootDirectory = newValue; + obj.onRootDirectorySet() + end + + function set.SubfolderStructure(obj, newValue) + obj.SubfolderStructure = newValue; + obj.onSubfolderStructureSet() + end + end + + methods (Access = protected) % Implementation of superclass (UIControlTable) 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), ... + 'SubfolderDropdown', obj.createSubfolderSelectionDropdown(rowNum, 2), ... + 'SubfolderTypeDropdown', obj.createSubfolderTypeSelectionDropdown(rowNum, 3), ... + 'DynamicRegexp', obj.createFilterExpressionEditfield(rowNum, 5), ... + 'IgnoreList', obj.createIgnoreExpressionEditfield(rowNum, 4), ... + 'AddImage', obj.createAddRowButton(rowNum, 6) ... + ); + + % Customize components... + if obj.NumRows == 0; hRow.RemoveImage.Enable = 'off'; end + if rowNum == 1; hRow.RemoveImage.Enable = 'off'; end + + if rowNum > 1 + % Disable the button for add new row on the previous row. + obj.RowControls(rowNum-1).AddImage.Enable = 'off'; + end + + obj.updateRowComponentValues(hRow, rowData, rowNum) + end + + function updateRowComponentValues(obj, rowComponents, rowData, rowNum) + % updateRowComponentValues - Update component values from data + + if isempty(rowData.Type) || strcmp(rowData.Type, 'Undefined') + rowComponents.SubfolderTypeDropdown.Value = 'Select type'; + obj.Data(rowNum).Type = 'Undefined'; + else + rowComponents.SubfolderTypeDropdown.Value = rowData.Type; + end + + if ~isempty(rowData.Expression) + rowComponents.DynamicRegexp.Value = rowData.Expression; + end + + if ~isempty(rowData.IgnoreList) + rowComponents.IgnoreList.Value = strjoin(rowData.IgnoreList, ', '); + end + end + end + + methods (Access = private) % Callbacks for table / component + + function onRootDirectorySet(obj) + %onRootDirectorySet - Update table when root directory is set + if ~obj.IsConstructed; return; end + + obj.IsUpdating = true; + obj.resetTable() + obj.Data = obj.SubfolderStructure; + obj.updateTable() + obj.IsUpdating = false; + end + + function onSubfolderStructureSet(obj) + %onSubfolderStructureSet - Update table when subfolder structure is set + + if ~obj.IsConstructed; return; end + + obj.IsUpdating = true; + obj.resetTable() + obj.Data = obj.SubfolderStructure; + obj.updateTable() + obj.IsUpdating = false; + end + end + + methods % Todo: (Access = protected) % Override superclass (UIControlTable) methods + function wasSuccess = addRow(obj, src, ~) + + src.Enable = 'off'; + addRow@applify.apptable(obj) + + % Get row number of new row. + rowNum = obj.getComponentRowNumber(src) + 1; + + if ~obj.IsAdvancedView + obj.setRowDisplayMode(rowNum, false) + end + + % Todo: should refactor this so that first, we check if folders + % are available, then add row if confirmed... + wasSuccess = obj.updateSubfolderItems(rowNum); + if ~wasSuccess + obj.removeRow() + return + end + + evtData = event.EventData(); + obj.notify('FilterChanged', evtData) + end + + function removeRow(obj, src, ~) + + if nargin < 2 % Remove last row if no input is given. + i = obj.NumRows; + elseif isnumeric(src) + i = src; + else + i = obj.getComponentRowNumber(src); + end + + removeRow@applify.apptable(obj, i) + + % Enable button for adding new row on the row above the one + % that was just removed. + if i > 1 + obj.RowControls(i-1).AddImage.Enable = 'on'; + end + + evtData = event.EventData(); + obj.notify('FilterChanged', evtData) + end + + % Todo: Generalize. Should be method of UIControlTable to get + % current data as a table + function S = getSubfolderStructure(obj) + + % This method retrieves data from components and places it in a + % struct. + + S = struct('Name', {}, 'Type', {}, 'Expression', {}, 'IgnoreList', {{}}); + + for j = 1:numel(obj.RowControls) + + S(j).Name = obj.RowControls(j).SubfolderDropdown.Value; + S(j).Type = obj.RowControls(j).SubfolderTypeDropdown.Value; + + if strcmp(S(j).Type, 'Select type') + S(j).Type = 'Undefined'; + end + + inputExpr = obj.RowControls(j).DynamicRegexp.Value; + + % Convert input expressions to expression that can be + % used with the regexp function + if strcmp(S(j).Type, 'Date') + S(j).Expression = utility.string.dateformat2expression(inputExpr); + S(j).Expression = utility.string.numbersymbol2expression(S(j).Expression); + else + S(j).Expression = utility.string.numbersymbol2expression(inputExpr); + end + + ignoreList = obj.RowControls(j).IgnoreList.Value; + if isempty(ignoreList) + S(j).IgnoreList = {}; + else + S(j).IgnoreList = strsplit(obj.RowControls(j).IgnoreList.Value, ','); + S(j).IgnoreList = strtrim(S(j).IgnoreList); + % If someone accidentally entered a comma at the end of + % the list. + if isempty(S(j).IgnoreList{end}) + S(j).IgnoreList(end) = []; + end + end + end + end + end + + methods % (Access = protected) % Internal methods + + function notify(obj, eventName, eventData) + %notify Disable event notification when table is being updated + % + % Note: Some methods that notify about events are being invoked + % during table update. The method ensures that events are not + % triggered during table update. + + if obj.IsUpdating + return; + else + notify@handle(obj, eventName, eventData) + end + end + + function showAdvancedOptions(obj) + + % Relocate / show header elements + obj.setColumnHeaderDisplayMode(true) + + % Relocate / show column elements + for i = 1:numel(obj.RowControls) + obj.setRowDisplayMode(i, true) + end + + obj.IsAdvancedView = true; + drawnow + end + + function hideAdvancedOptions(obj) + + % Relocate / show header elements + obj.setColumnHeaderDisplayMode(false) + + % Relocate / show column elements + for i = 1:numel(obj.RowControls) + obj.setRowDisplayMode(i, false) + end + + obj.IsAdvancedView = false; + drawnow + end + + function setColumnHeaderDisplayMode(obj, showAdvanced) + + xOffset = sum(obj.ColumnWidths(4:5)) + obj.ColumnSpacing; + visibility = 'off'; + + if showAdvanced + xOffset = -1 * xOffset; + visibility = 'on'; + end + + % Relocate / show header elements + %obj.ColumnHeaderLabels{2}.Position(3) = obj.ColumnHeaderLabels{2}.Position(3) + xOffset; + %obj.ColumnLabelHelpButton{2}.Position(1) = obj.ColumnLabelHelpButton{2}.Position(1) + xOffset; + + obj.ColumnHeaderLabels{3}.Position(1) = obj.ColumnHeaderLabels{3}.Position(1) + xOffset; + obj.ColumnLabelHelpButton{3}.Position(1) = obj.ColumnLabelHelpButton{3}.Position(1) + xOffset; + + obj.ColumnHeaderLabels{4}.Visible = visibility; + obj.ColumnLabelHelpButton{4}.Visible = visibility; + + obj.ColumnHeaderLabels{5}.Visible = visibility; + obj.ColumnLabelHelpButton{5}.Visible = visibility; + end + + function setRowDisplayMode(obj, rowNum, showAdvanced) + + xOffset = sum(obj.ColumnWidths(4:5)) + obj.ColumnSpacing; + visibility = 'off'; + + if showAdvanced + xOffset = -1 * xOffset; + visibility = 'on'; + end + + hRow = obj.RowControls(rowNum); + hRow.SubfolderDropdown.Position(3) = hRow.SubfolderDropdown.Position(3) + xOffset; + hRow.SubfolderTypeDropdown.Position(1) = hRow.SubfolderTypeDropdown.Position(1) + xOffset; + hRow.DynamicRegexp.Visible = visibility; + hRow.IgnoreList.Visible = visibility; + end + + function markClean(obj) + obj.IsDirty = false; + end + + function markDirty(obj) + obj.IsDirty = true; + end + end + + methods % Updating subfolders... + function updateTable(obj) + + % Recreate rows. + for i = 1:numel(obj.Data) + rowData = obj.getRowData(i); + obj.createTableRow(rowData, i) + + obj.updateSubfolderItems(i); % Semicolon, this fcn has output. + if ~obj.IsAdvancedView + obj.setRowDisplayMode(i, false) + end + end + end + + function subfolderChanged(obj, src, ~) + + obj.IsDirty = true; + iRow = obj.getComponentRowNumber(src); + + %Update data property obj.Data(iRow).Name + obj.Data(iRow).Name = obj.RowControls(iRow).SubfolderDropdown.Value; + + if iRow == obj.NumRows + return + end + + % Update list of subfolder items on the next row + obj.updateSubfolderItems( iRow+1 ) + + % Remove subfolders on successive rows if present + for i = iRow+2:numel(obj.NumRows) + obj.removeRow() + end + end + + function success = updateSubfolderItems(obj, iRow) + %updateSubfolderItems Update values in controls... + + success = true; + + % if isempty(obj.CurrentDataLocation.RootPath) + % rootDirectoryPath = ''; + % else + % rootDirectoryPath = obj.CurrentDataLocation.RootPath(1).Value; + % end + + rootDirectoryPath = obj.RootDirectory; + + % Get path for parent folder of current row (subfolder depth) + parentFolderPath = obj.getFolderPathAtDepth(iRow-1); + + % List subfolders at current level: + subfolderNames = obj.listFoldersAtDepth(parentFolderPath, iRow); + + % Todo: Add something like this if implementing virtual folders + % in a datalocation +% % % if isempty(dirName) +% % % % show message... +% % % [~, dirName] = utility.path.listFiles(folderPath); +% % % end + + % Get handle to dropdown control + hSubfolderDropdown = obj.RowControls(iRow).SubfolderDropdown; + + % Show message dialog and return if no subfolders are found. + if isempty(rootDirectoryPath) + hSubfolderDropdown.Items = {'Root folder is not specified'}; + if ~nargout; clear success; end + return + elseif ~isfolder( rootDirectoryPath ) + hSubfolderDropdown.Items = {'Root folder does not exist'}; + if ~nargout; clear success; end + return + elseif isempty(subfolderNames) % && iRow > 1 +% % message = 'No subfolders were found within the selected folder'; +% % hFigure = ancestor(obj.Parent, 'figure'); +% % uialert(hFigure, message, 'Aborting') + success = false; + hSubfolderDropdown.Items = {'No subfolders were found'}; + if ~nargout; clear success; end + return + end + + % Need to update field based on current data. + hSubfolderDropdown.Items = subfolderNames; + + if isempty( char( obj.Data(iRow).Name ) ) + % Select the first subfolder: + newValue = subfolderNames{1}; + else + if ~contains(hSubfolderDropdown.Items, obj.Data(iRow).Name) + % Todo: Add message saying that folder was not + % available in detected items. + newValue = subfolderNames{1}; + else + newValue = obj.Data(iRow).Name; + end + end + + if ~isequal(hSubfolderDropdown.Value, newValue) + hSubfolderDropdown.Value = newValue; + if ~obj.IsUpdating + obj.subfolderChanged(hSubfolderDropdown) + end + end + + obj.Data(iRow).Name = hSubfolderDropdown.Value; + + % Switch button for adding new row. + if iRow == obj.NumRows + obj.RowControls(iRow).AddImage.Enable = 'on'; + end + + obj.IsDirty = true; + + if ~nargout; clear success; end + end + end + + methods (Access = private) % Todo: methods of folder model + function folderPath = getFolderPathAtDepth(obj, subfolderDepth) + % getFolderAtDepth - Get path name for the subfolder at given depth + + rootDirectoryPath = obj.RootDirectory; + + if subfolderDepth >= 0 && ~isempty(rootDirectoryPath) + folderPath = rootDirectoryPath; + + for iLevel = 1:subfolderDepth % Get folderpath from data struct... + folderPath = fullfile(folderPath, obj.Data(iLevel).Name); + end + else + folderPath = ''; + end + end + + function folderNames = listFoldersAtDepth(obj, parentFolderPath, subfolderDepth) + % listFoldersAtDepth - List all folders at a given depth which pass filters + S = obj.getSubfolderStructure(); + + % Look for subfolders in the folderpath + [~, folderNames] = utility.path.listSubDir(parentFolderPath, ... + S(subfolderDepth).Expression, S(subfolderDepth).IgnoreList); + end + end + + methods (Access = private) % Callbacks for row components + % Callback for SubfolderSelectionDropdown + function onSubfolderSelectionChanged(obj, src, ~) + obj.subfolderChanged(src) + end + + % Callback for SubfolderSelectionTypeDropdown + function onSubFolderTypeSelectionChanged(obj, src, evt) + iRow = obj.getComponentRowNumber(src); + obj.SubfolderStructure(iRow).Type = src.Value; + obj.Data(iRow).Type = src.Value; + obj.markDirty() + end + + % Callback for IgnoreExpressionEditfield + function onIgnoreExpressionValueChanged(obj, src, evt) + iRow = obj.getComponentRowNumber(src); + if isempty(src.Value) + obj.Data(iRow).IgnoreList = {}; + else + obj.Data(iRow).IgnoreList = strtrim( strsplit(src.Value, ',') ); + end + obj.SubfolderStructure(iRow).IgnoreList = obj.Data(iRow).IgnoreList; + obj.markDirty() + + evtData = event.EventData(); + obj.notify('FilterChanged', evtData) + + obj.updateSubfolderItems(iRow) + end + + % Callback for FilterExpressionEditfield + function onFilterExpressionValueChanged(obj, src, evt) + iRow = obj.getComponentRowNumber(src); + obj.Data(iRow).Expression = src.Value; + obj.markDirty() + + evtData = event.EventData(); + obj.notify('FilterChanged', evtData) + + obj.updateSubfolderItems(iRow); % supress output + end + + % Callback for AddRowButton + function onAddSubfolderButtonPushed(obj, src, ~) + + wasSuccess = obj.addRow(src); + + % Show message to user if this failed. + if ~wasSuccess + hFigure = ancestor(obj.Parent, 'figure'); + message = 'No subfolders were found within the selected folder'; + uialert(hFigure, message, 'Aborting') + else + obj.SubfolderStructure(end+1) = feval( class(obj.SubfolderStructure) ); + end + end + + function onRemoveSubfolderLevelButtonPushed(obj, ~, ~) + obj.removeRow() + obj.SubfolderStructure(end) = []; + end + end + + methods (Access = private) % Create individual components + function hButton = createRemoveRowButton(obj, rowIdx, columnIdx) + % createRemoveRowButton - Button with minus icon for removing row + [xi, y, wi, h] = obj.getCellPosition(rowIdx, columnIdx); + hButton = uibutton(obj.TablePanel); + hButton.Position = [xi y wi h]; + hButton.Text = ''; + hButton.Icon = nansen.internal.getIconPathName('minus.png'); + hButton.ButtonPushedFcn = @obj.onRemoveSubfolderLevelButtonPushed; + end + + function hDropdown = createSubfolderSelectionDropdown(obj, rowIdx, columnIdx) + % createSubfolderSelectionDropdown - Create dropdown for selecting subfolder name + [xi, y, wi, h] = obj.getCellPosition(rowIdx, columnIdx); + hDropdown = uidropdown(obj.TablePanel); + hDropdown.Position = [xi y wi h]; + hDropdown.FontName = 'Segoe UI'; + hDropdown.BackgroundColor = [1 1 1]; + hDropdown.ValueChangedFcn = @obj.onSubfolderSelectionChanged; + hDropdown.Items = {'Select subfolder'}; + hDropdown.Value = 'Select subfolder'; + end + + function hDropdown = createSubfolderTypeSelectionDropdown(obj, rowIdx, columnIdx) + % createSubfolderTypeSelectionDropdown - Create dropdown for selecting subfolder type + [xi, y, wi, h] = obj.getCellPosition(rowIdx, columnIdx); + hDropdown = uidropdown(obj.TablePanel); + hDropdown.Position = [xi y wi h]; + hDropdown.ValueChangedFcn = @obj.onSubFolderTypeSelectionChanged; + % Todo: Get from enum? + hDropdown.Items = {'Select type', 'Date', 'Subject', 'Session', 'Epoch', 'Other'}; + end + + function hEditfield = createFilterExpressionEditfield(obj, rowIdx, columnIdx) + % createFilterExpressionEditfield - Create editfield for entering a filter expression + % + % This field will be used to filter the folders on the + % corresponding subfolder level. + [xi, y, wi, h] = obj.getCellPosition(rowIdx, columnIdx); + hEditfield = uieditfield(obj.TablePanel, 'text'); + hEditfield.Position = [xi y wi h]; + hEditfield.FontName = 'Segoe UI'; + hEditfield.BackgroundColor = [1 1 1]; + hEditfield.ValueChangedFcn = @obj.onFilterExpressionValueChanged; + end + + function hEditfield = createIgnoreExpressionEditfield(obj, rowIdx, columnIdx) + % createSubfolderTypeSelectionDropdown - Create dropdown for selecting subfolder type + [xi, y, wi, h] = obj.getCellPosition(rowIdx, columnIdx); + hEditfield = uieditfield(obj.TablePanel, 'text'); + hEditfield.Position = [xi y wi h]; + hEditfield.FontName = 'Segoe UI'; + hEditfield.BackgroundColor = [1 1 1]; + hEditfield.ValueChangedFcn = @obj.onIgnoreExpressionValueChanged; + end + + function bButton = createAddRowButton(obj, rowIdx, columnIdx) + % createSubfolderTypeSelectionDropdown - Create dropdown for selecting subfolder type + [xi, y, wi, h] = obj.getCellPosition(rowIdx, columnIdx); + bButton = uibutton(obj.TablePanel); + bButton.Position = [xi y wi h]; + bButton.Text = ''; + bButton.Icon = nansen.internal.getIconPathName('plus.png'); + bButton.ButtonPushedFcn = @obj.onAddSubfolderButtonPushed; + bButton.Enable = 'off'; + end + end +end \ No newline at end of file diff --git a/+ndi/+dataset/+gui/+pages/+component/FolderOrganizationToolbar.mlapp b/+ndi/+dataset/+gui/+pages/+component/FolderOrganizationToolbar.mlapp new file mode 100644 index 0000000000000000000000000000000000000000..d16e27ba281f86ea7bd013b7e6b954b8cee9da37 GIT binary patch literal 18528 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', ''}, 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"} = "'}, names]; - app.SelectDAQReaderDropDown.Items = names; - app.SelectDAQReaderDropDown.Value = names{1}; - end - - function populateDaqMetadataReaderOptions(app) - names = ndi.setup.daq.listDaqMetadataReaders(); - names = [{'', ''}, names]; - app.SelectDAQEpochProbemapClassDropDown.Items = names; - app.SelectDAQEpochProbemapClassDropDown.Value = names{1}; - end - - function populateFileViewer(app, treeList) - % populateFileViewer - Add a file hierarchy to the file viewer (uitree) - delete(app.FileTree.Children) - app.addFileTreeNode(treeList, app.FileTree, true) - app.FileTree.ContextMenu = []; - end - - function addFileTreeNode(app, treeList, hParentNode, isRoot) - % addFileTreeNode - Recursively adds nodes to the FileTree. - % - % treeList is a struct representing a nested list of files and - % folders as returned by the treeDir function. - for i = 1:numel(treeList) - if ~isRoot - hChildNode = uitreenode(hParentNode); - hChildNode.Text = treeList.FolderName; - else - hChildNode = hParentNode; - end - - if ~isempty(treeList(i).Subfolders) - for j = 1:numel(treeList(i).Subfolders) - app.addFileTreeNode(treeList(i).Subfolders(j), hChildNode, false) - end - end - - if ~isempty(treeList(i).Files) - for j = 1:numel(treeList(i).Files) - hFileNode = uitreenode(hChildNode); - hFileNode.Text = treeList(i).Files(j).name; - end - end - end - end - - function onDatasetDirectoryChanged(app, rootPath) - % onDatasetDirectoryChanged - Handles changes to the dataset root folder - if nargin < 2 - rootPath = fullfile(app.RootDirectory, app.EpochFolder); - end - - if isfolder(rootPath) - S = treeDir(rootPath); - app.populateFileViewer(S) - else - uialert(app.UIFigure, 'The provided folderpath is not valid') - end - end - - function dataClass = getCurrentDataClassSelection(app) - if app.DAQReaderButton.Value - dataClass = "DAQ Reader"; - elseif app.MetadataReaderButton.Value - dataClass = "Metadata Reader"; - elseif app.EpochProbeMapButton.Value - dataClass = "Epoch Probe Map"; - else - error('This should not happen') - end - end - - function updateCheckedNodes(app) - % Determine which nodes to check in the FileTree based on which - % nodes are currently visible (i.e unfiltered) and which dataclass - % is currently selected. - % This method is typically called when the filter is updated or - % the data class selection changes. - - dataClass = app.getCurrentDataClassSelection(); - - % Get visible nodes: - nodes = app.FileTree.Children; - if ~isempty(nodes) - visibleFileNames = {nodes.Text}; - else - return - end - - checkedItems = [app.FileParameters.ClassType] == dataClass; - - namesToCheck = string( [ app.FileParameters(checkedItems).OriginalFilename ] ); - - [~, iA] = intersect(visibleFileNames, namesToCheck, 'stable'); - if isempty(iA) - app.FileTree.CheckedNodes = []; - else - app.FileTree.CheckedNodes = nodes(iA); - end - end - - function disableFileParameterComponents(app) - app.RegularExpressionEditField.Enable = "off"; - app.RegularExpressionSwitch.Enable = "off"; - end - - function enableFileParameterComponents(app) - app.RegularExpressionEditField.Enable = "on"; - app.RegularExpressionSwitch.Enable = "on"; - end - - function updateFileParameterComponents(app, fileParameter) - app.enableFileParameterComponents() - - if fileParameter.UseRegularExpression - app.RegularExpressionSwitch.Value = 'On'; - else - app.RegularExpressionSwitch.Value = 'Off'; - end - app.RegularExpressionSwitchValueChanged([]) - - if ~ismissing( fileParameter.RegularExpression ) - app.RegularExpressionEditField.Value = fileParameter.RegularExpression; - else - app.RegularExpressionEditField.Value = ""; - end - end - - function resetFileParameterComponents(app) - app.disableFileParameterComponents() - app.RegularExpressionSwitch.Value = "Off"; - app.RegularExpressionSwitchValueChanged([]) - app.RegularExpressionEditField.Value = ""; - end - end - - methods (Access = private) - function applyTheme(app) - for i = 1:numel(app.TabGroup.Children) - %app.TabGroup.Children(i).BackgroundColor = app.Theme.FigureBgColor; - end - end - end - - % Callbacks that handle component events - methods (Access = private) - - % Code that executes after component creation - function startupFcn(app, rootDirectory, epochFolder, epochOrganization, daqSystemConfiguration) - arguments - app (1,1) DAQSystemConfigurator - rootDirectory (1,1) string = missing - epochFolder (1,1) string = missing - epochOrganization (1,1) string ... - {mustBeMember(epochOrganization, ["flat", "nested"])} = "flat" - daqSystemConfiguration (1,1) ndi.setup.DaqSystemConfiguration = ndi.setup.DaqSystemConfiguration; - end - - app.RootDirectory = rootDirectory; - app.EpochFolder = epochFolder; - app.EpochOrganization = epochOrganization; - - app.populateDaqSystemClassOptions() - app.populateDaqReaderOptions() - app.populateDaqMetadataReaderOptions() - app.populateEpochProbeMapClass() - - app.DaqSystemConfiguration = daqSystemConfiguration; - - % Hide the probe tab by default - app.ProbesTab.Parent = app.HiddenTabGroup; - - app.applyTheme() - - if ~ismissing(app.RootDirectory) && isfolder(app.RootDirectory) - app.onDatasetDirectoryChanged() - end - - app.disableFileParameterComponents() - - app.initializeProbeTable() - end - - % Selection changed function: FileTree - function FileTreeSelectionChanged(app, event) - % Check if the selected node is also checked, and update file - % parameter components accordingly. - % If selected node is checked, update file parameter components - % based on the file parameters for the selected file - % If selected node is not checked, reset and disable the file - % parameter components. - - selectedNodes = app.FileTree.SelectedNodes; - checkedNodes = app.FileTree.CheckedNodes; - - if isempty(selectedNodes) - selectedNodeNames = string.empty; - else - selectedNodeNames = string( {selectedNodes.Text} ); - end - - if isempty(checkedNodes) - checkedNodeNames = string.empty; - else - checkedNodeNames = string( {checkedNodes.Text} ); - end - - if ~any(strcmp(checkedNodeNames, selectedNodeNames)) - fileParameterIndex = false; - else - fileParameterIndex = strcmp( [app.FileParameters.OriginalFilename], selectedNodeNames ); - end - - app.CurrentFileParameterSelectionIndex = find(fileParameterIndex); - - if any(fileParameterIndex) - fileParameter = app.FileParameters(fileParameterIndex); - app.updateFileParameterComponents(fileParameter) - app.enableFileParameterComponents() - else - app.resetFileParameterComponents() - app.disableFileParameterComponents() - end - end - - % Callback function: FileTree - function FileTreeCheckedNodesChanged(app, event) - % Find out if any checked nodes have been added or removed and - % update the FileParameters property accordingly. - - checkedNodes = event.CheckedNodes; - previousCheckedNodes = event.PreviousCheckedNodes; - - if isempty(checkedNodes) - currentNames = string.empty; - else - currentNames = string( {checkedNodes.Text} ); - end - - if isempty(previousCheckedNodes) - previousNames = string.empty; - else - previousNames = string( {previousCheckedNodes.Text} ); - end - - [addedNames, ~] = setdiff( currentNames, previousNames ); - [removedNames, iRemoved] = setdiff( previousNames, currentNames ); - - nodeIsAdded = ~isempty(addedNames); - nodeIsRemoved = ~isempty(removedNames); - - allSelectedNames = string( [app.FileParameters.OriginalFilename] ); - - if nodeIsAdded - % Check if node is already added to another file reader... - if any(strcmp(allSelectedNames, addedNames)) - idx = strcmp(allSelectedNames, addedNames); - uialert(app.UIFigure, '"%s" is already added for %s', addedNames, app.FileParameters(idx).ClassType) - return - - else % Add a new file parameter - app.FileParameters(end+1) = ndi.file.internal.FileParameterSpecification(... - "OriginalFilename", addedNames{1}, ... - "RegularExpression", addedNames{1}, ... - "ClassType", app.getCurrentDataClassSelection ); - app.updateFileParameterComponents( app.FileParameters(end) ); - app.FileTreeSelectionChanged() - end - end - - if nodeIsRemoved - app.FileParameters(iRemoved) = []; - if ~isequal(iRemoved, app.CurrentFileParameterSelectionIndex) - warning('Something unexpected happened.') - %keyboard % Debug - end - app.CurrentFileParameterSelectionIndex = []; - app.resetFileParameterComponents( ); - end - end - - % Value changed function: RegularExpressionSwitch - function RegularExpressionSwitchValueChanged(app, event) - value = app.RegularExpressionSwitch.Value; - if value=="On" - app.RegularExpressionEditField.Visible = 'on'; - else - app.RegularExpressionEditField.Visible = 'off'; - end - - if ~isempty(app.CurrentFileParameterSelectionIndex) - app.FileParameters(app.CurrentFileParameterSelectionIndex).UseRegularExpression = value=="On"; - end - end - - % Value changed function: DAQReaderButton - function DAQReaderButtonValueChanged(app, event) - value = app.DAQReaderButton.Value; - if value - app.MetadataReaderButton.Value = false; - app.EpochProbeMapButton.Value = false; - else - if ~app.MetadataReaderButton.Value && ~ app.EpochProbeMapButton.Value - app.DAQReaderButton.Value = true; - return - end - end - app.updateCheckedNodes() - app.FileTreeSelectionChanged() - end - - % Value changed function: MetadataReaderButton - function MetadataReaderButtonValueChanged(app, event) - value = app.MetadataReaderButton.Value; - if value - app.DAQReaderButton.Value = false; - app.EpochProbeMapButton.Value = false; - else - if ~app.DAQReaderButton.Value && ~app.EpochProbeMapButton.Value - app.MetadataReaderButton.Value = true; - return - end - end - app.updateCheckedNodes() - app.FileTreeSelectionChanged() - end - - % Value changed function: EpochProbeMapButton - function EpochProbeMapButtonValueChanged(app, event) - value = app.EpochProbeMapButton.Value; - if value - app.MetadataReaderButton.Value = false; - app.DAQReaderButton.Value = false; - else - if ~app.MetadataReaderButton.Value && ~ app.DAQReaderButton.Value - app.EpochProbeMapButton.Value = true; - return - end - end - app.updateCheckedNodes() - app.FileTreeSelectionChanged() - end - - % Value changed function: RegularExpressionEditField - function RegularExpressionEditFieldValueChanged(app, event) - value = app.RegularExpressionEditField.Value; - if ~isempty(app.CurrentFileParameterSelectionIndex) - app.FileParameters(app.CurrentFileParameterSelectionIndex).RegularExpression = value; - end - end - - % Callback function - function SelectSpecificHandlerDropDownValueChanged(app, event) - % Todo: Update how this is writte to the FileParameters object list - value = app.SelectSpecificHandlerDropDown.Value; - if ~isempty(app.CurrentFileParameterSelectionIndex) - app.FileParameters(app.CurrentFileParameterSelectionIndex).ClassName = value; - end - end - - % Callback function - function AddProbeButtonPushed(app, event) - probeData = app.ProbeTable.getDefaultProbe(); - app.ProbeTable.addRow([], probeData) - end - - % Value changing function: FileTreeFilterEditField - function FileTreeFilterEditFieldValueChanging(app, event) - changingValue = event.Value; - - nodes = [app.FileTree.Children; app.HiddenTree.Children]; - wasSelected = ~isempty(app.FileTree.SelectedNodes); - - allNames = {nodes.Text}; - - % Sort nodes by name - [allNames, idx] = sort(allNames); - nodes = nodes(idx); - - % Determine which nodes to keep after filtering - keep = contains(allNames, changingValue); - - nodesKeep = [nodes(keep)]; - nodesHide = [nodes(~keep)]; - - set(nodesKeep, 'Parent', app.FileTree); - set(nodesHide, 'Parent', app.HiddenTree); - - if wasSelected && isempty(app.FileTree.SelectedNodes) - app.CurrentFileParameterSelectionIndex = []; - app.resetFileParameterComponents(); - app.disableFileParameterComponents(); - end - - app.updateCheckedNodes() - end - - % Selection change function: TabGroup - function TabGroupSelectionChanged(app, event) - selectedTab = app.TabGroup.SelectedTab; - if selectedTab == app.ProbesTab - app.ProbeTable.redraw() - end - end - - % Value changed function: CustomizeprobesperepochCheckBox - function CustomizeProbesPerEpochCheckBoxValueChanged(app, event) - value = app.CustomizeprobesperepochCheckBox.Value; - if value - app.SelectEpochDropDown.Visible = "on"; - app.SelectEpochDropDownLabel.Visible = 'on'; - - else - app.SelectEpochDropDown.Visible = "off"; - app.SelectEpochDropDownLabel.Visible = 'off'; - end - end - - % Button pushed function: HelpButton_DR, HelpButton_DS, - % ...and 2 other components - function HelpButtonPushed(app, event) - switch event.Source - case app.HelpButton_DS - msg = 'Information about DAQ system'; - case app.HelpButton_DR - msg = 'Information about DAQ reader'; - case app.HelpButton_MDR - msg = 'Information about DAQ metadata reader'; - case app.HelpButton_PM - msg = 'Information about DAQ probe table reader'; - end - uialert(app.UIFigure, msg, "Help", "Icon", "info") - end - - % Button pushed function: ImportDAQSystemButton - function ImportDAQSystemButtonPushed(app, event) - % Open (external app) where user can select DAQ system - if ~isfield(app.UIForm, 'SelectDaqSystemDialog') - app.UIForm.SelectDaqSystemDialog = uiSelectDaqSystem(); % Create the form - else - app.UIForm.SelectDaqSystemDialog.Visible = 'on'; % Make the form visible - end - - app.UIForm.SelectDaqSystemDialog.waitfor(); % Wait for user to proceed - - % Get user-inputs from form - selectedConfigFile = app.UIForm.SelectDaqSystemDialog.getSelection(); - - % Update data in table if user pressed save. - mode = app.UIForm.SelectDaqSystemDialog.FinishState; - if mode == "Save" - app.DaqSystemConfiguration = ... - ndi.setup.DaqSystemConfiguration.fromConfigFile(selectedConfigFile); - end - - app.UIForm.SelectDaqSystemDialog.reset() - app.UIForm.SelectDaqSystemDialog.Visible = 'off'; % Hide the form (for later reuse) - end - - % Button pushed function: ExportDaqSystemButton - function ExportDaqSystemButtonPushed(app, event) - [fileName, folderPath] = uiputfile('*.json',''); - if fileName == 0; return; end - - savePath = fullfile(folderPath, fileName); - - % Export DAQ System - app.DaqSystemConfiguration.export(savePath) - end - - % Value changed function: LoadProbesFromFileSwitch - function LoadProbesFromFileSwitchValueChanged(app, event) - value = app.LoadProbesFromFileSwitch.Value; - - app.SelectDAQEpochProbemapClassDropDown.Visible = value; - app.SelectDAQEpochProbemapClassDropDownLabel.Visible = value; - app.HelpButton_PM.Visible = value; - app.CreateNewButton_PM.Visible = value; - - if strcmp(value, 'On') - app.ProbesTab.Parent = app.HiddenTabGroup; - else - app.ProbesTab.Parent = app.TabGroup; - uistack(app.ProbesTab, 'bottom') - end - end - - % Button pushed function: CreateNewButton_DR - function CreateNewButton_DRPushed(app, event) - uiCreateDaqReader() - end - - % Callback function - function SaveChangesButtonPushed(app, event) - uiresume(app.UIFigure) - app.FinishState = "Complete"; - end - - % Close request function: UIFigure - function UIFigureCloseRequest(app, event) - if app.WaitFor - app.FinishState = "Aborted"; - uiresume(app.UIFigure) - else - delete(app) - end - end - end - - % Component initialization - methods (Access = private) - - % Create UIFigure and components - function createComponents(app) - - % Get the file path for locating images - pathToMLAPP = fileparts(mfilename('fullpath')); - - % Create UIFigure and hide until all components are created - app.UIFigure = uifigure('Visible', 'off'); - app.UIFigure.Position = [100 100 873 567]; - app.UIFigure.Name = 'Configure DAQ System'; - app.UIFigure.CloseRequestFcn = createCallbackFcn(app, @UIFigureCloseRequest, true); - - % Create HiddenTree - app.HiddenTree = uitree(app.UIFigure, 'checkbox'); - app.HiddenTree.Visible = 'off'; - app.HiddenTree.Position = [6 -148 231 109]; - - % Create HiddenTreeLabel - app.HiddenTreeLabel = uilabel(app.UIFigure); - app.HiddenTreeLabel.Position = [6 -40 229 22]; - app.HiddenTreeLabel.Text = 'Hidden tree (for hiding filtered tree nodes)'; - - % Create HiddenTabGroup - app.HiddenTabGroup = uitabgroup(app.UIFigure); - app.HiddenTabGroup.Visible = 'off'; - app.HiddenTabGroup.Position = [289 -148 263 109]; - - % Create Tab - app.Tab = uitab(app.HiddenTabGroup); - - % Create HiddenTabGroupLabel - app.HiddenTabGroupLabel = uilabel(app.UIFigure); - app.HiddenTabGroupLabel.Position = [289 -40 212 22]; - app.HiddenTabGroupLabel.Text = 'Hidden tabgroup (for hiding probe tab)'; - - % Create MainGridLayout - app.MainGridLayout = uigridlayout(app.UIFigure); - app.MainGridLayout.ColumnWidth = {'1x'}; - app.MainGridLayout.RowHeight = {'1x', 70}; - app.MainGridLayout.RowSpacing = 0; - app.MainGridLayout.Padding = [0 0 0 0]; - - % Create TabGroup - app.TabGroup = uitabgroup(app.MainGridLayout); - app.TabGroup.SelectionChangedFcn = createCallbackFcn(app, @TabGroupSelectionChanged, true); - app.TabGroup.Layout.Row = 1; - app.TabGroup.Layout.Column = 1; - - % Create CreateDAQSystemTab - app.CreateDAQSystemTab = uitab(app.TabGroup); - app.CreateDAQSystemTab.Title = 'Create DAQ System'; - app.CreateDAQSystemTab.BackgroundColor = [0.9608 0.9608 0.9608]; - - % Create DaqSystemPageLayout - app.DaqSystemPageLayout = uigridlayout(app.CreateDAQSystemTab); - app.DaqSystemPageLayout.ColumnWidth = {300, 100, 25, '1x', 150}; - app.DaqSystemPageLayout.RowHeight = {25, 25, 3, '0.5x', 25, 25, 3, '0.5x', 25, 25, 3, '0.5x', 25, 25, 10, '0.5x', 23, 25, 25, 3, '0.5x', 50}; - app.DaqSystemPageLayout.RowSpacing = 5; - app.DaqSystemPageLayout.Padding = [35 35 35 35]; - app.DaqSystemPageLayout.BackgroundColor = [0.9804 0.9804 0.9804]; - - % Create DAQSystemBaseDropDownLabel - app.DAQSystemBaseDropDownLabel = uilabel(app.DaqSystemPageLayout); - app.DAQSystemBaseDropDownLabel.VerticalAlignment = 'bottom'; - app.DAQSystemBaseDropDownLabel.Layout.Row = 5; - app.DAQSystemBaseDropDownLabel.Layout.Column = 1; - app.DAQSystemBaseDropDownLabel.Text = 'DAQ System Base'; - - % Create LogoImage - app.LogoImage = uiimage(app.DaqSystemPageLayout); - app.LogoImage.Layout.Row = 1; - app.LogoImage.Layout.Column = 5; - app.LogoImage.ImageSource = fullfile(pathToMLAPP, 'resources', 'ndi_logo.png'); - - % Create DAQSystemBaseDropDown - app.DAQSystemBaseDropDown = uidropdown(app.DaqSystemPageLayout); - app.DAQSystemBaseDropDown.Layout.Row = 6; - app.DAQSystemBaseDropDown.Layout.Column = 1; - - % Create SelectDAQReaderDropDownLabel - app.SelectDAQReaderDropDownLabel = uilabel(app.DaqSystemPageLayout); - app.SelectDAQReaderDropDownLabel.VerticalAlignment = 'bottom'; - app.SelectDAQReaderDropDownLabel.Layout.Row = 9; - app.SelectDAQReaderDropDownLabel.Layout.Column = 1; - app.SelectDAQReaderDropDownLabel.Text = 'Select DAQ Reader'; - - % Create SelectDAQReaderDropDown - app.SelectDAQReaderDropDown = uidropdown(app.DaqSystemPageLayout); - app.SelectDAQReaderDropDown.Layout.Row = 10; - app.SelectDAQReaderDropDown.Layout.Column = 1; - - % Create SelectDAQMetadataReaderDropDownLabel - app.SelectDAQMetadataReaderDropDownLabel = uilabel(app.DaqSystemPageLayout); - app.SelectDAQMetadataReaderDropDownLabel.VerticalAlignment = 'bottom'; - app.SelectDAQMetadataReaderDropDownLabel.Layout.Row = 13; - app.SelectDAQMetadataReaderDropDownLabel.Layout.Column = 1; - app.SelectDAQMetadataReaderDropDownLabel.Text = 'Select DAQ Metadata Reader'; - - % Create SelectDAQMetadataReaderDropDown - app.SelectDAQMetadataReaderDropDown = uidropdown(app.DaqSystemPageLayout); - app.SelectDAQMetadataReaderDropDown.Layout.Row = 14; - app.SelectDAQMetadataReaderDropDown.Layout.Column = 1; - - % Create SelectDAQEpochProbemapClassDropDownLabel - app.SelectDAQEpochProbemapClassDropDownLabel = uilabel(app.DaqSystemPageLayout); - app.SelectDAQEpochProbemapClassDropDownLabel.VerticalAlignment = 'bottom'; - app.SelectDAQEpochProbemapClassDropDownLabel.Layout.Row = 18; - app.SelectDAQEpochProbemapClassDropDownLabel.Layout.Column = 1; - app.SelectDAQEpochProbemapClassDropDownLabel.Text = 'Select DAQ Epoch Probemap Class'; - - % Create SelectDAQEpochProbemapClassDropDown - app.SelectDAQEpochProbemapClassDropDown = uidropdown(app.DaqSystemPageLayout); - app.SelectDAQEpochProbemapClassDropDown.Layout.Row = 19; - app.SelectDAQEpochProbemapClassDropDown.Layout.Column = 1; - - % Create LoadProbesFromFileLabel - app.LoadProbesFromFileLabel = uilabel(app.DaqSystemPageLayout); - app.LoadProbesFromFileLabel.FontWeight = 'bold'; - app.LoadProbesFromFileLabel.Layout.Row = 17; - app.LoadProbesFromFileLabel.Layout.Column = [1 2]; - app.LoadProbesFromFileLabel.Text = 'Load Probes From File'; - - % Create LoadProbesFromFileSwitch - app.LoadProbesFromFileSwitch = uiswitch(app.DaqSystemPageLayout, 'slider'); - app.LoadProbesFromFileSwitch.Items = {'On', 'Off'}; - app.LoadProbesFromFileSwitch.ValueChangedFcn = createCallbackFcn(app, @LoadProbesFromFileSwitchValueChanged, true); - app.LoadProbesFromFileSwitch.Layout.Row = 17; - app.LoadProbesFromFileSwitch.Layout.Column = 2; - app.LoadProbesFromFileSwitch.Value = 'On'; - - % Create CreateNewButton_PM - app.CreateNewButton_PM = uibutton(app.DaqSystemPageLayout, 'push'); - app.CreateNewButton_PM.Layout.Row = 19; - app.CreateNewButton_PM.Layout.Column = 2; - app.CreateNewButton_PM.Text = 'Create New'; - - % Create CreateNewButton_MDR - app.CreateNewButton_MDR = uibutton(app.DaqSystemPageLayout, 'push'); - app.CreateNewButton_MDR.Layout.Row = 14; - app.CreateNewButton_MDR.Layout.Column = 2; - app.CreateNewButton_MDR.Text = 'Create New'; - - % Create CreateNewButton_DR - app.CreateNewButton_DR = uibutton(app.DaqSystemPageLayout, 'push'); - app.CreateNewButton_DR.ButtonPushedFcn = createCallbackFcn(app, @CreateNewButton_DRPushed, true); - app.CreateNewButton_DR.Layout.Row = 10; - app.CreateNewButton_DR.Layout.Column = 2; - app.CreateNewButton_DR.Text = 'Create New'; - - % Create CreateNewButton_DS - app.CreateNewButton_DS = uibutton(app.DaqSystemPageLayout, 'push'); - app.CreateNewButton_DS.Layout.Row = 6; - app.CreateNewButton_DS.Layout.Column = 2; - app.CreateNewButton_DS.Text = 'Create New'; - - % Create HelpButton_PM - app.HelpButton_PM = uibutton(app.DaqSystemPageLayout, 'push'); - app.HelpButton_PM.ButtonPushedFcn = createCallbackFcn(app, @HelpButtonPushed, true); - app.HelpButton_PM.Layout.Row = 19; - app.HelpButton_PM.Layout.Column = 3; - app.HelpButton_PM.Text = '?'; - - % Create HelpButton_MDR - app.HelpButton_MDR = uibutton(app.DaqSystemPageLayout, 'push'); - app.HelpButton_MDR.ButtonPushedFcn = createCallbackFcn(app, @HelpButtonPushed, true); - app.HelpButton_MDR.Layout.Row = 14; - app.HelpButton_MDR.Layout.Column = 3; - app.HelpButton_MDR.Text = '?'; - - % Create HelpButton_DR - app.HelpButton_DR = uibutton(app.DaqSystemPageLayout, 'push'); - app.HelpButton_DR.ButtonPushedFcn = createCallbackFcn(app, @HelpButtonPushed, true); - app.HelpButton_DR.Layout.Row = 10; - app.HelpButton_DR.Layout.Column = 3; - app.HelpButton_DR.Text = '?'; - - % Create HelpButton_DS - app.HelpButton_DS = uibutton(app.DaqSystemPageLayout, 'push'); - app.HelpButton_DS.ButtonPushedFcn = createCallbackFcn(app, @HelpButtonPushed, true); - app.HelpButton_DS.Layout.Row = 6; - app.HelpButton_DS.Layout.Column = 3; - app.HelpButton_DS.Text = '?'; - - % Create DaqSystemNameEditField - app.DaqSystemNameEditField = uieditfield(app.DaqSystemPageLayout, 'text'); - app.DaqSystemNameEditField.Layout.Row = 2; - app.DaqSystemNameEditField.Layout.Column = 1; - - % Create DAQSystemNameLabel - app.DAQSystemNameLabel = uilabel(app.DaqSystemPageLayout); - app.DAQSystemNameLabel.Layout.Row = 1; - app.DAQSystemNameLabel.Layout.Column = 1; - app.DAQSystemNameLabel.Text = 'DAQ System Name'; - - % Create LinkFilesTab - app.LinkFilesTab = uitab(app.TabGroup); - app.LinkFilesTab.Title = 'Link Files'; - app.LinkFilesTab.BackgroundColor = [0.9412 0.9412 0.9412]; - - % Create LinkedFilePageGridLayout - app.LinkedFilePageGridLayout = uigridlayout(app.LinkFilesTab); - app.LinkedFilePageGridLayout.ColumnWidth = {'2x', '1x'}; - app.LinkedFilePageGridLayout.RowHeight = {20, '1x'}; - app.LinkedFilePageGridLayout.ColumnSpacing = 30; - app.LinkedFilePageGridLayout.Padding = [35 35 35 35]; - - % Create FileSelectionGridLayout - app.FileSelectionGridLayout = uigridlayout(app.LinkedFilePageGridLayout); - app.FileSelectionGridLayout.ColumnWidth = {125, 125, 125, '1x'}; - app.FileSelectionGridLayout.RowHeight = {23, 23, 20, '1x', 23, 20, 23}; - app.FileSelectionGridLayout.Padding = [20 10 10 10]; - app.FileSelectionGridLayout.Layout.Row = 2; - app.FileSelectionGridLayout.Layout.Column = 1; - - % Create UseRegularExpressionforSelectedFileLabel - app.UseRegularExpressionforSelectedFileLabel = uilabel(app.FileSelectionGridLayout); - app.UseRegularExpressionforSelectedFileLabel.FontWeight = 'bold'; - app.UseRegularExpressionforSelectedFileLabel.Layout.Row = 6; - app.UseRegularExpressionforSelectedFileLabel.Layout.Column = [1 2]; - app.UseRegularExpressionforSelectedFileLabel.Text = 'Use Regular Expression for Selected File'; - - % Create RegularExpressionSwitch - app.RegularExpressionSwitch = uiswitch(app.FileSelectionGridLayout, 'slider'); - app.RegularExpressionSwitch.ValueChangedFcn = createCallbackFcn(app, @RegularExpressionSwitchValueChanged, true); - app.RegularExpressionSwitch.Layout.Row = 6; - app.RegularExpressionSwitch.Layout.Column = 3; - - % Create RegularExpressionEditField - app.RegularExpressionEditField = uieditfield(app.FileSelectionGridLayout, 'text'); - app.RegularExpressionEditField.ValueChangedFcn = createCallbackFcn(app, @RegularExpressionEditFieldValueChanged, true); - app.RegularExpressionEditField.Layout.Row = 7; - app.RegularExpressionEditField.Layout.Column = [1 3]; - - % Create EpochProbeMapButton - app.EpochProbeMapButton = uibutton(app.FileSelectionGridLayout, 'state'); - app.EpochProbeMapButton.ValueChangedFcn = createCallbackFcn(app, @EpochProbeMapButtonValueChanged, true); - app.EpochProbeMapButton.Text = 'Epoch Probe Map'; - app.EpochProbeMapButton.Layout.Row = 1; - app.EpochProbeMapButton.Layout.Column = 3; - - % Create MetadataReaderButton - app.MetadataReaderButton = uibutton(app.FileSelectionGridLayout, 'state'); - app.MetadataReaderButton.ValueChangedFcn = createCallbackFcn(app, @MetadataReaderButtonValueChanged, true); - app.MetadataReaderButton.Text = 'Metadata Reader'; - app.MetadataReaderButton.Layout.Row = 1; - app.MetadataReaderButton.Layout.Column = 2; - - % Create DAQReaderButton - app.DAQReaderButton = uibutton(app.FileSelectionGridLayout, 'state'); - app.DAQReaderButton.ValueChangedFcn = createCallbackFcn(app, @DAQReaderButtonValueChanged, true); - app.DAQReaderButton.Text = 'DAQ Reader'; - app.DAQReaderButton.Layout.Row = 1; - app.DAQReaderButton.Layout.Column = 1; - app.DAQReaderButton.Value = true; - - % Create FileTreeFilterEditField - app.FileTreeFilterEditField = uieditfield(app.FileSelectionGridLayout, 'text'); - app.FileTreeFilterEditField.ValueChangingFcn = createCallbackFcn(app, @FileTreeFilterEditFieldValueChanging, true); - app.FileTreeFilterEditField.Placeholder = 'Enter text here to filter list'; - app.FileTreeFilterEditField.Layout.Row = 3; - app.FileTreeFilterEditField.Layout.Column = [1 3]; - - % Create FileTree - app.FileTree = uitree(app.FileSelectionGridLayout, 'checkbox'); - app.FileTree.SelectionChangedFcn = createCallbackFcn(app, @FileTreeSelectionChanged, true); - app.FileTree.Layout.Row = 4; - app.FileTree.Layout.Column = [1 3]; - - % Assign Checked Nodes - app.FileTree.CheckedNodesChangedFcn = createCallbackFcn(app, @FileTreeCheckedNodesChanged, true); - - % Create SelectReaderTypeforLinkingFilesLabel - app.SelectReaderTypeforLinkingFilesLabel = uilabel(app.LinkedFilePageGridLayout); - app.SelectReaderTypeforLinkingFilesLabel.FontWeight = 'bold'; - app.SelectReaderTypeforLinkingFilesLabel.Layout.Row = 1; - app.SelectReaderTypeforLinkingFilesLabel.Layout.Column = 1; - app.SelectReaderTypeforLinkingFilesLabel.Text = 'Select Reader Type for Linking Files'; - - % Create ProbesTab - app.ProbesTab = uitab(app.TabGroup); - app.ProbesTab.Title = 'Probes'; - app.ProbesTab.BackgroundColor = [0.9137 0.9294 0.9569]; - - % Create ProbePageGridLayout - app.ProbePageGridLayout = uigridlayout(app.ProbesTab); - app.ProbePageGridLayout.ColumnWidth = {'1x'}; - app.ProbePageGridLayout.RowHeight = {25, '1x'}; - app.ProbePageGridLayout.Padding = [50 50 50 50]; - app.ProbePageGridLayout.BackgroundColor = [0.9412 0.9412 0.9412]; - - % Create ProbeTablePanel - app.ProbeTablePanel = uipanel(app.ProbePageGridLayout); - app.ProbeTablePanel.BorderType = 'none'; - app.ProbeTablePanel.BackgroundColor = [1 1 1]; - app.ProbeTablePanel.Layout.Row = 2; - app.ProbeTablePanel.Layout.Column = 1; - - % Create ProbeTableGridLayout - app.ProbeTableGridLayout = uigridlayout(app.ProbeTablePanel); - app.ProbeTableGridLayout.ColumnWidth = {'1x'}; - app.ProbeTableGridLayout.RowHeight = {'1x'}; - app.ProbeTableGridLayout.Padding = [0 0 0 0]; - app.ProbeTableGridLayout.BackgroundColor = [1 1 1]; - - % Create ProbeTableToolbarGridLayout - app.ProbeTableToolbarGridLayout = uigridlayout(app.ProbePageGridLayout); - app.ProbeTableToolbarGridLayout.ColumnWidth = {75, 100, 75, 150, '1x', 100}; - app.ProbeTableToolbarGridLayout.RowHeight = {'1x'}; - app.ProbeTableToolbarGridLayout.Padding = [0 0 0 0]; - app.ProbeTableToolbarGridLayout.Layout.Row = 1; - app.ProbeTableToolbarGridLayout.Layout.Column = 1; - app.ProbeTableToolbarGridLayout.BackgroundColor = [0.9412 0.9412 0.9412]; - - % Create SelectEpochDropDownLabel - app.SelectEpochDropDownLabel = uilabel(app.ProbeTableToolbarGridLayout); - app.SelectEpochDropDownLabel.Visible = 'off'; - app.SelectEpochDropDownLabel.Layout.Row = 1; - app.SelectEpochDropDownLabel.Layout.Column = 3; - app.SelectEpochDropDownLabel.Text = 'Select Epoch'; - - % Create SelectEpochDropDown - app.SelectEpochDropDown = uidropdown(app.ProbeTableToolbarGridLayout); - app.SelectEpochDropDown.Items = {'Epoch 1', 'Epoch 2', 'Epoch 3', 'Epoch 4'}; - app.SelectEpochDropDown.Visible = 'off'; - app.SelectEpochDropDown.Layout.Row = 1; - app.SelectEpochDropDown.Layout.Column = 4; - app.SelectEpochDropDown.Value = 'Epoch 1'; - - % Create CustomizeprobesperepochCheckBox - app.CustomizeprobesperepochCheckBox = uicheckbox(app.ProbeTableToolbarGridLayout); - app.CustomizeprobesperepochCheckBox.ValueChangedFcn = createCallbackFcn(app, @CustomizeProbesPerEpochCheckBoxValueChanged, true); - app.CustomizeprobesperepochCheckBox.Text = 'Customize probes per epoch'; - app.CustomizeprobesperepochCheckBox.Layout.Row = 1; - app.CustomizeprobesperepochCheckBox.Layout.Column = [1 2]; - - % Create FooterGridLayout - app.FooterGridLayout = uigridlayout(app.MainGridLayout); - app.FooterGridLayout.RowHeight = {'1x', 35, '1x'}; - app.FooterGridLayout.ColumnSpacing = 50; - app.FooterGridLayout.Padding = [35 0 35 0]; - app.FooterGridLayout.Layout.Row = 2; - app.FooterGridLayout.Layout.Column = 1; - app.FooterGridLayout.BackgroundColor = [0.9176 0.9294 0.9529]; - - % Create ImportDAQSystemButton - app.ImportDAQSystemButton = uibutton(app.FooterGridLayout, 'push'); - app.ImportDAQSystemButton.ButtonPushedFcn = createCallbackFcn(app, @ImportDAQSystemButtonPushed, true); - app.ImportDAQSystemButton.Icon = fullfile(pathToMLAPP, 'resources', 'icons', 'import.png'); - app.ImportDAQSystemButton.BackgroundColor = [1 1 1]; - app.ImportDAQSystemButton.FontWeight = 'bold'; - app.ImportDAQSystemButton.FontColor = [0 0.1255 0.3294]; - app.ImportDAQSystemButton.Layout.Row = 2; - app.ImportDAQSystemButton.Layout.Column = 1; - app.ImportDAQSystemButton.Text = 'Import DAQ System'; - - % Create ExportDaqSystemButton - app.ExportDaqSystemButton = uibutton(app.FooterGridLayout, 'push'); - app.ExportDaqSystemButton.ButtonPushedFcn = createCallbackFcn(app, @ExportDaqSystemButtonPushed, true); - app.ExportDaqSystemButton.Icon = fullfile(pathToMLAPP, 'resources', 'icons', 'export.png'); - app.ExportDaqSystemButton.BackgroundColor = [1 1 1]; - app.ExportDaqSystemButton.FontWeight = 'bold'; - app.ExportDaqSystemButton.FontColor = [0 0.1255 0.3294]; - app.ExportDaqSystemButton.Layout.Row = 2; - app.ExportDaqSystemButton.Layout.Column = 2; - app.ExportDaqSystemButton.Text = 'Export Daq System'; - - % Show the figure after all components are created - app.UIFigure.Visible = 'on'; - end - end - - % App creation and deletion - methods (Access = public) - - % Construct app - function app = DAQSystemConfigurator(varargin) - - % Create UIFigure and components - createComponents(app) - - % Register the app with App Designer - registerApp(app, app.UIFigure) - - % Execute the startup function - runStartupFcn(app, @(app)startupFcn(app, varargin{:})) - - if nargout == 0 - clear app - end - end - - % Code that executes before app deletion - function delete(app) - - % Delete UIFigure when app is deleted - delete(app.UIFigure) - end - end -end \ No newline at end of file diff --git a/apps/NDIDatasetWizard.m b/apps/NDIDatasetWizard.m new file mode 100644 index 000000000..3d16892fb --- /dev/null +++ b/apps/NDIDatasetWizard.m @@ -0,0 +1,8 @@ +function app = NDIDatasetWizard() +% NDIDatasetWizard - Launcher for the Dataset Wizard + app = ndi.dataset.gui.DatasetWizardApp(); + + if nargout < 1 + clear app + end +end \ No newline at end of file diff --git a/ndi-matlab-dependencies.json b/ndi-matlab-dependencies.json index 57555a556..3cc94ec95 100644 --- a/ndi-matlab-dependencies.json +++ b/ndi-matlab-dependencies.json @@ -9,8 +9,14 @@ "https://github.com/VH-lab/DID-matlab", "https://github.com/VH-lab/NDR-matlab", "https://github.com/ehennestad/Catalog", + "https://github.com/ehennestad/StructEditor", + "https://github.com/ehennestad/WidgetTable", "https://github.com/a-ma72/mksqlite", "https://github.com/Waltham-Data-Science/NDI-compress-matlabp", - "fex://c519502d-d45f-4dbd-8564-df06ab49717f" + "https://github.com/VervaekeLab/NANSEN", + "fex://7504e82c-a4fa-4757-8c5c-ad77094a8acd", + "fex://3171ed18-2788-4e49-a0cf-785bbd4d0a13", + "fex://d00d9727-fdd6-417d-9ee8-99c97b77eef5", + "fex://e5a9c7a3-4a80-11e4-9553-005056977bd0" ] } From 3da12b21bb7ba345af39d9c11c569aac6a965bb2 Mon Sep 17 00:00:00 2001 From: ehennestad Date: Wed, 3 Jul 2024 13:18:48 +0200 Subject: [PATCH 07/24] Fix: Bugs in dataset wizard / folder organization page Fix various bugs arising from restructuring --- .../FolderOrganizationPage.m | 41 ++++--------------- .../FolderOrganizationTableController.m | 5 +++ .../FolderOrganizationTableView.m | 3 ++ .../+gui/+models/FolderOrganizationModel.m | 21 ++++++++++ +ndi/+dataset/+gui/NDIDatasetConfiguration.m | 2 +- apps/NDIDatasetWizard.m | 9 +++- 6 files changed, 45 insertions(+), 36 deletions(-) diff --git a/+ndi/+dataset/+gui/+folderorganization/FolderOrganizationPage.m b/+ndi/+dataset/+gui/+folderorganization/FolderOrganizationPage.m index 6a9b6b9db..e32ab504a 100644 --- a/+ndi/+dataset/+gui/+folderorganization/FolderOrganizationPage.m +++ b/+ndi/+dataset/+gui/+folderorganization/FolderOrganizationPage.m @@ -85,34 +85,18 @@ function onVisiblePropertyValueSet(obj) end function onPageEntered(obj) - % Subclasses may override + % No action needed end function onPageExited(obj) - % Update model data based on UITable data. - if obj.IsInitialized - % Todo: get data... - S = obj.UITable.getSubfolderStructure(); - if ~isempty(obj.DataModel) - obj.DataModel.updateFolderLevelFromStruct(S) - end - end + % Todo: Close folder viewer if it is open. end function createComponents(obj) % createComponents - Create components for page. - args = { 'Parent', obj.UIPanel }; - - % % obj.UITable = ndi.dataset.gui.pages.component.FolderOrganizationTable(obj.DataModel, args{:}); - % % obj.UITable.hideAdvancedOptions() - % % if ~isempty(obj.RootDirectory) - % % obj.UITable.RootDirectory = obj.RootDirectory; - % % end - defaultTableData = obj.DataModel.getDefaultFolderLevelTable(); - obj.TableGridLayout = uigridlayout(obj.UIPanel); obj.TableGridLayout.ColumnWidth={'1x'}; obj.TableGridLayout.RowHeight={'1x'}; @@ -135,12 +119,10 @@ function createComponents(obj) 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; @@ -152,15 +134,15 @@ function createComponents(obj) obj.UIToolbar.PreviewButtonPushedFcn = @obj.onFolderPreviewButtonClicked; obj.UIToolbar.SelectTemplateDropDownValueChangedFcn = @obj.onPresetTemplateSelected; - % Todo: + % 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.MenuSelectedFcn = createCallbackFcn(obj, @DiseaseMenuSelected, true); obj.SaveMenu.Text = 'Save to template file'; obj.LoadMenu = uimenu(obj.ContextMenu); - %obj.LoadMenu.MenuSelectedFcn = createCallbackFcn(obj, @DiseaseMenuSelected, true); obj.LoadMenu.Text = 'Load from template file'; % Do this last. @@ -223,8 +205,8 @@ function createFolderListViewer(obj) addlistener(obj.FolderListViewer, 'ObjectBeingDestroyed', ... @(s, e) obj.onFolderListViewerDeleted); - obj.FolderOrganizationFilterListener = listener(obj.UITable, ... - 'FilterChanged', @(s,e) obj.updateFolderList); + obj.FolderOrganizationFilterListener = listener(obj.DataModel, ... + 'FolderModelChanged', @(s,e) obj.updateFolderList); % Give focus to the app figure figure(obj.ParentApp.UIFigure) @@ -274,15 +256,8 @@ function updateFolderList(obj) if isempty(obj.FolderListViewer) || ~isvalid(obj.FolderListViewer) return end - - % Make sure data location model is updated with current values - S = obj.UITable.getSubfolderStructure(); - folderPath = cellstr(obj.RootDirectory); - for i = 1:numel(S) - [folderPath, ~] = utility.path.listSubDir(... - folderPath, S(i).Expression, S(i).IgnoreList); - end + folderPath = obj.DataModel.listAllFolders(); if isempty(folderPath); folderPath = {''}; end diff --git a/+ndi/+dataset/+gui/+folderorganization/FolderOrganizationTableController.m b/+ndi/+dataset/+gui/+folderorganization/FolderOrganizationTableController.m index fcefcb5ab..e214c8d08 100644 --- a/+ndi/+dataset/+gui/+folderorganization/FolderOrganizationTableController.m +++ b/+ndi/+dataset/+gui/+folderorganization/FolderOrganizationTableController.m @@ -45,6 +45,11 @@ function onTableCellValueChanged(obj, ~, evt) 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, '