Plugins
Authors: Francois Tadel
Brainstorm connects with features from many third-party libraries of methods. The external software can be downloaded or updated automatically by Brainstorm when needed. This tutorial presents the API to register and manage plugins.
Interactive management
The Brainstorm interface offers menus to Install/Update/Uninstall plugins.
Install: Downloads and unzips the package in the Brainstorm user folder:
$HOME/.brainstorm/plugins/
Uninstall: Deletes the plugin folder and all its subfolders.
Load: Adds all the subfolders needed by the plugin to the Matlab path, plus other optional taks.
Unload: Removes all the plugin folders from the Matlab path.
Update: Some plugins are designed to update themselves automatically whenever a new version is available online, or requested by Brainstorm. Others plugins must be updated manually.
Manual install: If you already have a given plugin installed on your computer (eg. FieldTrip, SPM12) and don't want Brainstorm to manage the download/update or the Matlab path for you, reference it with the menu: Custom install > Set installation folder.
List: You can list all the installed plugins with the menu List:<<BR>><<BR>>
Command-line management
The calls to install or manage plugins are all documented in the header of bst_plugin.m:
1 function [varargout] = bst_plugin(varargin)
2 % BST_PLUGIN: Manages Brainstorm plugins
3 %
4 % USAGE: PlugDesc = bst_plugin('GetSupported') % List all the plugins supported by Brainstorm
5 % PlugDesc = bst_plugin('GetSupported', PlugName/PlugDesc) % Get only one specific supported plugin
6 % PlugDesc = bst_plugin('GetInstalled') % Get all the installed plugins
7 % PlugDesc = bst_plugin('GetInstalled', PlugName/PlugDesc) % Get a specific installed plugin
8 % PlugDesc = bst_plugin('GetLoaded') % Get all the loaded plugins
9 % [PlugDesc, errMsg] = bst_plugin('GetDescription', PlugName/PlugDesc) % Get a full structure representing a plugin
10 % [Version, URLzip] = bst_plugin('GetVersionOnline', PlugName, URLzip, isCache) % Get the latest online version of some plugins
11 % sha = bst_plugin('GetGithubCommit', URLzip) % Get SHA of the last commit of a GitHub repository from a master.zip url
12 % ReadmeFile = bst_plugin('GetReadmeFile', PlugDesc) % Get full path to plugin readme file
13 % LogoFile = bst_plugin('GetLogoFile', PlugDesc) % Get full path to plugin logo file
14 % Version = bst_plugin('CompareVersions', v1, v2) % Compare two version strings
15 % [isOk, errMsg] = bst_plugin('AddUserDefDesc', RegMethod, jsonLocation=[]) % Register user-defined plugin definition
16 % [isOk, errMsg] = bst_plugin('RemoveUserDefDesc' PlugName) % Remove user-defined plugin definition
17 % [isOk, errMsg, PlugDesc] = bst_plugin('Load', PlugName/PlugDesc, isVerbose=1)
18 % [isOk, errMsg, PlugDesc] = bst_plugin('LoadInteractive', PlugName/PlugDesc)
19 % [isOk, errMsg, PlugDesc] = bst_plugin('Unload', PlugName/PlugDesc, isVerbose=1)
20 % [isOk, errMsg, PlugDesc] = bst_plugin('UnloadInteractive', PlugName/PlugDesc)
21 % [isOk, errMsg, PlugDesc] = bst_plugin('Install', PlugName, isInteractive=0, minVersion=[]) % Install and Load a plugin and its dependencies
22 % [isOk, errMsg, PlugDesc] = bst_plugin('InstallMultipleChoice',PlugNames, isInteractive=0) % Install at least one of the input plugins
23 % [isOk, errMsg, PlugDesc] = bst_plugin('InstallInteractive', PlugName)
24 % [isOk, errMsg] = bst_plugin('Uninstall', PlugName, isInteractive=0, isDependencies=1)
25 % [isOk, errMsg] = bst_plugin('UninstallInteractive', PlugName)
26 % bst_plugin('Configure', PlugDesc) % Execute some additional tasks after loading or installation
27 % bst_plugin('SetCustomPath', PlugName, PlugPath)
28 % bst_plugin('List', Target='installed') % Target={'supported','installed'}
29 % bst_plugin('Archive', OutputFile=[ask]) % Archive software environment
30 % bst_plugin('MenuCreate', jMenu)
31 % bst_plugin('MenuUpdate', jMenu)
32 % bst_plugin('LinkSpmToolbox', Action, Toolbox) % 0=Delete/1=Create/2=Check a symbolic link for a Toolbox in SPM12 toolbox folder
33 % bst_plugin('UpdateDescription', PlugDesc, doDelete=0) % Update plugin description after load
34 %
35 %
36 % PLUGIN DEFINITION
37 % =================
38 %
39 % The plugins registered in Brainstorm are listed in function GetSupported().
40 % Each one is an entry in the PlugDesc array, following the structure defined in db_template('plugdesc').
41 % The fields allowed are described below.
42 %
43 % Mandatory fields
44 % ================
45 % - Name : String: Plugin name = subfolder in the Brainstorm user folder
46 % - Version : String: Version of the plugin (eg. '1.2', '21a', 'github-master', 'latest')
47 % - URLzip : String: Download URL, zip or tgz file accessible over HTTP/HTTPS/FTP
48 % - URLinfo : String: Information URL = Software website
49 %
50 % Optional fields
51 % ===============
52 % - AutoUpdate : Boolean: If true, the plugin is updated automatically when there is a new version available (default: true).
53 % - AutoLoad : Boolean: If true, the plugin is loaded automatically at Brainstorm startup
54 % - Category : String: Sub-menu in which the plugin is listed
55 % - ExtraMenus : Cell matrix {Nx2}: List of entries to add to the plugins menu
56 % | ExtraMenus{i,1}: String: Label of the menu
57 % | ExtraMenus{i,2}: String: Matlab code to eval when the menu is clicked
58 % - TestFile : String: Name of a file that should be located in one of the loaded folders of the plugin (eg. 'spm.m' for SPM12).
59 % | This is used to test whether the plugin was correctly installed, or whether it is available somewhere else in the Matlab path.
60 % - ReadmeFile : String: Name of the text file to display after installing the plugin (must be in the plugin folder).
61 % | If empty, it tries using brainstorm3/doc/plugin/plugname_readme.txt
62 % - LogoFile : String: Name of the image file to display during the plugin download, installation, and associated computations (must be in the plugin folder).
63 % | Supported extensions: gif, png. If empty, try using brainstorm3/doc/plugin/<Name>_logo.[gif|png]
64 % - MinMatlabVer : Integer: Minimum Matlab version required for using this plugin, as returned by bst_get('MatlabVersion')
65 % - CompiledStatus : Integer: Behavior of this plugin in the compiled version of Brainstorm:
66 % | 0: Plugin is not available in the compiled distribution of Brainstorm
67 % | 1: Plugin is available for download (only for plugins based on native compiled code)
68 % | 2: Plugin is included in the compiled distribution of Brainstorm
69 % - RequiredPlugs : Cell-array: Additional plugins required by this plugin, that must be installed/loaded beforehand.
70 % | {Nx2} => {'plugname','version'; ...} or
71 % | {Nx1} => {'plugname'; ...}
72 % - UnloadPlugs : Cell-array of names of incompatible plugin, to unload before loaing this one
73 % - LoadFolders : Cell-array of subfolders to add to the Matlab path when setting up the plugin. Use {'*'} to add all the plugin subfolders.
74 % - GetVersionFcn : String to eval or function handle to call to get the version after installation
75 % - InstalledFcn : String to eval or function handle to call after installing the plugin
76 % - UninstalledFcn : String to eval or function handle to call after uninstalling the plugin
77 % - LoadedFcn : String to eval or function handle to call after loading the plugin
78 % - UnloadedFcn : String to eval or function handle to call after unloading the plugin
79 % - DeleteFiles : List of files to delete after installation
80 %
81 % Fields set when installing the plugin
82 % =====================================
83 % - Processes : List of process functions to be added to the pipeline manager
84 %
85 % Fields set when loading the plugin
86 % ==================================
87 % - Path : Installation path (eg. /home/username/.brainstorm/plugins/fieldtrip)
88 % - SubFolder : If all the code is in a single subfolder (eg. /plugins/fieldtrip/fieldtrip-20210304),
89 % this is detected and the full path to the TestFile would be typically fullfile(Path, SubFolder).
90 % - isLoaded : 0=Not loaded, 1=Loaded
91 % - isManaged : 0=Installed manually by the user, 1=Installed automatically by Brainstorm
92 %
93
94 % @=============================================================================
95 % This function is part of the Brainstorm software:
96 % https://neuroimage.usc.edu/brainstorm
97 %
98 % Copyright (c) University of Southern California & McGill University
99 % This software is distributed under the terms of the GNU General Public License
100 % as published by the Free Software Foundation. Further details on the GPLv3
101 % license can be found at http://www.gnu.org/copyleft/gpl.html.
102 %
103 % FOR RESEARCH PURPOSES ONLY. THE SOFTWARE IS PROVIDED "AS IS," AND THE
104 % UNIVERSITY OF SOUTHERN CALIFORNIA AND ITS COLLABORATORS DO NOT MAKE ANY
105 % WARRANTY, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF
106 % MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, NOR DO THEY ASSUME ANY
107 % LIABILITY OR RESPONSIBILITY FOR THE USE OF THIS SOFTWARE.
108 %
109 % For more information type "brainstorm license" at command prompt.
110 % =============================================================================@
111 %
112 % Authors: Francois Tadel, 2021-2023
113
114 eval(macro_method);
115 end
116
117
118 %% ===== GET SUPPORTED PLUGINS =====
119 % USAGE: PlugDesc = bst_plugin('GetSupported') % List all the plugins supported by Brainstorm
120 % PlugDesc = bst_plugin('GetSupported', PlugName/PlugDesc) % Get only one specific supported plugin
121 % PlugDesc = bst_plugin('GetSupported', ..., UserDefVerbose) % Print info on user-defined plugins
122 function PlugDesc = GetSupported(SelPlug, UserDefVerbose)
123 % Parse inputs
124 if (nargin < 2) || isempty(UserDefVerbose)
125 UserDefVerbose = 0;
126 end
127 if (nargin < 1) || isempty(SelPlug)
128 SelPlug = [];
129 end
130 % Initialized returned structure
131 PlugDesc = repmat(db_template('PlugDesc'), 0);
132 % Get OS
133 OsType = bst_get('OsType', 0);
134
135 % Add new curated plugins by 'CATEGORY:' and alphabetic order
136 % ================================================================================================================
137 % === ANATOMY: BRAIN2MESH ===
138 PlugDesc(end+1) = GetStruct('brain2mesh');
139 PlugDesc(end).Version = 'github-master';
140 PlugDesc(end).Category = 'Anatomy';
141 PlugDesc(end).URLzip = 'https://github.com/fangq/brain2mesh/archive/master.zip';
142 PlugDesc(end).URLinfo = 'https://mcx.space/brain2mesh/';
143 PlugDesc(end).TestFile = 'brain2mesh.m';
144 PlugDesc(end).ReadmeFile = 'README.md';
145 PlugDesc(end).CompiledStatus = 2;
146 PlugDesc(end).RequiredPlugs = {'spm12'; 'iso2mesh'};
147 PlugDesc(end).DeleteFiles = {'examples', 'brain1020.m', 'closestnode.m', 'label2tpm.m', 'slicesurf.m', 'slicesurf3.m', 'tpm2label.m', 'polylineinterp.m', 'polylinelen.m', 'polylinesimplify.m'};
148
149 % === ANATOMY: CAT12 ===
150 PlugDesc(end+1) = GetStruct('cat12');
151 PlugDesc(end).Version = 'latest';
152 PlugDesc(end).Category = 'Anatomy';
153 PlugDesc(end).AutoUpdate = 1;
154 PlugDesc(end).URLzip = 'https://www.neuro.uni-jena.de/cat12/cat12_latest.zip';
155 PlugDesc(end).URLinfo = 'https://www.neuro.uni-jena.de/cat/';
156 PlugDesc(end).TestFile = 'cat_version.m';
157 PlugDesc(end).ReadmeFile = 'Contents.txt';
158 PlugDesc(end).CompiledStatus = 0;
159 PlugDesc(end).RequiredPlugs = {'spm12'};
160 PlugDesc(end).GetVersionFcn = 'bst_getoutvar(2, @cat_version)';
161 PlugDesc(end).InstalledFcn = 'LinkSpmToolbox(1, ''cat12'');';
162 PlugDesc(end).UninstalledFcn = 'LinkSpmToolbox(0, ''cat12'');';
163 PlugDesc(end).LoadedFcn = 'LinkSpmToolbox(2, ''cat12'');';
164 PlugDesc(end).UnloadedFcn = 'LinkSpmToolbox(0, ''cat12'');';
165 PlugDesc(end).ExtraMenus = {'Online tutorial', 'web(''https://neuroimage.usc.edu/brainstorm/Tutorials/SegCAT12'', ''-browser'')'};
166
167 % === ANATOMY: CT2MRIREG ===
168 PlugDesc(end+1) = GetStruct('ct2mrireg');
169 PlugDesc(end).Version = 'github-master';
170 PlugDesc(end).Category = 'Anatomy';
171 PlugDesc(end).AutoUpdate = 1;
172 PlugDesc(end).URLzip = 'https://github.com/ajoshiusc/USCCleveland/archive/master.zip';
173 PlugDesc(end).URLinfo = 'https://github.com/ajoshiusc/USCCleveland/tree/master/ct2mrireg';
174 PlugDesc(end).TestFile = 'ct2mrireg.m';
175 PlugDesc(end).ReadmeFile = 'ct2mrireg/README.md';
176 PlugDesc(end).CompiledStatus = 2;
177 PlugDesc(end).LoadFolders = {'ct2mrireg'};
178 PlugDesc(end).DeleteFiles = {'fmri_analysis', 'for_clio', 'mixed_atlas', 'process_script', 'reg_prepost', 'visualize_channels', '.gitignore', 'README.md'};
179
180 % === ANATOMY: ISO2MESH ===
181 PlugDesc(end+1) = GetStruct('iso2mesh');
182 PlugDesc(end).Version = '1.9.8';
183 PlugDesc(end).Category = 'Anatomy';
184 PlugDesc(end).AutoUpdate = 1;
185 PlugDesc(end).URLzip = 'https://github.com/fangq/iso2mesh/archive/refs/tags/v1.9.8.zip';
186 PlugDesc(end).URLinfo = 'https://iso2mesh.sourceforge.net';
187 PlugDesc(end).TestFile = 'iso2meshver.m';
188 PlugDesc(end).ReadmeFile = 'README.txt';
189 PlugDesc(end).CompiledStatus = 2;
190 PlugDesc(end).LoadedFcn = 'assignin(''base'', ''ISO2MESH_TEMP'', bst_get(''BrainstormTmpDir''));';
191 PlugDesc(end).UnloadPlugs = {'easyh5','jsnirfy'};
192
193 % === ANATOMY: NEUROMAPS ===
194 PlugDesc(end+1) = GetStruct('neuromaps');
195 PlugDesc(end).Version = 'github-main';
196 PlugDesc(end).Category = 'Anatomy';
197 PlugDesc(end).AutoUpdate = 0;
198 PlugDesc(end).AutoLoad = 0;
199 PlugDesc(end).CompiledStatus = 2;
200 PlugDesc(end).URLzip = 'https://github.com/thuy-n/bst-neuromaps/archive/refs/heads/main.zip';
201 PlugDesc(end).URLinfo = 'https://github.com/thuy-n/bst-neuromaps';
202 PlugDesc(end).ReadmeFile = 'README.md';
203 PlugDesc(end).LoadFolders = {'*'};
204 PlugDesc(end).TestFile = 'process_nmp_fetch_maps.m';
205
206 % === ANATOMY: ROAST ===
207 PlugDesc(end+1) = GetStruct('roast');
208 PlugDesc(end).Version = '3.0';
209 PlugDesc(end).Category = 'Anatomy';
210 PlugDesc(end).AutoUpdate = 1;
211 PlugDesc(end).URLzip = 'https://www.parralab.org/roast/roast-3.0.zip';
212 PlugDesc(end).URLinfo = 'https://www.parralab.org/roast/';
213 PlugDesc(end).TestFile = 'roast.m';
214 PlugDesc(end).ReadmeFile = 'README.md';
215 PlugDesc(end).CompiledStatus = 0;
216 PlugDesc(end).UnloadPlugs = {'spm12', 'iso2mesh'};
217 PlugDesc(end).LoadFolders = {'lib/spm12', 'lib/iso2mesh', 'lib/cvx', 'lib/ncs2daprox', 'lib/NIFTI_20110921'};
218
219 % === ANATOMY: ZEFFIRO ===
220 PlugDesc(end+1) = GetStruct('zeffiro');
221 PlugDesc(end).Version = 'github-main_development_branch';
222 PlugDesc(end).Category = 'Anatomy';
223 PlugDesc(end).AutoUpdate = 1;
224 PlugDesc(end).URLzip = 'https://github.com/sampsapursiainen/zeffiro_interface/archive/main_development_branch.zip';
225 PlugDesc(end).URLinfo = 'https://github.com/sampsapursiainen/zeffiro_interface';
226 PlugDesc(end).TestFile = 'zeffiro_downloader.m';
227 PlugDesc(end).ReadmeFile = 'README.md';
228 PlugDesc(end).CompiledStatus = 0;
229 PlugDesc(end).LoadFolders = {'*'};
230 PlugDesc(end).DeleteFiles = {'.gitignore'};
231
232
233 % === FORWARD: OPENMEEG ===
234 PlugDesc(end+1) = GetStruct('openmeeg');
235 PlugDesc(end).Version = '2.4.1';
236 PlugDesc(end).Category = 'Forward';
237 PlugDesc(end).AutoUpdate = 1;
238 switch(OsType)
239 case 'linux64'
240 PlugDesc(end).URLzip = 'https://files.inria.fr/OpenMEEG/download/OpenMEEG-2.4.1-Linux.tar.gz';
241 PlugDesc(end).TestFile = 'libOpenMEEG.so';
242 case 'mac64'
243 PlugDesc(end).URLzip = 'https://files.inria.fr/OpenMEEG/download/OpenMEEG-2.4.1-MacOSX.tar.gz';
244 PlugDesc(end).TestFile = 'libOpenMEEG.1.1.0.dylib';
245 case 'mac64arm'
246 PlugDesc(end).Version = '2.5.8';
247 PlugDesc(end).URLzip = ['https://github.com/openmeeg/openmeeg/releases/download/', PlugDesc(end).Version, '/OpenMEEG-', PlugDesc(end).Version, '-', 'macOS_M1.tar.gz'];
248 PlugDesc(end).TestFile = 'libOpenMEEG.1.1.0.dylib';
249 case 'win32'
250 PlugDesc(end).URLzip = 'https://files.inria.fr/OpenMEEG/download/release-2.2/OpenMEEG-2.2.0-win32-x86-cl-OpenMP-shared.tar.gz';
251 PlugDesc(end).TestFile = 'om_assemble.exe';
252 case 'win64'
253 PlugDesc(end).URLzip = 'https://files.inria.fr/OpenMEEG/download/OpenMEEG-2.4.1-Win64.tar.gz';
254 PlugDesc(end).TestFile = 'om_assemble.exe';
255 end
256 PlugDesc(end).URLinfo = 'https://openmeeg.github.io/';
257 PlugDesc(end).ExtraMenus = {'Alternate versions', 'web(''https://files.inria.fr/OpenMEEG/download/'', ''-browser'')'; ...
258 'Download Visual C++', 'web(''https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170'', ''-browser'')'; ...
259 'Online tutorial', 'web(''https://neuroimage.usc.edu/brainstorm/Tutorials/TutBem'', ''-browser'')'};
260 PlugDesc(end).CompiledStatus = 1;
261 PlugDesc(end).LoadFolders = {'bin', 'lib'};
262
263 % === FORWARD: DUNEURO ===
264 PlugDesc(end+1) = GetStruct('duneuro');
265 PlugDesc(end).Version = 'latest';
266 PlugDesc(end).Category = 'Forward';
267 PlugDesc(end).AutoUpdate = 1;
268 PlugDesc(end).URLzip = 'https://neuroimage.usc.edu/bst/getupdate.php?d=bst_duneuro.zip';
269 PlugDesc(end).URLinfo = 'https://neuroimage.usc.edu/brainstorm/Tutorials/Duneuro';
270 PlugDesc(end).TestFile = 'bst_duneuro_meeg_win64.exe';
271 PlugDesc(end).CompiledStatus = 1;
272 PlugDesc(end).LoadFolders = {'bin'};
273
274 % === INVERSE: BRAINENTROPY ===
275 PlugDesc(end+1) = GetStruct('brainentropy');
276 PlugDesc(end).Version = 'github-master';
277 PlugDesc(end).Category = 'Inverse';
278 PlugDesc(end).AutoUpdate = 1;
279 PlugDesc(end).URLzip = 'https://github.com/multi-funkim/best-brainstorm/archive/master.zip';
280 PlugDesc(end).URLinfo = 'https://neuroimage.usc.edu/brainstorm/Tutorials/TutBEst';
281 PlugDesc(end).TestFile = 'process_inverse_mem.m';
282 PlugDesc(end).AutoLoad = 1;
283 PlugDesc(end).CompiledStatus = 2;
284 PlugDesc(end).LoadFolders = {'*'};
285 PlugDesc(end).GetVersionFcn = @be_versions;
286 PlugDesc(end).DeleteFiles = {'docs', '.github'};
287
288 % === I/O: ADI-SDK === ADInstrument SDK for reading LabChart files
289 PlugDesc(end+1) = GetStruct('adi-sdk');
290 PlugDesc(end).Version = 'github-master';
291 PlugDesc(end).Category = 'I/O';
292 switch (OsType)
293 case 'win64', PlugDesc(end).URLzip = 'https://github.com/JimHokanson/adinstruments_sdk_matlab/archive/master.zip';
294 end
295 PlugDesc(end).URLinfo = 'https://github.com/JimHokanson/adinstruments_sdk_matlab';
296 PlugDesc(end).TestFile = 'adi.m';
297 PlugDesc(end).CompiledStatus = 0;
298
299 % === I/O: AXION ===
300 PlugDesc(end+1) = GetStruct('axion');
301 PlugDesc(end).Version = '1.0';
302 PlugDesc(end).Category = 'I/O';
303 PlugDesc(end).URLzip = 'https://neuroimage.usc.edu/bst/getupdate.php?d=AxionBioSystems.zip';
304 PlugDesc(end).URLinfo = 'https://www.axionbiosystems.com/products/software/neural-module';
305 PlugDesc(end).TestFile = 'AxisFile.m';
306 % PlugDesc(end).ReadmeFile = 'README.md';
307 PlugDesc(end).CompiledStatus = 0;
308
309 % === I/O: BCI2000 ===
310 PlugDesc(end+1) = GetStruct('bci2000');
311 PlugDesc(end).Version = 'latest';
312 PlugDesc(end).Category = 'I/O';
313 PlugDesc(end).URLzip = 'https://bci2000.org/downloads/mex.zip';
314 PlugDesc(end).URLinfo = 'https://www.bci2000.org/mediawiki/index.php/User_Reference:Matlab_MEX_Files';
315 PlugDesc(end).TestFile = 'load_bcidat.m';
316 PlugDesc(end).CompiledStatus = 0;
317
318 % === I/O: BLACKROCK ===
319 PlugDesc(end+1) = GetStruct('blackrock');
320 PlugDesc(end).Version = 'github-master';
321 PlugDesc(end).Category = 'I/O';
322 PlugDesc(end).URLzip = 'https://github.com/BlackrockMicrosystems/NPMK/archive/master.zip';
323 PlugDesc(end).URLinfo = 'https://github.com/BlackrockMicrosystems/NPMK/blob/master/NPMK/Users%20Guide.pdf';
324 PlugDesc(end).TestFile = 'openNSx.m';
325 PlugDesc(end).CompiledStatus = 2;
326 PlugDesc(end).LoadFolders = {'*'};
327 PlugDesc(end).DeleteFiles = {'NPMK/installNPMK.m', 'NPMK/Users Guide.pdf', 'NPMK/Versions.txt', ...
328 'NPMK/@KTUEAImpedanceFile', 'NPMK/@KTNSPOnline', 'NPMK/@KTNEVComments', 'NPMK/@KTFigureAxis', 'NPMK/@KTFigure', 'NPMK/@KTUEAMapFile/.svn', ...
329 'NPMK/openNSxSync.m', 'NPMK/NTrode Utilities', 'NPMK/NSx Utilities', 'NPMK/NEV Utilities', 'NPMK/LoadingEngines', ...
330 'NPMK/Other tools/.svn', 'NPMK/Other tools/edgeDetect.m', 'NPMK/Other tools/kshuffle.m', 'NPMK/Other tools/openCCF.m', 'NPMK/Other tools/parseCCF.m', ...
331 'NPMK/Other tools/periEventPlot.asv', 'NPMK/Other tools/periEventPlot.m', 'NPMK/Other tools/playSound.m', ...
332 'NPMK/Dependent Functions/.svn', 'NPMK/Dependent Functions/.DS_Store', 'NPMK/Dependent Functions/bnsx.dat', 'NPMK/Dependent Functions/syncPatternDetectNEV.m', ...
333 'NPMK/Dependent Functions/syncPatternDetectNSx.m', 'NPMK/Dependent Functions/syncPatternFinderNSx.m'};
334
335 % === I/O: EASYH5 ===
336 PlugDesc(end+1) = GetStruct('easyh5');
337 PlugDesc(end).Version = 'github-master';
338 PlugDesc(end).Category = 'I/O';
339 PlugDesc(end).URLzip = 'https://github.com/NeuroJSON/easyh5/archive/master.zip';
340 PlugDesc(end).URLinfo = 'https://github.com/NeuroJSON/easyh5';
341 PlugDesc(end).TestFile = 'loadh5.m';
342 PlugDesc(end).CompiledStatus = 2;
343 PlugDesc(end).LoadFolders = {'*'};
344 PlugDesc(end).DeleteFiles = {'examples'};
345 PlugDesc(end).ReadmeFile = 'README.md';
346 PlugDesc(end).UnloadPlugs = {'iso2mesh'};
347
348 % === I/O: JNIfTI ===
349 PlugDesc(end+1) = GetStruct('jnifti');
350 PlugDesc(end).Version = '0.8';
351 PlugDesc(end).Category = 'I/O';
352 PlugDesc(end).AutoUpdate = 1;
353 PlugDesc(end).URLzip = 'https://github.com/NeuroJSON/jnifty/archive/refs/tags/v0.8.zip';
354 PlugDesc(end).URLinfo = 'https://github.com/NeuroJSON/jnifty';
355 PlugDesc(end).TestFile = 'jnii2nii.m';
356 PlugDesc(end).ReadmeFile = 'README.txt';
357 PlugDesc(end).CompiledStatus = 2;
358 PlugDesc(end).LoadedFcn = '';
359 PlugDesc(end).RequiredPlugs = {'jsonlab'};
360
361 % === I/O: JSNIRFY ===
362 PlugDesc(end+1) = GetStruct('jsnirfy');
363 PlugDesc(end).Version = 'github-master';
364 PlugDesc(end).Category = 'I/O';
365 PlugDesc(end).URLzip = 'https://github.com/NeuroJSON/jsnirfy/archive/master.zip';
366 PlugDesc(end).URLinfo = 'https://github.com/NeuroJSON/jsnirfy';
367 PlugDesc(end).TestFile = 'loadsnirf.m';
368 PlugDesc(end).CompiledStatus = 2;
369 PlugDesc(end).LoadFolders = {'*'};
370 PlugDesc(end).DeleteFiles = {'external', '.gitmodules'};
371 PlugDesc(end).ReadmeFile = 'README.md';
372 PlugDesc(end).RequiredPlugs = {'easyh5'; 'jsonlab'};
373 PlugDesc(end).UnloadPlugs = {'iso2mesh'};
374
375 % === I/O: JSONLab ===
376 PlugDesc(end+1) = GetStruct('jsonlab');
377 PlugDesc(end).Version = 'github-master';
378 PlugDesc(end).Category = 'I/O';
379 PlugDesc(end).URLzip = 'https://github.com/NeuroJSON/jsonlab/archive/refs/heads/master.zip';
380 PlugDesc(end).URLinfo = 'https://neurojson.org/jsonlab';
381 PlugDesc(end).TestFile = 'savejson.m';
382 PlugDesc(end).CompiledStatus = 2;
383 PlugDesc(end).LoadFolders = {'*'};
384 PlugDesc(end).DeleteFiles = {'examples', 'images', 'test', '.github', '.gitignore'};
385 PlugDesc(end).ReadmeFile = 'README.rst';
386 PlugDesc(end).UnloadPlugs = {'iso2mesh'};
387
388 % === I/O: MFF ===
389 PlugDesc(end+1) = GetStruct('mff');
390 PlugDesc(end).Version = 'github-master';
391 PlugDesc(end).Category = 'I/O';
392 PlugDesc(end).AutoUpdate = 0;
393 PlugDesc(end).URLzip = 'https://github.com/arnodelorme/mffmatlabio/archive/master.zip';
394 PlugDesc(end).URLinfo = 'https://github.com/arnodelorme/mffmatlabio';
395 PlugDesc(end).TestFile = 'eegplugin_mffmatlabio.m';
396 PlugDesc(end).ReadmeFile = 'README.md';
397 PlugDesc(end).MinMatlabVer = 803; % 2014a
398 PlugDesc(end).CompiledStatus = 0;
399 PlugDesc(end).LoadedFcn = @Configure;
400 % Stable version: https://neuroimage.usc.edu/bst/getupdate.php?d='mffmatlabio-3.5.zip'
401
402 % === I/O: NEUROELECTRICS ===
403 PlugDesc(end+1) = GetStruct('neuroelectrics');
404 PlugDesc(end).Version = '1.8';
405 PlugDesc(end).Category = 'I/O';
406 PlugDesc(end).AutoUpdate = 0;
407 PlugDesc(end).URLzip = 'https://sccn.ucsd.edu/eeglab/plugins/Neuroelectrics1.8.zip';
408 PlugDesc(end).URLinfo = 'https://www.neuroelectrics.com/wiki/index.php/EEGLAB';
409 PlugDesc(end).TestFile = 'pop_nedf.m';
410 PlugDesc(end).ReadmeFile = 'README.txt';
411 PlugDesc(end).CompiledStatus = 2;
412 PlugDesc(end).InstalledFcn = ['d=pwd; cd(fileparts(which(''pop_nedf''))); mkdir(''private''); ' ...
413 'f=fopen(''private' filesep 'eeg_emptyset.m'',''wt''); fprintf(f,''function EEG=eeg_emptyset()\nEEG=struct();''); fclose(f);' ...
414 'f=fopen(''private' filesep 'eeg_checkset.m'',''wt''); fprintf(f,''function EEG=eeg_checkset(EEG)''); fclose(f);' ...
415 'cd(d);'];
416
417 % === I/O: npy-matlab ===
418 PlugDesc(end+1) = GetStruct('npy-matlab');
419 PlugDesc(end).Version = 'github-master';
420 PlugDesc(end).Category = 'I/O';
421 PlugDesc(end).URLzip = 'https://github.com/kwikteam/npy-matlab/archive/refs/heads/master.zip';
422 PlugDesc(end).URLinfo = 'https://github.com/kwikteam/npy-matlab';
423 PlugDesc(end).TestFile = 'constructNPYheader.m';
424 PlugDesc(end).LoadFolders = {'*'};
425 PlugDesc(end).ReadmeFile = 'README.md';
426 PlugDesc(end).CompiledStatus = 0;
427
428 % === I/O: NWB ===
429 PlugDesc(end+1) = GetStruct('nwb');
430 PlugDesc(end).Version = 'github-master';
431 PlugDesc(end).Category = 'I/O';
432 PlugDesc(end).URLzip = 'https://github.com/NeurodataWithoutBorders/matnwb/archive/master.zip';
433 PlugDesc(end).URLinfo = 'https://github.com/NeurodataWithoutBorders/matnwb';
434 PlugDesc(end).TestFile = 'nwbRead.m';
435 PlugDesc(end).ReadmeFile = 'README.md';
436 PlugDesc(end).MinMatlabVer = 901; % 2016b
437 PlugDesc(end).CompiledStatus = 0;
438 PlugDesc(end).LoadFolders = {'*'};
439 PlugDesc(end).LoadedFcn = @Configure;
440
441 % === I/O: PLEXON ===
442 PlugDesc(end+1) = GetStruct('plexon');
443 PlugDesc(end).Version = '1.8.4';
444 PlugDesc(end).Category = 'I/O';
445 PlugDesc(end).URLzip = 'https://plexon-prod.s3.amazonaws.com/wp-content/uploads/2017/08/OmniPlex-and-MAP-Offline-SDK-Bundle_0.zip';
446 PlugDesc(end).URLinfo = 'https://plexon.com/software-downloads/#software-downloads-SDKs';
447 PlugDesc(end).TestFile = 'plx_info.m';
448 PlugDesc(end).ReadmeFile = 'Change Log.txt';
449 PlugDesc(end).CompiledStatus = 0;
450 PlugDesc(end).DownloadedFcn = ['d = fullfile(PlugDesc.Path, ''OmniPlex and MAP Offline SDK Bundle'');' ...
451 'unzip(fullfile(d, ''Matlab Offline Files SDK.zip''), PlugDesc.Path);' ...
452 'file_delete(d,1,3);'];
453 PlugDesc(end).InstalledFcn = ['if (exist(''mexPlex'', ''file'') ~= 3), d = pwd;' ...
454 'cd(fullfile(fileparts(which(''plx_info'')), ''mexPlex''));', ...
455 'build_and_verify_mexPlex; cd(d); end'];
456
457 % === I/O: PLOTLY ===
458 PlugDesc(end+1) = GetStruct('plotly');
459 PlugDesc(end).Version = 'github-master';
460 PlugDesc(end).Category = 'I/O';
461 PlugDesc(end).URLzip = 'https://github.com/plotly/plotly_matlab/archive/master.zip';
462 PlugDesc(end).URLinfo = 'https://plotly.com/matlab/';
463 PlugDesc(end).TestFile = 'plotlysetup_online.m';
464 PlugDesc(end).ReadmeFile = 'README.mkdn';
465 PlugDesc(end).CompiledStatus = 0;
466 PlugDesc(end).LoadFolders = {'*'};
467 PlugDesc(end).ExtraMenus = {'Online tutorial', 'web(''https://neuroimage.usc.edu/brainstorm/Tutorials/Plotly'', ''-browser'')'};
468
469 % === I/O: TDT-SDK === Tucker-Davis Technologies Matlab SDK
470 PlugDesc(end+1) = GetStruct('tdt-sdk');
471 PlugDesc(end).Version = 'latest';
472 PlugDesc(end).Category = 'I/O';
473 PlugDesc(end).URLzip = 'https://www.tdt.com/files/examples/TDTMatlabSDK.zip';
474 PlugDesc(end).URLinfo = 'https://www.tdt.com/support/matlab-sdk/';
475 PlugDesc(end).TestFile = 'TDT_Matlab_Tools.pdf';
476 PlugDesc(end).CompiledStatus = 0;
477 PlugDesc(end).LoadFolders = {'*'};
478
479 % === I/O: XDF ===
480 PlugDesc(end+1) = GetStruct('xdf');
481 PlugDesc(end).Version = 'github-master';
482 PlugDesc(end).Category = 'I/O';
483 PlugDesc(end).AutoUpdate = 0;
484 PlugDesc(end).URLzip = 'https://github.com/xdf-modules/xdf-Matlab/archive/refs/heads/master.zip';
485 PlugDesc(end).URLinfo = 'https://github.com/xdf-modules/xdf-Matlab';
486 PlugDesc(end).TestFile = 'load_xdf.m';
487 PlugDesc(end).ReadmeFile = 'readme.md';
488 PlugDesc(end).CompiledStatus = 2;
489
490 % === SIMULATION: SIMMEEG ===
491 PlugDesc(end+1) = GetStruct('simmeeg');
492 PlugDesc(end).Version = 'github-master';
493 PlugDesc(end).Category = 'Simulation';
494 PlugDesc(end).AutoUpdate = 1;
495 PlugDesc(end).URLzip = 'https://github.com/branelab/SimMEEG/archive/master.zip';
496 PlugDesc(end).URLinfo = 'https://audiospeech.ubc.ca/research/brane/brane-lab-software/';
497 PlugDesc(end).TestFile = 'SimMEEG_GUI.m';
498 PlugDesc(end).ReadmeFile = 'SIMMEEG_TERMS_OF_USE.txt';
499 PlugDesc(end).CompiledStatus = 0;
500 PlugDesc(end).RequiredPlugs = {'fieldtrip', '20200911'};
501
502
503 % === STATISTICS: FASTICA ===
504 PlugDesc(end+1) = GetStruct('fastica');
505 PlugDesc(end).Version = '2.5';
506 PlugDesc(end).Category = 'Statistics';
507 PlugDesc(end).URLzip = 'https://research.ics.aalto.fi/ica/fastica/code/FastICA_2.5.zip';
508 PlugDesc(end).URLinfo = 'https://research.ics.aalto.fi/ica/fastica/';
509 PlugDesc(end).TestFile = 'fastica.m';
510 PlugDesc(end).ReadmeFile = 'Contents.m';
511 PlugDesc(end).CompiledStatus = 2;
512
513 % === STATISTICS: LIBSVM ===
514 PlugDesc(end+1) = GetStruct('libsvm');
515 PlugDesc(end).Version = 'github-master';
516 PlugDesc(end).Category = 'Statistics';
517 PlugDesc(end).URLzip = 'https://github.com/cjlin1/libsvm/archive/master.zip';
518 PlugDesc(end).URLinfo = 'https://www.csie.ntu.edu.tw/~cjlin/libsvm/';
519 PlugDesc(end).TestFile = 'svm.cpp';
520 PlugDesc(end).ReadmeFile = 'README';
521 PlugDesc(end).MinMatlabVer = 803; % 2014a
522 PlugDesc(end).CompiledStatus = 2;
523 PlugDesc(end).LoadFolders = {'*'};
524 PlugDesc(end).InstalledFcn = 'd=pwd; cd(fileparts(which(''make''))); make; cd(d);';
525
526 % === STATISTICS: mTRF ===
527 PlugDesc(end+1) = GetStruct('mtrf');
528 PlugDesc(end).Version = '2.4';
529 PlugDesc(end).Category = 'Statistics';
530 PlugDesc(end).URLzip = 'https://github.com/mickcrosse/mTRF-Toolbox/archive/refs/tags/v2.4.zip';
531 PlugDesc(end).URLinfo = 'https://github.com/mickcrosse/mTRF-Toolbox';
532 PlugDesc(end).TestFile = 'mTRFtrain.m';
533 PlugDesc(end).ReadmeFile = 'README.md';
534 PlugDesc(end).CompiledStatus = 0;
535 PlugDesc(end).LoadFolders = {'mtrf'};
536 PlugDesc(end).DeleteFiles = {'.gitattributes', '.github/ISSUE_TEMPLATE', 'data', 'doc', 'examples', 'img'};
537
538 % === STATISTICS: MVGC ===
539 PlugDesc(end+1) = GetStruct('mvgc');
540 PlugDesc(end).Version = '1.3';
541 PlugDesc(end).Category = 'Statistics';
542 PlugDesc(end).URLzip = 'https://github.com/lcbarnett/MVGC1/archive/refs/tags/v1.3.zip';
543 PlugDesc(end).URLinfo = 'https://github.com/lcbarnett/MVGC1';
544 PlugDesc(end).TestFile = 'startup_mvgc.m';
545 PlugDesc(end).ReadmeFile = 'README.md';
546 PlugDesc(end).CompiledStatus = 2;
547 PlugDesc(end).LoadFolders = {''};
548 PlugDesc(end).DeleteFiles = {'C', 'deprecated', 'utils/legacy', 'maintainer'};
549 PlugDesc(end).DownloadedFcn = ['file_move( fullfile(PlugDesc.Path, [''MVGC1-'', PlugDesc.Version], ''startup.m''), ' ...
550 'fullfile(PlugDesc.Path, [''MVGC1-'', PlugDesc.Version], ''startup_mvgc.m''));' ...
551 'file_delete(fullfile(PlugDesc.Path, [''MVGC1-'', PlugDesc.Version], ''demo'', ''mvgc_demo.m''), 1);', ...
552 'file_copy( fullfile(PlugDesc.Path, [''MVGC1-'', PlugDesc.Version], ''demo'', ''mvgc_demo_statespace.m''), ' ...
553 'fullfile(PlugDesc.Path, [''MVGC1-'', PlugDesc.Version], ''demo'', ''mvgc_demo.m''));' ];
554 PlugDesc(end).LoadedFcn = 'startup_mvgc;';
555
556 % === STATISTICS: PICARD ===
557 PlugDesc(end+1) = GetStruct('picard');
558 PlugDesc(end).Version = 'github-master';
559 PlugDesc(end).Category = 'Statistics';
560 PlugDesc(end).URLzip = 'https://github.com/pierreablin/picard/archive/refs/heads/master.zip';
561 PlugDesc(end).URLinfo = 'https://github.com/pierreablin/picard';
562 PlugDesc(end).TestFile = 'picard.m';
563 PlugDesc(end).ReadmeFile = 'README.rst';
564 PlugDesc(end).CompiledStatus = 2;
565 PlugDesc(end).LoadFolders = {'matlab_octave'};
566
567 % === ELECTROPHYSIOLOGY: DERIVELFP ===
568 PlugDesc(end+1) = GetStruct('derivelfp');
569 PlugDesc(end).Version = '1.0';
570 PlugDesc(end).Category = 'e-phys';
571 PlugDesc(end).AutoUpdate = 0;
572 PlugDesc(end).URLzip = 'https://packlab.mcgill.ca/despikingtoolbox.zip';
573 PlugDesc(end).URLinfo = 'https://journals.physiology.org/doi/full/10.1152/jn.00642.2010';
574 PlugDesc(end).TestFile = 'despikeLFP.m';
575 PlugDesc(end).ReadmeFile = 'readme.txt';
576 PlugDesc(end).CompiledStatus = 2;
577 PlugDesc(end).LoadFolders = {'toolbox'};
578 PlugDesc(end).DeleteFiles = {'ExampleDespiking.m', 'appendixpaper.pdf', 'downsample2x.m', 'examplelfpdespiking.mat', 'sta.m', ...
579 'toolbox/delineSignal.m', 'toolbox/despikeLFPbyChunks.asv', 'toolbox/despikeLFPbyChunks.m'};
580
581 % === ELECTROPHYSIOLOGY: Kilosort ===
582 PlugDesc(end+1) = GetStruct('kilosort');
583 PlugDesc(end).Version = 'github-master';
584 PlugDesc(end).Category = 'e-phys';
585 PlugDesc(end).URLzip = 'https://github.com/cortex-lab/KiloSort/archive/refs/heads/master.zip';
586 PlugDesc(end).URLinfo = 'https://papers.nips.cc/paper/2016/hash/1145a30ff80745b56fb0cecf65305017-Abstract.html';
587 PlugDesc(end).TestFile = 'fitTemplates.m';
588 PlugDesc(end).ReadmeFile = 'readme.md';
589 PlugDesc(end).CompiledStatus = 0;
590 PlugDesc(end).LoadFolders = {'*'};
591 PlugDesc(end).RequiredPlugs = {'kilosort-wrapper'; 'phy'; 'npy-matlab'};
592 PlugDesc(end).InstalledFcn = 'process_spikesorting_kilosort(''copyKilosortConfig'', bst_fullfile(bst_get(''UserPluginsDir''), ''kilosort'', ''KiloSort-master'', ''configFiles'', ''StandardConfig_MOVEME.m''), bst_fullfile(bst_get(''UserPluginsDir''), ''kilosort'', ''KiloSort-master'', ''KilosortStandardConfig.m''));';
593
594
595 % === ELECTROPHYSIOLOGY: Kilosort Wrapper ===
596 PlugDesc(end+1) = GetStruct('kilosort-wrapper');
597 PlugDesc(end).Version = 'github-master';
598 PlugDesc(end).Category = 'e-phys';
599 PlugDesc(end).URLzip = 'https://github.com/brendonw1/KilosortWrapper/archive/refs/heads/master.zip';
600 PlugDesc(end).URLinfo = 'https://zenodo.org/record/3604165';
601 PlugDesc(end).TestFile = 'Kilosort2Neurosuite.m';
602 PlugDesc(end).ReadmeFile = 'README.md';
603 PlugDesc(end).CompiledStatus = 0;
604
605 % === ELECTROPHYSIOLOGY: phy ===
606 PlugDesc(end+1) = GetStruct('phy');
607 PlugDesc(end).Version = 'github-master';
608 PlugDesc(end).Category = 'e-phys';
609 PlugDesc(end).URLzip = 'https://github.com/cortex-lab/phy/archive/refs/heads/master.zip';
610 PlugDesc(end).URLinfo = 'https://phy.readthedocs.io/en/latest/';
611 PlugDesc(end).TestFile = 'feature_view_custom_grid.py';
612 PlugDesc(end).LoadFolders = {'*'};
613 PlugDesc(end).ReadmeFile = 'README.md';
614 PlugDesc(end).CompiledStatus = 0;
615 PlugDesc(end).RequiredPlugs = {'npy-matlab'};
616
617 % === ELECTROPHYSIOLOGY: ultramegasort2000 ===
618 PlugDesc(end+1) = GetStruct('ultramegasort2000');
619 PlugDesc(end).Version = 'github-master';
620 PlugDesc(end).Category = 'e-phys';
621 PlugDesc(end).URLzip = 'https://github.com/danamics/UMS2K/archive/refs/heads/master.zip';
622 PlugDesc(end).URLinfo = 'https://github.com/danamics/UMS2K/blob/master/UltraMegaSort2000%20Manual.pdf';
623 PlugDesc(end).TestFile = 'UltraMegaSort2000 Manual.pdf';
624 PlugDesc(end).LoadFolders = {'*'};
625 PlugDesc(end).ReadmeFile = 'README.md';
626 PlugDesc(end).CompiledStatus = 0;
627
628 % === ELECTROPHYSIOLOGY: waveclus ===
629 PlugDesc(end+1) = GetStruct('waveclus');
630 PlugDesc(end).Version = 'github-master';
631 PlugDesc(end).Category = 'e-phys';
632 PlugDesc(end).URLzip = 'https://github.com/csn-le/wave_clus/archive/refs/heads/master.zip';
633 PlugDesc(end).URLinfo = 'https://journals.physiology.org/doi/full/10.1152/jn.00339.2018';
634 PlugDesc(end).TestFile = 'wave_clus.m';
635 PlugDesc(end).LoadFolders = {'*'};
636 PlugDesc(end).ReadmeFile = 'README.md';
637 PlugDesc(end).CompiledStatus = 0;
638
639 % === fNIRS: NIRSTORM ===
640 PlugDesc(end+1) = GetStruct('nirstorm');
641 PlugDesc(end).Version = 'github-master';
642 PlugDesc(end).Category = 'fNIRS';
643 PlugDesc(end).AutoUpdate = 0;
644 PlugDesc(end).AutoLoad = 1;
645 PlugDesc(end).CompiledStatus = 2;
646 PlugDesc(end).URLzip = 'https://github.com/Nirstorm/nirstorm/archive/master.zip';
647 PlugDesc(end).URLinfo = 'https://github.com/Nirstorm/nirstorm';
648 PlugDesc(end).LoadFolders = {'bst_plugin/core','bst_plugin/forward','bst_plugin/GLM', 'bst_plugin/inverse' , 'bst_plugin/io','bst_plugin/math' ,'bst_plugin/mbll' ,'bst_plugin/misc', 'bst_plugin/OM', 'bst_plugin/preprocessing', 'bst_plugin/ppl'};
649 PlugDesc(end).TestFile = 'process_nst_mbll.m';
650 PlugDesc(end).ReadmeFile = 'README.md';
651 PlugDesc(end).GetVersionFcn = 'nst_get_version';
652 PlugDesc(end).RequiredPlugs = {'brainentropy'};
653 PlugDesc(end).MinMatlabVer = 803; % 2014a
654 PlugDesc(end).DeleteFiles = {'scripts', 'test', 'run_tests.m', 'test_suite_bak.m', '.gitignore'};
655
656 % === fNIRS: MCXLAB CUDA ===
657 PlugDesc(end+1) = GetStruct('mcxlab-cuda');
658 PlugDesc(end).Version = '2024.07.23';
659 PlugDesc(end).Category = 'fNIRS';
660 PlugDesc(end).AutoUpdate = 1;
661 PlugDesc(end).URLzip = 'https://mcx.space/nightly/release/git20240723/mcxlab-allinone-git20240723.zip';
662 PlugDesc(end).TestFile = 'mcxlab.m';
663 PlugDesc(end).URLinfo = 'https://mcx.space/wiki/';
664 PlugDesc(end).CompiledStatus = 0;
665 PlugDesc(end).LoadFolders = {'*'};
666 PlugDesc(end).UnloadPlugs = {'mcxlab-cl'};
667
668 % === fNIRS: MCXLAB CL ===
669 PlugDesc(end+1) = GetStruct('mcxlab-cl');
670 PlugDesc(end).Version = '2024.07.23';
671 PlugDesc(end).Category = 'fNIRS';
672 PlugDesc(end).AutoUpdate = 0;
673 PlugDesc(end).URLzip = 'https://mcx.space/nightly/release/git20240723/mcxlabcl-allinone-git20240723.zip';
674 PlugDesc(end).TestFile = 'mcxlabcl.m';
675 PlugDesc(end).URLinfo = 'https://mcx.space/wiki/';
676 PlugDesc(end).CompiledStatus = 2;
677 PlugDesc(end).LoadFolders = {'*'};
678 PlugDesc(end).UnloadPlugs = {'mcxlab-cuda'};
679
680 % === sEEG: GARDEL ===
681 PlugDesc(end+1) = GetStruct('gardel');
682 PlugDesc(end).Version = 'latest';
683 PlugDesc(end).Category = 'sEEG';
684 PlugDesc(end).URLzip = 'https://gitlab-dynamap.timone.univ-amu.fr/public_shared_tools/gardel/-/archive/GARDEL_Brainstorm/gardel-GARDEL_Brainstorm.zip';
685 PlugDesc(end).URLinfo = 'https://gitlab-dynamap.timone.univ-amu.fr/public_shared_tools/gardel/-/wikis/home';
686 PlugDesc(end).TestFile = 'code/Functions/elec_auto_segmentation.m';
687 PlugDesc(end).ReadmeFile = 'README.md';
688 PlugDesc(end).CompiledStatus = 2;
689 PlugDesc(end).RequiredPlugs = {'spm12'};
690 PlugDesc(end).LoadFolders = {'*'};
691 PlugDesc(end).InstalledFcn = {};
692 PlugDesc(end).DeleteFiles = {'gardel_splash.png', 'gardel_splash_480x480.png', 'gardel_splash480x480.svg', ...
693 'code/Functions/associate_matter.m', 'code/Functions/associate_region.m', 'code/Functions/BN_colorlut2complete_descr.m', ...
694 'code/Functions/coregistration.m', 'code/Functions/find_parsing_json.m', 'code/Functions/First_SPM_reorientation.m', ...
695 'code/Functions/make_bids_filename.m', 'code/Functions/new_brainmasking_to_be_compiled.m', 'code/Functions/NEW_SEG2.m', ...
696 'code/Functions/open_electrodes_file.m', 'code/Functions/open_nii_anatomical_convention.m', 'code/Functions/plot_brain.m', ...
697 'code/Functions/read_BNatlas.m', 'code/Functions/read_fscolorlut.m', 'code/Functions/Segmentation.asv', 'code/Functions/stlwrite.m', ...
698 'code/Functions/write_coordsystem_json.m', 'code/Functions/write2Brainstorm.m', 'code/Functions/write2MNI.m', ...
699 'code/toolboxes/NIFTI_dataViewer', 'code/toolboxes/freezeColors', 'code/toolboxes/dicm2nii', ...
700 'code/.gitkeep', 'code/BN_Atlas_246_LUT_sam.txt', 'code/CTthresholdGUI.fig', 'code/CTthresholdGUI.m', 'code/electrode_creation.m', ...
701 'code/electrodes_profiles.json', 'code/GARDEL User Manual_V2.docx', 'code/GARDEL User Manual_V2.pdf', 'code/GARDEL.fig', ...
702 'code/GARDEL.m', 'code/GARDELv2_bis.prj', 'code/MontageAnyWaveGardel.m', 'code/newtabMarsAtlas_SAM_2016.csv', ...
703 'code/save_bipolar_with_region.m', 'code/single_subj_T1.nii', 'code/VepFreeSurferColorLut.txt', 'code/write_localisations.m'};
704
705 % === sEEG: MIA ===
706 PlugDesc(end+1) = GetStruct('mia');
707 PlugDesc(end).Version = 'github-master';
708 PlugDesc(end).Category = 'sEEG';
709 PlugDesc(end).AutoUpdate = 0;
710 PlugDesc(end).AutoLoad = 1;
711 PlugDesc(end).CompiledStatus = 2;
712 PlugDesc(end).URLzip = 'https://github.com/MIA-iEEG/mia/archive/refs/heads/master.zip';
713 PlugDesc(end).URLinfo = 'https://www.neurotrack.fr/mia/';
714 PlugDesc(end).ReadmeFile = 'README.md';
715 PlugDesc(end).MinMatlabVer = 803; % 2014a
716 PlugDesc(end).LoadFolders = {'*'};
717 PlugDesc(end).TestFile = 'process_mia_export_db.m';
718 PlugDesc(end).ExtraMenus = {'Start MIA', 'mia', 'loaded'};
719
720 % === FIELDTRIP ===
721 PlugDesc(end+1) = GetStruct('fieldtrip');
722 PlugDesc(end).Version = 'latest';
723 PlugDesc(end).AutoUpdate = 0;
724 PlugDesc(end).URLzip = 'https://download.fieldtriptoolbox.org/fieldtrip-lite-20240405.zip';
725 PlugDesc(end).URLinfo = 'https://www.fieldtriptoolbox.org';
726 PlugDesc(end).TestFile = 'ft_defaults.m';
727 PlugDesc(end).ReadmeFile = 'README';
728 PlugDesc(end).CompiledStatus = 2;
729 PlugDesc(end).UnloadPlugs = {'spm12', 'roast'};
730 PlugDesc(end).LoadFolders = {'specest', 'preproc', 'forward', 'src', 'utilities'};
731 PlugDesc(end).GetVersionFcn = 'ft_version';
732 PlugDesc(end).LoadedFcn = ['global ft_default; ' ...
733 'ft_default = []; ' ...
734 'clear ft_defaults; ' ...
735 'if exist(''filtfilt'', ''file''), ft_default.toolbox.signal=''matlab''; end; ' ...
736 'if exist(''nansum'', ''file''), ft_default.toolbox.stats=''matlab''; end; ' ...
737 'if exist(''rgb2hsv'', ''file''), ft_default.toolbox.images=''matlab''; end; ' ...
738 'ft_defaults;'];
739
740 % === SPM12 ===
741 PlugDesc(end+1) = GetStruct('spm12');
742 PlugDesc(end).Version = 'latest';
743 PlugDesc(end).AutoUpdate = 0;
744 switch(OsType)
745 case 'mac64arm'
746 PlugDesc(end).URLzip = 'https://github.com/spm/spm12/archive/refs/heads/maint.zip';
747 PlugDesc(end).Version = 'github-maint';
748 otherwise
749 PlugDesc(end).Version = 'latest';
750 PlugDesc(end).URLzip = 'https://www.fil.ion.ucl.ac.uk/spm/download/restricted/eldorado/spm12.zip';
751 end
752 PlugDesc(end).URLinfo = 'https://www.fil.ion.ucl.ac.uk/spm/';
753 PlugDesc(end).TestFile = 'spm.m';
754 PlugDesc(end).ReadmeFile = 'README.md';
755 PlugDesc(end).CompiledStatus = 2;
756 PlugDesc(end).UnloadPlugs = {'fieldtrip', 'roast'};
757 PlugDesc(end).LoadFolders = {'matlabbatch'};
758 PlugDesc(end).GetVersionFcn = 'bst_getoutvar(2, @spm, ''Ver'')';
759 PlugDesc(end).LoadedFcn = 'spm(''defaults'',''EEG'');';
760
761 % === USER DEFINED PLUGINS ===
762 plugJsonFiles = dir(fullfile(bst_get('UserPluginsDir'), 'plugin_*.json'));
763 badJsonFiles = {};
764 plugUserDefNames = {};
765 for ix = 1:length(plugJsonFiles)
766 plugJsonText = fileread(fullfile(plugJsonFiles(ix).folder, plugJsonFiles(ix).name));
767 try
768 PlugUserDesc = bst_jsondecode(plugJsonText);
769 catch
770 badJsonFiles{end+1} = plugJsonFiles(ix).name;
771 continue
772 end
773 % Reshape fields "ExtraMenus"
774 if isfield(PlugUserDesc, 'ExtraMenus') && ~isempty(PlugUserDesc.ExtraMenus) && iscell(PlugUserDesc.ExtraMenus{1})
775 PlugUserDesc.ExtraMenus = cat(2, PlugUserDesc.ExtraMenus{:})';
776 end
777 % Reshape fields "RequiredPlugs"
778 if isfield(PlugUserDesc, 'RequiredPlugs') && ~isempty(PlugUserDesc.RequiredPlugs) && iscell(PlugUserDesc.RequiredPlugs{1})
779 PlugUserDesc.RequiredPlugs = cat(2, PlugUserDesc.RequiredPlugs{:})';
780 end
781 % Check for uniqueness for user-defined plugin
782 if ~ismember(PlugUserDesc.Name, {PlugDesc.Name})
783 plugUserDefNames{end+1} = PlugUserDesc.Name;
784 PlugDesc(end+1) = struct_copy_fields(GetStruct(PlugUserDesc.Name), PlugUserDesc);
785 end
786 end
787 % Print info on user-defined plugins
788 if UserDefVerbose
789 if ~isempty(plugUserDefNames)
790 fprintf(['BST> User-defined plugins... ' strjoin(plugUserDefNames, ' ') '\n']);
791 end
792 for iBad = 1 : length(badJsonFiles)
793 fprintf(['BST> User-defined plugins, error reading .json file... ' badJsonFiles{iBad} '\n']);
794 end
795 end
796
797 % ================================================================================================================
798
799 % Select only one plugin
800 if ~isempty(SelPlug)
801 % Get plugin name
802 if ischar(SelPlug)
803 PlugName = SelPlug;
804 else
805 PlugName = SelPlug.Name;
806 end
807 % Find in the list of plugins
808 iPlug = find(strcmpi({PlugDesc.Name}, PlugName));
809 if ~isempty(iPlug)
810 PlugDesc = PlugDesc(iPlug);
811 else
812 PlugDesc = [];
813 end
814 end
815 end
816
817
818 %% ===== PLUGIN STRUCT =====
819 function s = GetStruct(PlugName)
820 s = db_template('PlugDesc');
821 s.Name = PlugName;
822 end
823
824
825 %% ===== ADD USER DEFINED PLUGIN DESCRIPTION =====
826 function [isOk, errMsg] = AddUserDefDesc(RegMethod, jsonLocation)
827 isOk = 1;
828 errMsg = '';
829 isInteractive = strcmp(RegMethod, 'manual') || nargin < 2 || isempty(jsonLocation);
830
831 % Get json file location from user
832 if ismember(RegMethod, {'file', 'url'}) && isInteractive
833 if strcmp(RegMethod, 'file')
834 jsonLocation = java_getfile('open', 'Plugin description JSON file...', '', 'single', 'files', {{'.json'}, 'Brainstorm plugin description (*.json)', 'JSON'}, 1);
835 elseif strcmp(RegMethod, 'url')
836 jsonLocation = java_dialog('input', 'Enter the URL the plugin description file (.json)', 'Plugin description JSON file...', [], '');
837 end
838 if isempty(jsonLocation)
839 return
840 end
841 res = java_dialog('question', ['Warning: This plugin has not been verified.' 10 ...
842 'Malicious plugins can alter your database, proceed with caution and only install plugins from trusted sources.' 10 ...
843 'If any unusual behavior occurs after installation, start by uninstalling the plugins.' 10 ...
844 'Are you sure you want to proceed?'], ...
845 'Warning', [], {'yes', 'no'});
846 if strcmp(res, 'no')
847 return
848 end
849 end
850
851 % Get plugin description
852 switch RegMethod
853 case 'file'
854 jsonText = fileread(jsonLocation);
855 try
856 PlugDesc = bst_jsondecode(jsonText);
857 catch
858 errMsg = sprintf(['Could not parse JSON file:' 10 '%s'], jsonLocation);
859 end
860
861 case 'url'
862 % Handle GitHub links, convert the link to load the raw content
863 if strcmp(jsonLocation(1:4),'http') && strcmp(jsonLocation(end-4:end),'.json')
864 if ~isempty(regexp(jsonLocation, '^http[s]*://github.com', 'once'))
865 jsonLocation = strrep(jsonLocation, 'github.com','raw.githubusercontent.com');
866 jsonLocation = strrep(jsonLocation, 'blob/', '');
867 end
868 end
869 jsonText = bst_webread(jsonLocation);
870 try
871 PlugDesc = bst_jsondecode(jsonText);
872 catch
873 errMsg = sprintf(['Could not parse JSON file at:' 10 '%s'], jsonLocation);
874 end
875
876 case 'manual'
877 % Get info for user-defined plugin description from user
878 res = java_dialog('input', { ['<HTML>Provide the <B>mandatory</B> fields for a user defined Brainstorm plugin<BR>' ...
879 'See this page for further details:<BR>' ...
880 '<FONT COLOR="#0000FF">https://neuroimage.usc.edu/brainstorm/Tutorials/Plugins</FONT>' ...
881 '<BR><BR>' ...
882 'Plugin name<BR>' ...
883 '<I><FONT color="#707070">EXAMPLE: bst-users</FONT></I>'], ...
884 ['<HTML>Version<BR>' ...
885 '<I><FONT color="#707070">EXAMPLE: github-main or 3.1.4</FONT></I>'], ...
886 ['<HTML>URL for zip<BR>' ...
887 '<I><FONT color="#707070">EXAMPLE: https://github.com/brainstorm-tools/bst-users/archive/refs/heads/master.zip</FONT></I>'], ...
888 ['<HTML>URL for information<BR>' ...
889 '<I><FONT color="#707070">EXAMPLE: https://github.com/brainstorm-tools/bst-users</FONT></I>']}, ...
890 'User defined plugin', [], {'', '', '', ''});
891 if isempty(res) || any(cellfun(@isempty,res))
892 return
893 end
894 PlugDesc.Name = lower(res{1});
895 PlugDesc.Version = res{2};
896 PlugDesc.URLzip = res{3};
897 PlugDesc.URLinfo = res{4};
898 end
899 if ~isempty(errMsg)
900 bst_error(errMsg);
901 isOk = 0;
902 return;
903 end
904
905 % Validate retrieved plugin description
906 if length(PlugDesc) > 1
907 errMsg = 'JSON file should contain only one plugin description';
908 elseif ~all(ismember({'Name', 'Version', 'URLzip', 'URLinfo'}, fieldnames(PlugDesc)))
909 errMsg = 'Plugin description must contain the fields ''Name'', ''Version'', ''URLzip'' and ''URLinfo''';
910 else
911 PlugDesc.Name = lower(PlugDesc.Name);
912 PlugDescs = GetSupported();
913 if ismember(PlugDesc.Name, {PlugDescs.Name})
914 errMsg = sprintf('Plugin ''%s'' already exist in Brainstorm', PlugDesc.Name);
915 end
916 end
917 if ~isempty(errMsg)
918 bst_error(errMsg);
919 isOk = 0;
920 return;
921 end
922 % Override category
923 PlugDesc.Category = 'User defined';
924
925 % Write validated JSON file
926 pluginJsonFileOut = fullfile(bst_get('UserPluginsDir'), sprintf('plugin_%s.json', file_standardize(PlugDesc.Name)));
927 fid = fopen(pluginJsonFileOut, 'wt');
928 jsonText = bst_jsonencode(PlugDesc, 0);
929 fprintf(fid, jsonText);
930 fclose(fid);
931
932 fprintf(1, 'BST> Plugin ''%s'' was added to ''User defined'' plugins\n', PlugDesc.Name);
933 end
934
935
936 %% ===== REMOVE USER DEFINED PLUGIN DESCRIPTION =====
937 function [isOk, errMsg] = RemoveUserDefDesc(PlugName)
938 isOk = 1;
939 errMsg = '';
940 if nargin < 1 || isempty(PlugName)
941 PlugDescs = GetSupported();
942 PlugDescs = PlugDescs(ismember({PlugDescs.Category}, 'User defined'));
943 PlugName = java_dialog('combo', 'Indicate the name of the plugin to remove:', 'Remove plugin from ''User defined'' list', [], {PlugDescs.Name});
944 end
945 if isempty(PlugName)
946 return
947 end
948 PlugDesc = GetSupported(PlugName);
949 if ~isempty(PlugDesc.Path) || file_exist(bst_fullfile(bst_get('UserPluginsDir'), PlugDesc.Name))
950 [isOk, errMsg] = Uninstall(PlugDesc.Name, 0);
951 end
952 % Delete json file
953 if isOk
954 isOk = file_delete(fullfile(bst_get('UserPluginsDir'), sprintf('plugin_%s.json', file_standardize(PlugDesc.Name))), 1);
955 end
956
957 fprintf(1, 'BST> Plugin ''%s'' was removed from ''User defined'' plugins\n', PlugDesc.Name);
958 end
959
960
961 %% ===== CONFIGURE PLUGIN =====
962 function Configure(PlugDesc)
963 switch (PlugDesc.Name)
964 case 'mff'
965 % Add .jar file to static classpath
966 if ~exist('com.egi.services.mff.api.MFFFactory', 'class')
967 jarList = dir(bst_fullfile(PlugDesc.Path, PlugDesc.SubFolder, 'MFF-*.jar'));
968 jarPath = bst_fullfile(PlugDesc.Path, PlugDesc.SubFolder, jarList(1).name);
969 disp(['BST> Adding to Java classpath: ' jarPath]);
970 warning off
971 javaaddpathstatic(jarPath);
972 javaaddpath(jarPath);
973 warning on
974 end
975
976 case 'nwb'
977 % Add .jar file to static classpath
978 if ~exist('Schema', 'class')
979 jarPath = bst_fullfile(PlugDesc.Path, PlugDesc.SubFolder, 'jar', 'schema.jar');
980 disp(['BST> Adding to Java classpath: ' jarPath]);
981 warning off
982 javaaddpathstatic(jarPath);
983 javaaddpath(jarPath);
984 warning on
985 schema = Schema();
986 end
987 % Go to NWB folder
988 curDir = pwd;
989 cd(bst_fullfile(PlugDesc.Path, PlugDesc.SubFolder));
990 % Generate the NWB Schema (must be executed from the NWB folder)
991 generateCore();
992 % Restore current directory
993 cd(curDir);
994 end
995 end
996
997
998 %% ===== GET ONLINE VERSION =====
999 % Get the latest online version of some plugins
1000 function [Version, URLzip] = GetVersionOnline(PlugName, URLzip, isCache)
1001 global GlobalData;
1002 Version = [];
1003 % Parse inputs
1004 if (nargin < 2) || isempty(URLzip)
1005 URLzip = [];
1006 end
1007 % Use cache by default, to avoid fetching online too many times the same info
1008 if (nargin < 3) || isempty(isCache)
1009 isCache = 1;
1010 end
1011 % No internet: skip
1012 if ~GlobalData.Program.isInternet
1013 return;
1014 end
1015 % Check for existing plugin cache
1016 strCache = [PlugName, '_online_', strrep(date,'-','')];
1017 if isCache && isfield(GlobalData.Program.PluginCache, strCache) && isfield(GlobalData.Program.PluginCache.(strCache), 'Version')
1018 Version = GlobalData.Program.PluginCache.(strCache).Version;
1019 URLzip = GlobalData.Program.PluginCache.(strCache).URLzip;
1020 return;
1021 end
1022 % Get version online
1023 try
1024 switch (PlugName)
1025 case 'spm12'
1026 bst_progress('text', ['Checking latest online version for ' PlugName '...']);
1027 disp(['BST> Checking latest online version for ' PlugName '...']);
1028 s = bst_webread('https://www.fil.ion.ucl.ac.uk/spm/download/spm12_updates/');
1029 if ~isempty(s)
1030 n = regexp(s,'spm12_updates_r(\d.*?)\.zip','tokens','once');
1031 if ~isempty(n) && ~isempty(n{1})
1032 Version = n{1};
1033 end
1034 end
1035 case 'cat12'
1036 bst_progress('text', ['Checking latest online version for ' PlugName '...']);
1037 disp(['BST> Checking latest online version for ' PlugName '...']);
1038 s = bst_webread('https://www.neuro.uni-jena.de/cat12/');
1039 if ~isempty(s)
1040 n = regexp(s,'cat12_r(\d.*?)\.zip','tokens');
1041 if ~isempty(n)
1042 Version = max(cellfun(@str2double, [n{:}]));
1043 Version = num2str(Version);
1044 end
1045 end
1046 case 'fieldtrip'
1047 bst_progress('text', ['Checking latest online version for ' PlugName '...']);
1048 disp(['BST> Checking latest online version for ' PlugName '...']);
1049 s = bst_webread('https://download.fieldtriptoolbox.org');
1050 if ~isempty(s)
1051 n = regexp(s,'fieldtrip-lite-(\d.*?)\.zip','tokens');
1052 if ~isempty(n)
1053 Version = max(cellfun(@str2double, [n{:}]));
1054 Version = num2str(Version);
1055 URLzip = ['https://download.fieldtriptoolbox.org/fieldtrip-lite-' Version '.zip'];
1056 end
1057 end
1058 case 'duneuro'
1059 bst_progress('text', ['Checking latest online version for ' PlugName '...']);
1060 disp(['BST> Checking latest online version for ' PlugName '...']);
1061 str = bst_webread('https://neuroimage.usc.edu/bst/getversion_duneuro.php');
1062 Version = str(1:6);
1063 case 'nirstorm'
1064 bst_progress('text', ['Checking latest online version for ' PlugName '...']);
1065 disp(['BST> Checking latest online version for ' PlugName '...']);
1066 str = bst_webread('https://raw.githubusercontent.com/Nirstorm/nirstorm/master/bst_plugin/VERSION');
1067 Version = strtrim(str(9:end));
1068 case 'brainentropy'
1069 bst_progress('text', ['Checking latest online version for ' PlugName '...']);
1070 disp(['BST> Checking latest online version for ' PlugName '...']);
1071 str = bst_webread('https://raw.githubusercontent.com/multifunkim/best-brainstorm/master/best/VERSION.txt');
1072 str = strsplit(str,'\n');
1073 Version = strtrim(str{1});
1074 otherwise
1075 % If downloading from github: Get last GitHub commit SHA
1076 if isGithubMaster(URLzip)
1077 Version = GetGithubCommit(URLzip);
1078 else
1079 return;
1080 end
1081 end
1082 % Executed only if the version was fetched successfully: Keep cached version
1083 GlobalData.Program.PluginCache.(strCache).Version = Version;
1084 GlobalData.Program.PluginCache.(strCache).URLzip = URLzip;
1085 catch
1086 disp(['BST> Error: Could not get online version for plugin: ' PlugName]);
1087 end
1088 end
1089
1090
1091 %% ===== IS GITHUB MASTER ======
1092 % Returns 1 if the URL is a github master/main branch
1093 function isMaster = isGithubMaster(URLzip)
1094 isMaster = strMatchEdge(URLzip, 'https://github.com/', 'start') && ...
1095 (strMatchEdge(URLzip, 'master.zip', 'end') || strMatchEdge(URLzip, 'main.zip', 'end'));
1096 end
1097
1098
1099 %% ===== GET GITHUB COMMIT =====
1100 % Get SHA of the GitHub HEAD commit
1101 function sha = GetGithubCommit(URLzip)
1102 zipUri = matlab.net.URI(URLzip);
1103 % Primary branch name: master or main
1104 [~, primaryBranch] = bst_fileparts(char(zipUri.Path(end)));
1105 % Default result
1106 sha = ['github-', primaryBranch];
1107 % Only available after Matlab 2016b (because of matlab.net.http.RequestMessage)
1108 if (bst_get('MatlabVersion') < 901)
1109 return;
1110 end
1111 % Try getting the SHA from the GitHub API
1112 try
1113 % Get GitHub repository path
1114 zipUri = matlab.net.URI(URLzip);
1115 gitUser = char(zipUri.Path(2));
1116 gitRepo = char(zipUri.Path(3));
1117 % Request last commit SHA with GitHub API
1118 apiUri = matlab.net.URI(['https://api.github.com/repos/' gitUser '/' gitRepo '/commits/' primaryBranch]);
1119 request = matlab.net.http.RequestMessage;
1120 request = request.addFields(matlab.net.http.HeaderField('Accept', 'application/vnd.github.VERSION.sha'));
1121 r = send(request, apiUri);
1122 sha = char(r.Body.Data);
1123 catch
1124 disp(['BST> Warning: Could not get GitHub version for URL: ' zipUrl]);
1125 end
1126 end
1127
1128
1129 %% ===== COMPARE VERSIONS =====
1130 % Returns: 0: v1==v2
1131 % -1: v1<v2
1132 % 1: v1>v2
1133 function res = CompareVersions(v1, v2)
1134 % Get numbers
1135 iNum1 = find(ismember(v1, '0123456789'));
1136 iNum2 = find(ismember(v2, '0123456789'));
1137 iDot1 = find(v1 == '.');
1138 iDot2 = find(v2 == '.');
1139 % Equality (or one input empty)
1140 if isequal(v1,v2) || isempty(v1) || isempty(v2)
1141 res = 0;
1142 % Only numbers
1143 elseif (length(iNum1) == length(v1)) && (length(iNum2) == length(v2))
1144 n1 = str2double(v1);
1145 n2 = str2double(v2);
1146 if (n1 > n2)
1147 res = 1;
1148 elseif (n1 < n2)
1149 res = -1;
1150 else
1151 res = 0;
1152 end
1153 % Format '1.2.3'
1154 elseif (~isempty(iDot1) || ~isempty(iDot2)) && ~isempty(iNum1) && ~isempty(iNum2)
1155 % Get subversions 1
1156 split1 = str_split(v1, '.');
1157 sub1 = [];
1158 for i = 1:length(split1)
1159 t = str2num(split1{i}(ismember(split1{i},'0123456789')));
1160 if ~isempty(t)
1161 sub1(end+1) = t;
1162 else
1163 break;
1164 end
1165 end
1166 % Get subversions 1
1167 split2 = str_split(v2, '.');
1168 sub2 = [];
1169 for i = 1:length(split2)
1170 t = str2num(split2{i}(ismember(split2{i},'0123456789')));
1171 if ~isempty(t)
1172 sub2(end+1) = t;
1173 else
1174 break;
1175 end
1176 end
1177 % Add extra zeros to the shortest (so that "1.2" is higher than "1")
1178 if (length(sub1) < length(sub2))
1179 tmp = sub1;
1180 sub1 = zeros(size(sub2));
1181 sub1(1:length(tmp)) = tmp;
1182 elseif (length(sub1) > length(sub2))
1183 tmp = sub2;
1184 sub2 = zeros(size(sub1));
1185 sub2(1:length(tmp)) = tmp;
1186 end
1187 % Compare number by number
1188 for i = 1:length(sub1)
1189 if (sub1(i) > sub2(i))
1190 res = 1;
1191 return;
1192 elseif (sub1(i) < sub2(i))
1193 res = -1;
1194 return;
1195 else
1196 res = 0;
1197 end
1198 end
1199 % Mixture of numbers and digits: natural sorting of strings
1200 else
1201 [s,I] = sort_nat({v1, v2});
1202 if (I(1) == 1)
1203 res = -1;
1204 else
1205 res = 1;
1206 end
1207 end
1208 end
1209
1210
1211 %% ===== EXECUTE CALLBACK =====
1212 function [isOk, errMsg] = ExecuteCallback(PlugDesc, f)
1213 isOk = 0;
1214 errMsg = '';
1215 if ~isempty(PlugDesc.(f))
1216 try
1217 if ischar(PlugDesc.(f))
1218 disp(['BST> Executing callback ' f ': ' PlugDesc.(f)]);
1219 eval(PlugDesc.(f));
1220 elseif isa(PlugDesc.(f), 'function_handle')
1221 disp(['BST> Executing callback ' f ': ' func2str(PlugDesc.(f))]);
1222 feval(PlugDesc.(f), PlugDesc);
1223 end
1224 catch
1225 errMsg = ['Error executing callback ' f ': ' 10 lasterr];
1226 return;
1227 end
1228 end
1229 isOk = 1;
1230 end
1231
1232
1233 %% ===== GET INSTALLED PLUGINS =====
1234 % USAGE: [PlugDesc, SearchPlugs] = bst_plugin('GetInstalled', PlugName/PlugDesc) % Get one installed plugin
1235 % [PlugDesc, SearchPlugs] = bst_plugin('GetInstalled') % Get all installed plugins
1236 function [PlugDesc, SearchPlugs] = GetInstalled(SelPlug)
1237 % Parse inputs
1238 if (nargin < 1) || isempty(SelPlug)
1239 SelPlug = [];
1240 end
1241
1242 % === DEFINE SEARCH LIST ===
1243 % Looking for a single plugin
1244 if ~isempty(SelPlug)
1245 SearchPlugs = GetSupported(SelPlug);
1246 % Looking for all supported plugins
1247 else
1248 SearchPlugs = GetSupported();
1249 end
1250 % Brainstorm plugin folder
1251 UserPluginsDir = bst_get('UserPluginsDir');
1252 % Custom plugin paths
1253 PluginCustomPath = bst_get('PluginCustomPath');
1254 % Matlab path
1255 matlabPath = str_split(path, pathsep);
1256 % Compiled distribution
1257 isCompiled = bst_iscompiled();
1258
1259 % === LOOK FOR SUPPORTED PLUGINS ===
1260 % Empty plugin structure
1261 PlugDesc = repmat(db_template('PlugDesc'), 0);
1262 % Look for each plugin in the search list
1263 for iSearch = 1:length(SearchPlugs)
1264 % Compiled: skip plugins that are not available
1265 if isCompiled && (SearchPlugs(iSearch).CompiledStatus == 0)
1266 continue;
1267 end
1268 % Theoretical plugin path
1269 PlugName = SearchPlugs(iSearch).Name;
1270 PlugPath = bst_fullfile(UserPluginsDir, PlugName);
1271 % Handle case symbolic link
1272 try
1273 PlugPath = builtin('_canonicalizepath', PlugPath);
1274 catch
1275 % Nothing here
1276 end
1277 % Check if test function is available in the Matlab path
1278 TestFilePath = GetTestFilePath(SearchPlugs(iSearch));
1279 % If installed software found in Matlab path
1280 if ~isempty(TestFilePath)
1281 % Register loaded plugin
1282 iPlug = length(PlugDesc) + 1;
1283 PlugDesc(iPlug) = SearchPlugs(iSearch);
1284 PlugDesc(iPlug).isLoaded = 1;
1285 % Check if the file is inside the Brainstorm user folder (where it is supposed to be) => Managed plugin
1286 if strMatchEdge(TestFilePath, PlugPath, 'start')
1287 PlugDesc(iPlug).isManaged = 1;
1288 % Process compiled together with Brainstorm
1289 elseif isCompiled && ~isempty(strfind(TestFilePath, ['.brainstorm' filesep 'plugins' filesep PlugName]))
1290 compiledDir = ['.brainstorm' filesep 'plugins' filesep PlugName];
1291 iPath = strfind(TestFilePath, compiledDir);
1292 PlugPath = [TestFilePath(1:iPath-2), filesep, compiledDir];
1293 % Otherwise: Custom installation
1294 else
1295 % If the test file was found in a defined subfolder: remove the subfolder from the plugin path
1296 PlugPath = TestFilePath;
1297 for iSub = 1:length(PlugDesc(iPlug).LoadFolders)
1298 subDir = strrep(PlugDesc(iPlug).LoadFolders{iSub}, '/', filesep);
1299 if (length(PlugPath) > length(subDir)) && isequal(PlugPath(end-length(subDir)+1:end), subDir)
1300 PlugPath = PlugPath(1:end - length(subDir) - 1);
1301 break;
1302 end
1303 end
1304 PlugDesc(iPlug).isManaged = 0;
1305 end
1306 PlugDesc(iPlug).Path = PlugPath;
1307 % Plugin installed: Managed by Brainstorm
1308 elseif isdir(PlugPath) && file_exist(bst_fullfile(PlugPath, 'plugin.mat'))
1309 iPlug = length(PlugDesc) + 1;
1310 PlugDesc(iPlug) = SearchPlugs(iSearch);
1311 PlugDesc(iPlug).Path = PlugPath;
1312 PlugDesc(iPlug).isLoaded = 0;
1313 PlugDesc(iPlug).isManaged = 1;
1314 % Plugin installed: Custom path
1315 elseif isfield(PluginCustomPath, PlugName) && ~isempty(PluginCustomPath.(PlugName)) && file_exist(PluginCustomPath.(PlugName))
1316 iPlug = length(PlugDesc) + 1;
1317 PlugDesc(iPlug) = SearchPlugs(iSearch);
1318 PlugDesc(iPlug).Path = PluginCustomPath.(PlugName);
1319 PlugDesc(iPlug).isLoaded = 0;
1320 PlugDesc(iPlug).isManaged = 0;
1321 end
1322 end
1323
1324 % === LOOK FOR UNREFERENCED PLUGINS ===
1325 % Compiled: do not look for unreferenced plugins
1326 if isCompiled
1327 PlugList = [];
1328 % Get a specific unlisted plugin
1329 elseif ~isempty(SelPlug)
1330 % Get plugin name
1331 if ischar(SelPlug)
1332 PlugName = lower(SelPlug);
1333 else
1334 PlugName = SelPlug.Name;
1335 end
1336 % If plugin is already referenced: skip
1337 if ismember(PlugName, {PlugDesc.Name})
1338 PlugList = [];
1339 % Else: Try to get target plugin as unreferenced
1340 else
1341 PlugList = struct('name', PlugName);
1342 end
1343 % Get all folders in Brainstorm plugins folder
1344 else
1345 PlugList = dir(UserPluginsDir);
1346 end
1347 % Process folders containing a plugin.mat file
1348 for iDir = 1:length(PlugList)
1349 % Ignore entry if plugin name is already in list of documented plugins
1350 PlugName = PlugList(iDir).name;
1351 if ismember(PlugName, {PlugDesc.Name})
1352 continue;
1353 end
1354 % Process only folders
1355 PlugDir = bst_fullfile(UserPluginsDir, PlugName);
1356 if ~isdir(PlugDir) || (PlugName(1) == '.')
1357 continue;
1358 end
1359 % Process only folders containing a 'plugin.mat' file
1360 PlugMatFile = bst_fullfile(PlugDir, 'plugin.mat');
1361 if ~file_exist(PlugMatFile)
1362 continue;
1363 end
1364 % If selecting only one plugin
1365 if ~isempty(SelPlug) && ischar(SelPlug) && ~strcmpi(PlugName, SelPlug)
1366 continue;
1367 end
1368 % Add plugin to list
1369 iPlug = length(PlugDesc) + 1;
1370 PlugDesc(iPlug) = GetStruct(PlugList(iDir).name);
1371 PlugDesc(iPlug).Path = PlugDir;
1372 PlugDesc(iPlug).isManaged = 1;
1373 PlugDesc(iPlug).isLoaded = ismember(PlugDir, matlabPath);
1374 end
1375
1376 % === READ PLUGIN.MAT ===
1377 for iPlug = 1:length(PlugDesc)
1378 % Try to load the plugin.mat file in the plugin folder
1379 PlugMatFile = bst_fullfile(PlugDesc(iPlug).Path, 'plugin.mat');
1380 if file_exist(PlugMatFile)
1381 try
1382 PlugMat = load(PlugMatFile);
1383 catch
1384 PlugMat = struct();
1385 end
1386 % Copy fields
1387 excludedFields = {'Name', 'Path', 'isLoaded', 'isManaged', 'LoadedFcn', 'UnloadedFcn', 'DownloadedFcn', 'InstalledFcn', 'UninstalledFcn'};
1388 loadFields = setdiff(fieldnames(db_template('PlugDesc')), excludedFields);
1389 for iField = 1:length(loadFields)
1390 if isfield(PlugMat, loadFields{iField}) && ~isempty(PlugMat.(loadFields{iField}))
1391 PlugDesc(iPlug).(loadFields{iField}) = PlugMat.(loadFields{iField});
1392 end
1393 end
1394 else
1395 PlugDesc(iPlug).URLzip = [];
1396 end
1397 end
1398 end
1399
1400
1401 %% ===== GET LOADED PLUGINS =====
1402 % USAGE: [PlugDesc, SearchPlugs] = bst_plugin('GetLoaded')
1403 function PlugDesc = GetLoaded()
1404 PlugDesc = GetInstalled();
1405 PlugDesc = PlugDesc([PlugDesc.isLoaded] == 1);
1406 end
1407
1408
1409 %% ===== GET DESCRIPTION =====
1410 % USAGE: [PlugDesc, errMsg] = GetDescription(PlugName/PlugDesc)
1411 function [PlugDesc, errMsg] = GetDescription(PlugName)
1412 % Initialize returned values
1413 errMsg = '';
1414 PlugDesc = [];
1415 % CALL: GetDescription(PlugDesc)
1416 if isstruct(PlugName)
1417 % Add the missing fields
1418 PlugDesc = struct_copy_fields(PlugName, db_template('PlugDesc'), 0);
1419 % CALL: GetDescription(PlugName)
1420 elseif ischar(PlugName)
1421 % Get supported plugins
1422 AllPlugs = GetSupported();
1423 % Find plugin in supported plugins
1424 iPlug = find(strcmpi({AllPlugs.Name}, PlugName));
1425 if isempty(iPlug)
1426 errMsg = ['Unknown plugin: ' PlugName];
1427 return;
1428 end
1429 % Return found plugin
1430 PlugDesc = AllPlugs(iPlug);
1431 else
1432 errMsg = 'Invalid call to GetDescription().';
1433 end
1434 end
1435
1436
1437 %% ===== GET TEST FILE PATH =====
1438 function TestFilePath = GetTestFilePath(PlugDesc)
1439 % If a test file is defined
1440 if ~isempty(PlugDesc.TestFile)
1441 % Try to find the test function in the path
1442 whichTest = which(PlugDesc.TestFile);
1443 % If it was found: use the parent folder
1444 if ~isempty(whichTest)
1445 % Get the test file path
1446 TestFilePath = bst_fileparts(whichTest);
1447 % FieldTrip: Ignore if found embedded in SPM12
1448 if strcmpi(PlugDesc.Name, 'fieldtrip')
1449 p = which('spm.m');
1450 if ~isempty(p) && strMatchEdge(TestFilePath, bst_fileparts(p), 'start')
1451 TestFilePath = [];
1452 end
1453 % SPM12: Ignore if found embedded in ROAST or in FieldTrip
1454 elseif strcmpi(PlugDesc.Name, 'spm12')
1455 p = which('roast.m');
1456 q = which('ft_defaults.m');
1457 if (~isempty(p) && strMatchEdge(TestFilePath, bst_fileparts(p), 'start')) || (~isempty(q) && strMatchEdge(TestFilePath, bst_fileparts(q), 'start'))
1458 TestFilePath = [];
1459 end
1460 % Iso2mesh: Ignore if found embedded in ROAST
1461 elseif strcmpi(PlugDesc.Name, 'iso2mesh')
1462 p = which('roast.m');
1463 if ~isempty(p) && strMatchEdge(TestFilePath, bst_fileparts(p), 'start')
1464 TestFilePath = [];
1465 end
1466 % jsonlab and jsnirfy: Ignore if found embedded in iso2mesh
1467 elseif strcmpi(PlugDesc.Name, 'jsonlab') || strcmpi(PlugDesc.Name, 'jsnirfy')
1468 p = which('iso2meshver.m');
1469 if ~isempty(p) && strMatchEdge(TestFilePath, bst_fileparts(p), 'start')
1470 TestFilePath = [];
1471 end
1472 % easyh5: Ignore if found embedded in iso2mesh or jsonlab
1473 elseif strcmpi(PlugDesc.Name, 'easyh5')
1474 p = which('iso2meshver.m');
1475 q = which('savejson.m');
1476 if (~isempty(p) && strMatchEdge(TestFilePath, bst_fileparts(p), 'start')) || (~isempty(q) && strMatchEdge(TestFilePath, bst_fileparts(q), 'start'))
1477 TestFilePath = [];
1478 end
1479 end
1480 else
1481 TestFilePath = [];
1482 end
1483 else
1484 TestFilePath = [];
1485 end
1486 end
1487
1488
1489 %% ===== GET README FILE ====
1490 % Get full path to the readme file
1491 function ReadmeFile = GetReadmeFile(PlugDesc)
1492 ReadmeFile = [];
1493 % If readme file is defined in the plugin structure
1494 if ~isempty(PlugDesc.ReadmeFile)
1495 % If full path already set: use it
1496 if file_exist(PlugDesc.ReadmeFile)
1497 ReadmeFile = PlugDesc.ReadmeFile;
1498 % Else: check in the plugin Path/SubFolder
1499 else
1500 tmpFile = bst_fullfile(PlugDesc.Path, PlugDesc.ReadmeFile);
1501 if file_exist(tmpFile)
1502 ReadmeFile = tmpFile;
1503 elseif ~isempty(PlugDesc.SubFolder)
1504 tmpFile = bst_fullfile(PlugDesc.Path, PlugDesc.SubFolder, PlugDesc.ReadmeFile);
1505 if file_exist(tmpFile)
1506 ReadmeFile = tmpFile;
1507 end
1508 end
1509 end
1510 end
1511 % Search for default readme
1512 if isempty(ReadmeFile)
1513 tmpFile = bst_fullfile(bst_get('BrainstormDocDir'), 'plugins', [PlugDesc.Name '_readme.txt']);
1514 if file_exist(tmpFile)
1515 ReadmeFile = tmpFile;
1516 end
1517 end
1518 end
1519
1520
1521 %% ===== GET LOGO FILE ====
1522 % Get full path to the logo file
1523 function LogoFile = GetLogoFile(PlugDesc)
1524 LogoFile = [];
1525 % If logo file is defined in the plugin structure
1526 if ~isempty(PlugDesc.LogoFile)
1527 % If full path already set: use it
1528 if file_exist(PlugDesc.LogoFile)
1529 LogoFile = PlugDesc.LogoFile;
1530 % Else: check in the plugin Path/SubFolder
1531 else
1532 tmpFile = bst_fullfile(PlugDesc.Path, PlugDesc.LogoFile);
1533 if file_exist(tmpFile)
1534 LogoFile = tmpFile;
1535 elseif ~isempty(PlugDesc.SubFolder)
1536 tmpFile = bst_fullfile(PlugDesc.Path, PlugDesc.SubFolder, PlugDesc.LogoFile);
1537 if file_exist(tmpFile)
1538 LogoFile = tmpFile;
1539 end
1540 end
1541 end
1542 end
1543 % Search for default logo
1544 if isempty(LogoFile)
1545 tmpFile = bst_fullfile(bst_get('BrainstormDocDir'), 'plugins', [PlugDesc.Name '_logo.gif']);
1546 if file_exist(tmpFile)
1547 LogoFile = tmpFile;
1548 end
1549 end
1550 if isempty(LogoFile)
1551 tmpFile = bst_fullfile(bst_get('BrainstormDocDir'), 'plugins', [PlugDesc.Name '_logo.png']);
1552 if file_exist(tmpFile)
1553 LogoFile = tmpFile;
1554 end
1555 end
1556 end
1557
1558
1559 %% ===== INSTALL =====
1560 % USAGE: [isOk, errMsg, PlugDesc] = bst_plugin('Install', PlugName, isInteractive=0, minVersion=[])
1561 function [isOk, errMsg, PlugDesc] = Install(PlugName, isInteractive, minVersion)
1562 % Returned variables
1563 isOk = 0;
1564 % Parse inputs
1565 if (nargin < 3) || isempty(minVersion)
1566 minVersion = [];
1567 elseif isnumeric(minVersion)
1568 minVersion = num2str(minVersion);
1569 end
1570 if (nargin < 2) || isempty(isInteractive)
1571 isInteractive = 0;
1572 end
1573 if ~ischar(PlugName)
1574 errMsg = 'Invalid call to Install()';
1575 PlugDesc = [];
1576 return;
1577 end
1578 % Get plugin structure from name
1579 [PlugDesc, errMsg] = GetDescription(PlugName);
1580 if ~isempty(errMsg)
1581 return;
1582 end
1583 % Check if plugin is supported on Apple silicon
1584 OsType = bst_get('OsType', 0);
1585 if strcmpi(OsType, 'mac64arm') && ismember(PlugName, PluginsNotSupportAppleSilicon())
1586 errMsg = ['Plugin ', PlugName ' is not supported on Apple silicon yet.'];
1587 PlugDesc = [];
1588 return;
1589 end
1590 % Check if there is a URL to download
1591 if isempty(PlugDesc.URLzip)
1592 errMsg = ['No download URL for ', OsType, ': ', PlugName ''];
1593 return;
1594 end
1595 % Compiled version
1596 isCompiled = bst_iscompiled();
1597 if isCompiled && (PlugDesc.CompiledStatus == 0)
1598 errMsg = ['Plugin ', PlugName ' is not available in the compiled version of Brainstorm.'];
1599 return;
1600 end
1601 % Minimum Matlab version
1602 if ~isempty(PlugDesc.MinMatlabVer) && (PlugDesc.MinMatlabVer > 0) && (bst_get('MatlabVersion') < PlugDesc.MinMatlabVer)
1603 strMinVer = sprintf('%d.%d', ceil(PlugDesc.MinMatlabVer / 100), mod(PlugDesc.MinMatlabVer, 100));
1604 errMsg = ['Plugin ', PlugName ' is not supported for versions of Matlab <= ' strMinVer];
1605 return;
1606 end
1607 % Get online update (use existing cache)
1608 [newVersion, newURLzip] = GetVersionOnline(PlugName, PlugDesc.URLzip, 1);
1609 if ~isempty(newVersion)
1610 PlugDesc.Version = newVersion;
1611 end
1612 if ~isempty(newURLzip)
1613 PlugDesc.URLzip = newURLzip;
1614 end
1615
1616 % === PROCESS DEPENDENCIES ===
1617 % Check required plugins
1618 if ~isempty(PlugDesc.RequiredPlugs)
1619 bst_progress('text', ['Processing dependencies for ' PlugName '...']);
1620 disp(['BST> Processing dependencies: ' PlugName ' requires: ' sprintf('%s ', PlugDesc.RequiredPlugs{:,1})]);
1621 % Get the list of plugins that need to be installed
1622 installPlugs = {};
1623 installVer = {};
1624 strInstall = '';
1625 for iPlug = 1:size(PlugDesc.RequiredPlugs,1)
1626 PlugCheck = GetInstalled(PlugDesc.RequiredPlugs{iPlug,1});
1627 % Plugin not install: Install it
1628 if isempty(PlugCheck)
1629 installPlugs{end+1} = PlugDesc.RequiredPlugs{iPlug,1};
1630 installVer{end+1} = [];
1631 strInstall = [strInstall, '<B>' installPlugs{end} '</B> '];
1632 % Plugin installed: check version
1633 elseif (size(PlugDesc.RequiredPlugs,2) == 2)
1634 minVerDep = PlugDesc.RequiredPlugs{iPlug,2};
1635 if ~isempty(minVerDep) && (CompareVersions(minVerDep, PlugCheck.Version) > 0)
1636 installPlugs{end+1} = PlugDesc.RequiredPlugs{iPlug,1};
1637 installVer{end+1} = PlugDesc.RequiredPlugs{iPlug,2};
1638 strInstall = [strInstall, '<B>' installPlugs{end} '</B>(' installVer{end} ') '];
1639 end
1640 end
1641 end
1642 % If there are plugins to install
1643 if ~isempty(installPlugs)
1644 if isInteractive
1645 java_dialog('msgbox', ['<HTML>Plugin <B>' PlugName '</B> requires: ' strInstall ...
1646 '<BR><BR>Brainstorm will now install these plugins.' 10 10], 'Plugin manager');
1647 end
1648 for iPlug = 1:length(installPlugs)
1649 [isInstalled, errMsg] = Install(installPlugs{iPlug}, isInteractive, installVer{iPlug});
1650 if ~isInstalled
1651 errMsg = ['Error processing dependency: ' PlugDesc.RequiredPlugs{iPlug,1} 10 errMsg];
1652 return;
1653 end
1654 end
1655 end
1656 end
1657
1658 % === UPDATE: CHECK PREVIOUS INSTALL ===
1659 % Check if installed
1660 OldPlugDesc = GetInstalled(PlugName);
1661 % If already installed
1662 if ~isempty(OldPlugDesc)
1663 % If the plugin is not managed by Brainstorm: do not check versions
1664 if ~OldPlugDesc.isManaged
1665 isUpdate = 0;
1666 % If the requested version is higher
1667 elseif ~isempty(minVersion) && (CompareVersions(minVersion, OldPlugDesc.Version) > 0)
1668 isUpdate = 1;
1669 strUpdate = ['the installed version is outdated.<BR>Minimum version required: <I>' minVersion '</I>'];
1670 % If an update is available and auto-updates are requested
1671 elseif (PlugDesc.AutoUpdate == 1) && bst_get('AutoUpdates') && ... % If updates are enabled
1672 ((isGithubMaster(PlugDesc.URLzip) && ~strcmpi(PlugDesc.Version, OldPlugDesc.Version)) || ... % GitHub-master: update if different commit SHA strings
1673 (~isGithubMaster(PlugDesc.URLzip) && (CompareVersions(PlugDesc.Version, OldPlugDesc.Version) > 0))) % Regular stable version: update if online version is newer
1674 isUpdate = 1;
1675 strUpdate = 'an update is available online.';
1676 else
1677 isUpdate = 0;
1678 end
1679 % Update plugin
1680 if isUpdate
1681 % Compare versions
1682 strCompare = ['<FONT color="#707070">' ...
1683 'Old version : <I>' OldPlugDesc.Version '</I><BR>' ...
1684 'New version : <I>' PlugDesc.Version '</I></FONT><BR><BR>'];
1685 % Ask user for updating
1686 if isInteractive
1687 isConfirm = java_dialog('confirm', ...
1688 ['<HTML>Plugin <B>' PlugName '</B>: ' strUpdate '<BR>' ...
1689 'Download and install the latest version?<BR><BR>' strCompare], 'Plugin manager');
1690 % If update not confirmed: simply load the existing plugin
1691 if ~isConfirm
1692 [isOk, errMsg, PlugDesc] = Load(PlugDesc);
1693 return;
1694 end
1695 end
1696 disp(['BST> Plugin ' PlugName ' is outdated and will be updated.']);
1697 % Uninstall existing plugin
1698 [isOk, errMsg] = Uninstall(PlugName, 0, 0);
1699 if ~isOk
1700 errMsg = ['An error occurred while updating plugin ' PlugName ':' 10 10 errMsg 10];
1701 return;
1702 end
1703
1704 % No update: Load existing plugin and return
1705 else
1706 % Load plugin
1707 if ~OldPlugDesc.isLoaded
1708 [isLoaded, errMsg, PlugDesc] = Load(OldPlugDesc);
1709 if ~isLoaded
1710 errMsg = ['Could not load plugin ' PlugName ':' 10 errMsg];
1711 return;
1712 end
1713 else
1714 disp(['BST> Plugin ' PlugName ' already loaded: ' OldPlugDesc.Path]);
1715 end
1716 % Return old plugin
1717 PlugDesc = OldPlugDesc;
1718 isOk = 1;
1719 return;
1720 end
1721 else
1722 % Get user confirmation
1723 if isInteractive
1724 if ~isempty(PlugDesc.Version) && ~isequal(PlugDesc.Version, 'github-master') && ~isequal(PlugDesc.Version, 'latest')
1725 strVer = ['<FONT color="#707070">Latest version: ' PlugDesc.Version '</FONT><BR><BR>'];
1726 else
1727 strVer = '';
1728 end
1729 isConfirm = java_dialog('confirm', ...
1730 ['<HTML>Plugin <B>' PlugName '</B> is not installed on your computer.<BR>' ...
1731 '<B>Download</B> the latest version of ' PlugName ' now?<BR><BR>' ...
1732 strVer, ...
1733 '<FONT color="#707070">If this program is available on your computer,<BR>' ...
1734 'cancel this installation and use the menu: Plugins > <BR>' ...
1735 PlugName ' > Custom install > Set installation folder.</FONT><BR><BR>'], 'Plugin manager');
1736 if ~isConfirm
1737 errMsg = 'Installation aborted by user.';
1738 return;
1739 end
1740 end
1741 end
1742
1743 % === INSTALL PLUGIN ===
1744 bst_progress('text', ['Installing plugin ' PlugName '...']);
1745 % Managed plugin folder
1746 PlugPath = bst_fullfile(bst_get('UserPluginsDir'), PlugName);
1747 % Delete existing folder
1748 if isdir(PlugPath)
1749 file_delete(PlugPath, 1, 3);
1750 end
1751 % Create folder
1752 if ~isdir(PlugPath)
1753 res = mkdir(PlugPath);
1754 if ~res
1755 errMsg = ['Error: Cannot create folder' 10 PlugPath];
1756 return
1757 end
1758 end
1759 % Setting progressbar image
1760 LogoFile = GetLogoFile(PlugDesc);
1761 if ~isempty(LogoFile)
1762 bst_progress('setimage', LogoFile);
1763 end
1764 % Get package file format
1765 if strcmpi(PlugDesc.URLzip(end-3:end), '.zip')
1766 pkgFormat = 'zip';
1767 elseif strcmpi(PlugDesc.URLzip(end-6:end), '.tar.gz') || strcmpi(PlugDesc.URLzip(end-3:end), '.tgz')
1768 pkgFormat = 'tgz';
1769 else
1770 disp('BST> Could not guess file format, trying ZIP...');
1771 pkgFormat = 'zip';
1772 end
1773 % Download file
1774 pkgFile = bst_fullfile(PlugPath, ['plugin.' pkgFormat]);
1775 disp(['BST> Downloading URL : ' PlugDesc.URLzip]);
1776 disp(['BST> Saving to file : ' pkgFile]);
1777 errMsg = gui_brainstorm('DownloadFile', PlugDesc.URLzip, pkgFile, ['Download plugin: ' PlugName], LogoFile);
1778 % If file was not downloaded correctly
1779 if ~isempty(errMsg)
1780 errMsg = ['Impossible to download ' PlugName ' automatically:' 10 errMsg];
1781 if ~isCompiled
1782 errMsg = [errMsg 10 10 ...
1783 'Alternative download solution:' 10 ...
1784 '1) Copy the URL below from the Matlab command window: ' 10 ...
1785 ' ' PlugDesc.URLzip 10 ...
1786 '2) Paste it in a web browser' 10 ...
1787 '3) Save the file and unzip it' 10 ...
1788 '4) Add to the Matlab path the folder containing ' PlugDesc.TestFile '.'];
1789 end
1790 bst_progress('removeimage');
1791 return;
1792 end
1793 % Update progress bar
1794 bst_progress('text', ['Installing plugin: ' PlugName '...']);
1795 if ~isempty(LogoFile)
1796 bst_progress('setimage', LogoFile);
1797 end
1798 % Unzip file
1799 switch (pkgFormat)
1800 case 'zip'
1801 bst_unzip(pkgFile, PlugPath);
1802 case 'tgz'
1803 if ispc
1804 untar(pkgFile, PlugPath);
1805 else
1806 curdir = pwd;
1807 cd(PlugPath);
1808 system(['tar -xf ' pkgFile]);
1809 cd(curdir);
1810 end
1811 end
1812 file_delete(pkgFile, 1, 3);
1813
1814 % === SAVE PLUGIN.MAT ===
1815 PlugDesc.Path = PlugPath;
1816 PlugMatFile = bst_fullfile(PlugDesc.Path, 'plugin.mat');
1817 excludedFields = {'LoadedFcn', 'UnloadedFcn', 'DownloadedFcn', 'InstalledFcn', 'UninstalledFcn', 'Path', 'isLoaded', 'isManaged'};
1818 PlugDescSave = rmfield(PlugDesc, excludedFields);
1819 bst_save(PlugMatFile, PlugDescSave, 'v6');
1820
1821 % === CALLBACK: POST-DOWNLOADED ===
1822 [isOk, errMsg] = ExecuteCallback(PlugDesc, 'DownloadedFcn');
1823 if ~isOk
1824 return;
1825 end
1826
1827 % === LOAD PLUGIN ===
1828 % Load plugin
1829 [isOk, errMsg, PlugDesc] = Load(PlugDesc);
1830 if ~isOk
1831 bst_progress('removeimage');
1832 return;
1833 end
1834 % Update plugin description after first load, and delete unwanted files
1835 [isOk, errMsg, PlugDesc] = UpdateDescription(PlugDesc, 1);
1836 if ~isOk
1837 return;
1838 end
1839
1840 % === SHOW PLUGIN INFO ===
1841 % Log install
1842 bst_webread(['https://neuroimage.usc.edu/bst/pluglog.php?c=K8Yda7B&plugname=' PlugDesc.Name '&action=install']);
1843 % Show plugin information (interactive mode only)
1844 if isInteractive
1845 % Hide progress bar
1846 isProgress = bst_progress('isVisible');
1847 if isProgress
1848 bst_progress('hide');
1849 end
1850 % Message box: aknowledgements
1851 java_dialog('msgbox', ['<HTML>Plugin <B>' PlugName '</B> was sucessfully installed.<BR><BR>' ...
1852 'This software is not distributed by the Brainstorm developers.<BR>' ...
1853 'Please take a few minutes to read the license information,<BR>' ...
1854 'check the authors'' website and register online if recommended.<BR><BR>' ...
1855 '<B>Cite the authors</B> in your publications if you are using their software.<BR><BR>'], 'Plugin manager');
1856 % Show the readme file
1857 if ~isempty(PlugDesc.ReadmeFile)
1858 view_text(PlugDesc.ReadmeFile, ['Installed plugin: ' PlugName], 1, 1);
1859 end
1860 % Open the website
1861 if ~isempty(PlugDesc.URLinfo)
1862 web(PlugDesc.URLinfo, '-browser')
1863 end
1864 % Restore progress bar
1865 if isProgress
1866 bst_progress('show');
1867 end
1868 end
1869 % Remove logo
1870 bst_progress('removeimage');
1871 % Return success
1872 isOk = 1;
1873 end
1874
1875
1876 %% ===== UPDATE DESCRIPTION =====
1877 % USAGE: [isOk, errMsg, PlugDesc] = bst_plugin('UpdateDescription', PlugDesc, doDelete=0)
1878 function [isOk, errMsg, PlugDesc] = UpdateDescription(PlugDesc, doDelete)
1879 isOk = 1;
1880 errMsg = '';
1881 PlugPath = PlugDesc.Path;
1882 PlugName = PlugDesc.Name;
1883
1884 if nargin < 2
1885 doDelete = 0;
1886 end
1887
1888 % Plug in needs to be installed
1889 if isempty(bst_plugin('GetInstalled', PlugDesc.Name))
1890 isOk = 0;
1891 errMsg = ['Cannot update description, plugin ''' PlugDesc.Name ''' needs to be installed'];
1892 return
1893 end
1894
1895 % === DELETE UNWANTED FILES ===
1896 if doDelete && ~isempty(PlugDesc.DeleteFiles) && iscell(PlugDesc.DeleteFiles)
1897 warning('off', 'MATLAB:RMDIR:RemovedFromPath');
1898 for iDel = 1:length(PlugDesc.DeleteFiles)
1899 if ~isempty(PlugDesc.SubFolder)
1900 fileDel = bst_fullfile(PlugDesc.Path, PlugDesc.SubFolder, PlugDesc.DeleteFiles{iDel});
1901 else
1902 fileDel = bst_fullfile(PlugDesc.Path, PlugDesc.DeleteFiles{iDel});
1903 end
1904 if file_exist(fileDel)
1905 try
1906 file_delete(fileDel, 1, 3);
1907 catch
1908 disp(['BST> Plugin ' PlugName ': Could not delete file: ' PlugDesc.DeleteFiles{iDel}]);
1909 end
1910 else
1911 disp(['BST> Plugin ' PlugName ': Missing file: ' PlugDesc.DeleteFiles{iDel}]);
1912 end
1913 end
1914 warning('on', 'MATLAB:RMDIR:RemovedFromPath');
1915 end
1916
1917 % === SEARCH PROCESSES ===
1918 % Look for process_* functions in the process folder
1919 PlugProc = file_find(PlugPath, 'process_*.m', Inf, 0);
1920 if ~isempty(PlugProc)
1921 % Remove absolute path: use only path relative to the plugin Path
1922 PlugDesc.Processes = cellfun(@(c)file_win2unix(strrep(c, [PlugPath, filesep], '')), PlugProc, 'UniformOutput', 0);
1923 end
1924
1925 % === SAVE PLUGIN.MAT ===
1926 % Save installation date
1927 c = clock();
1928 PlugDesc.InstallDate = datestr(datenum(c(1), c(2), c(3), c(4), c(5), c(6)), 'dd-mmm-yyyy HH:MM:SS');
1929 % Get readme and logo
1930 PlugDesc.ReadmeFile = GetReadmeFile(PlugDesc);
1931 PlugDesc.LogoFile = GetLogoFile(PlugDesc);
1932 % Update plugin.mat
1933 excludedFields = {'LoadedFcn', 'UnloadedFcn', 'DownloadedFcn', 'InstalledFcn', 'UninstalledFcn', 'Path', 'isLoaded', 'isManaged'};
1934 PlugDescSave = rmfield(PlugDesc, excludedFields);
1935 PlugMatFile = bst_fullfile(PlugDesc.Path, 'plugin.mat');
1936 bst_save(PlugMatFile, PlugDescSave, 'v6');
1937
1938 % === CALLBACK: POST-INSTALL ===
1939 [isOk, errMsg] = ExecuteCallback(PlugDesc, 'InstalledFcn');
1940 if ~isOk
1941 return;
1942 end
1943
1944 % === GET INSTALLED VERSION ===
1945 % Get installed version
1946 if ~isempty(PlugDesc.GetVersionFcn)
1947 testVer = [];
1948 try
1949 if ischar(PlugDesc.GetVersionFcn)
1950 testVer = eval(PlugDesc.GetVersionFcn);
1951 elseif isa(PlugDesc.GetVersionFcn, 'function_handle')
1952 testVer = feval(PlugDesc.GetVersionFcn);
1953 end
1954 catch
1955 disp(['BST> Could not get installed version with callback: ' PlugDesc.GetVersionFcn]);
1956 end
1957 if ~isempty(testVer)
1958 PlugDesc.Version = testVer;
1959 % Update plugin.mat
1960 PlugDescSave.Version = testVer;
1961 bst_save(PlugMatFile, PlugDescSave, 'v6');
1962 end
1963 end
1964 end
1965
1966 %% ===== INSTALL INTERACTIVE =====
1967 % USAGE: [isOk, errMsg, PlugDesc] = bst_plugin('InstallInteractive', PlugName)
1968 function [isOk, errMsg, PlugDesc] = InstallInteractive(PlugName)
1969 % Open progress bar
1970 isProgress = bst_progress('isVisible');
1971 if ~isProgress
1972 bst_progress('start', 'Plugin manager', 'Initialization...');
1973 end
1974 % Call silent function
1975 [isOk, errMsg, PlugDesc] = Install(PlugName, 1);
1976 % Handle errors
1977 if ~isOk
1978 bst_error(['Installation error:' 10 10 errMsg 10], 'Plugin manager', 0);
1979 elseif ~isempty(errMsg)
1980 java_dialog('msgbox', ['Installation message:' 10 10 errMsg 10], 'Plugin manager');
1981 end
1982 % Close progress bar
1983 if ~isProgress
1984 bst_progress('stop');
1985 end
1986 end
1987
1988
1989 %% ===== INSTALL MULTIPLE CHOICE =====
1990 % If multiple plugins provide the same functions (eg. FieldTrip and SPM): make sure at least one is installed
1991 % USAGE: [isOk, errMsg, PlugDesc] = bst_plugin('InstallMultipleChoice', PlugNames, isInteractive)
1992 function [isOk, errMsg, PlugDesc] = InstallMultipleChoice(PlugNames, isInteractive)
1993 % Check if one of the plugins is loaded
1994 for iPlug = 1:length(PlugNames)
1995 PlugInst = GetInstalled(PlugNames{iPlug});
1996 if ~isempty(PlugInst)
1997 [isOk, errMsg, PlugDesc] = Load(PlugNames{iPlug});
1998 if isOk
1999 return;
2000 end
2001 end
2002 end
2003 % If no plugin is loaded: Install the first in the list
2004 [isOk, errMsg, PlugDesc] = Install(PlugNames{1}, isInteractive);
2005 end
2006
2007
2008 %% ===== UNINSTALL =====
2009 % USAGE: [isOk, errMsg] = bst_plugin('Uninstall', PlugName, isInteractive=0, isDependencies=1)
2010 function [isOk, errMsg] = Uninstall(PlugName, isInteractive, isDependencies)
2011 % Returned variables
2012 isOk = 0;
2013 errMsg = '';
2014 % Parse inputs
2015 if (nargin < 3) || isempty(isDependencies)
2016 isDependencies = 1;
2017 end
2018 if (nargin < 2) || isempty(isInteractive)
2019 isInteractive = 0;
2020 end
2021 if ~ischar(PlugName)
2022 errMsg = 'Invalid call to Uninstall()';
2023 return;
2024 end
2025
2026 % === CHECK INSTALLATION ===
2027 % Get installation
2028 PlugDesc = GetInstalled(PlugName);
2029 % External plugin
2030 if ~isempty(PlugDesc) && ~isequal(PlugDesc.isManaged, 1)
2031 errMsg = ['<HTML>Plugin <B>' PlugName '</B> is not managed by Brainstorm.' 10 'Delete folder manually:' 10 PlugDesc.Path];
2032 return;
2033 % Plugin not installed: check if folder exists
2034 elseif isempty(PlugDesc) || isempty(PlugDesc.Path)
2035 % Get plugin structure from name
2036 [PlugDesc, errMsg] = GetDescription(PlugName);
2037 if ~isempty(errMsg)
2038 return;
2039 end
2040 % Managed plugin folder
2041 PlugPath = bst_fullfile(bst_get('UserPluginsDir'), PlugName);
2042 else
2043 PlugPath = PlugDesc.Path;
2044 end
2045 % Plugin not installed
2046 if ~file_exist(PlugPath)
2047 errMsg = ['Plugin ' PlugName ' is not installed.'];
2048 return;
2049 end
2050
2051 % === USER CONFIRMATION ===
2052 if isInteractive
2053 isConfirm = java_dialog('confirm', ['<HTML>Delete permanently plugin <B>' PlugName '</B>?' 10 10 PlugPath 10 10], 'Plugin manager');
2054 if ~isConfirm
2055 errMsg = 'Uninstall aborted by user.';
2056 return;
2057 end
2058 end
2059
2060 % === PROCESS DEPENDENCIES ===
2061 % Uninstall dependent plugins
2062 if isDependencies
2063 AllPlugs = GetSupported();
2064 for iPlug = 1:length(AllPlugs)
2065 if ~isempty(AllPlugs(iPlug).RequiredPlugs) && ismember(PlugDesc.Name, AllPlugs(iPlug).RequiredPlugs(:,1))
2066 disp(['BST> Uninstalling dependent plugin: ' AllPlugs(iPlug).Name]);
2067 Uninstall(AllPlugs(iPlug).Name, isInteractive);
2068 end
2069 end
2070 end
2071
2072 % === UNLOAD ===
2073 if isequal(PlugDesc.isLoaded, 1)
2074 [isUnloaded, errMsgUnload] = Unload(PlugDesc);
2075 if ~isempty(errMsgUnload)
2076 disp(['BST> Error unloading plugin ' PlugName ': ' errMsgUnload]);
2077 end
2078 end
2079
2080 % === UNINSTALL ===
2081 disp(['BST> Deleting plugin ' PlugName ': ' PlugPath]);
2082 % Delete plugin folder
2083 isDeleted = file_delete(PlugPath, 1, 3);
2084 if (isDeleted ~= 1)
2085 errMsg = ['Could not delete plugin folder: ' 10 PlugPath 10 10 ...
2086 'There is probably a file in that folder that is currently ' 10 ...
2087 'loaded in Matlab, but that cannot be unloaded dynamically.' 10 10 ...
2088 'Brainstorm will now close Matlab.' 10 ...
2089 'Restart Matlab and install again the plugin.' 10 10];
2090 if isInteractive
2091 java_dialog('error', errMsg, 'Restart Matlab');
2092 else
2093 disp([10 10 'BST> ' errMsg]);
2094 end
2095 quit('force');
2096 end
2097
2098 % === CALLBACK: POST-UNINSTALL ===
2099 [isOk, errMsg] = ExecuteCallback(PlugDesc, 'UninstalledFcn');
2100 if ~isOk
2101 return;
2102 end
2103
2104 % Return success
2105 isOk = 1;
2106 end
2107
2108
2109 %% ===== UNINSTALL INTERACTIVE =====
2110 % USAGE: [isOk, errMsg] = bst_plugin('UninstallInteractive', PlugName)
2111 function [isOk, errMsg] = UninstallInteractive(PlugName)
2112 % Open progress bar
2113 isProgress = bst_progress('isVisible');
2114 if ~isProgress
2115 bst_progress('start', 'Plugin manager', 'Initialization...');
2116 end
2117 % Call silent function
2118 [isOk, errMsg] = Uninstall(PlugName, 1);
2119 % Handle errors
2120 if ~isOk
2121 bst_error(['An error occurred while uninstalling plugin ' PlugName ':' 10 10 errMsg 10], 'Plugin manager', 0);
2122 elseif ~isempty(errMsg)
2123 java_dialog('msgbox', ['Uninstall message:' 10 10 errMsg 10], 'Plugin manager');
2124 end
2125 % Close progress bar
2126 if ~isProgress
2127 bst_progress('stop');
2128 end
2129 end
2130
2131
2132 %% ===== UPDATE INTERACTIVE =====
2133 % USAGE: [isOk, errMsg] = bst_plugin('UpdateInteractive', PlugName)
2134 function [isOk, errMsg] = UpdateInteractive(PlugName)
2135 % Open progress bar
2136 isProgress = bst_progress('isVisible');
2137 if ~isProgress
2138 bst_progress('start', 'Plugin manager', 'Initialization...');
2139 end
2140 % Get new plugin
2141 [PlugRef, errMsg] = GetDescription(PlugName);
2142 isOk = isempty(errMsg);
2143 % Get installed plugin
2144 if isOk
2145 PlugInst = GetInstalled(PlugName);
2146 if isempty(PlugInst) || ~PlugInst.isManaged
2147 isOk = 0;
2148 errMsg = ['Plugin ' PlugName ' is not installed or not managed by Brainstorm.'];
2149 end
2150 end
2151 % Get online update (use cache when available)
2152 [newVersion, newURLzip] = GetVersionOnline(PlugName, PlugRef.URLzip, 1);
2153 if ~isempty(newVersion)
2154 PlugRef.Version = newVersion;
2155 end
2156 if ~isempty(newURLzip)
2157 PlugRef.URLzip = newURLzip;
2158 end
2159 % User confirmation
2160 if isOk
2161 isOk = java_dialog('confirm', ['<HTML>Update plugin <B>' PlugName '</B> ?<BR><BR><FONT color="#707070">' ...
2162 'Old version : <I>' PlugInst.Version '</I><BR>' ...
2163 'New version : <I>' PlugRef.Version '</I><BR><BR></FONT>'], 'Plugin manager');
2164 if ~isOk
2165 errMsg = 'Update aborted by user.';
2166 end
2167 end
2168 % Uninstall old
2169 if isOk
2170 [isOk, errMsg] = Uninstall(PlugName, 0, 0);
2171 end
2172 % Install new
2173 if isOk
2174 [isOk, errMsg, PlugDesc] = Install(PlugName, 0);
2175 else
2176 PlugDesc = [];
2177 end
2178 % Handle errors
2179 if ~isOk
2180 bst_error(['An error occurred while updating plugin ' PlugName ':' 10 10 errMsg 10], 'Plugin manager', 0);
2181 elseif ~isempty(errMsg)
2182 java_dialog('msgbox', ['Update message:' 10 10 errMsg 10], 'Plugin manager');
2183 end
2184 % Close progress bar
2185 if ~isProgress
2186 bst_progress('stop');
2187 end
2188 % Plugin was updated successfully
2189 if ~isempty(PlugDesc)
2190 % Show the readme file
2191 if ~isempty(PlugDesc.ReadmeFile)
2192 view_text(PlugDesc.ReadmeFile, ['Installed plugin: ' PlugName], 1, 1);
2193 end
2194 % Open the website
2195 if ~isempty(PlugDesc.URLinfo)
2196 web(PlugDesc.URLinfo, '-browser')
2197 end
2198 end
2199 end
2200
2201
2202 %% ===== LOAD =====
2203 % USAGE: [isOk, errMsg, PlugDesc] = Load(PlugDesc, isVerbose=1)
2204 function [isOk, errMsg, PlugDesc] = Load(PlugDesc, isVerbose)
2205 % Parse inputs
2206 if (nargin < 2) || isempty(isVerbose)
2207 isVerbose = 1;
2208 end
2209 % Initialize returned variables
2210 isOk = 0;
2211 % Get plugin structure from name
2212 [PlugDesc, errMsg] = GetDescription(PlugDesc);
2213 if ~isempty(errMsg)
2214 return;
2215 end
2216 % Check if plugin is supported on Apple silicon
2217 OsType = bst_get('OsType', 0);
2218 if strcmpi(OsType, 'mac64arm') && ismember(PlugDesc.Name, PluginsNotSupportAppleSilicon())
2219 errMsg = ['Plugin ', PlugDesc.Name ' is not supported on Apple silicon yet.'];
2220 return;
2221 end
2222 % Minimum Matlab version
2223 if ~isempty(PlugDesc.MinMatlabVer) && (PlugDesc.MinMatlabVer > 0) && (bst_get('MatlabVersion') < PlugDesc.MinMatlabVer)
2224 strMinVer = sprintf('%d.%d', ceil(PlugDesc.MinMatlabVer / 100), mod(PlugDesc.MinMatlabVer, 100));
2225 errMsg = ['Plugin ', PlugDesc.Name ' is not supported for versions of Matlab <= ' strMinVer];
2226 return;
2227 end
2228
2229 % === PROCESS DEPENDENCIES ===
2230 % Unload incompatible plugins
2231 if ~isempty(PlugDesc.UnloadPlugs)
2232 for iPlug = 1:length(PlugDesc.UnloadPlugs)
2233 % disp(['BST> Unloading incompatible plugin: ' PlugDesc.UnloadPlugs{iPlug}]);
2234 Unload(PlugDesc.UnloadPlugs{iPlug}, isVerbose);
2235 end
2236 end
2237
2238 % === ALREADY LOADED ===
2239 % If plugin is already full loaded
2240 if isequal(PlugDesc.isLoaded, 1) && ~isempty(PlugDesc.Path)
2241 if isVerbose
2242 errMsg = ['Plugin ' PlugDesc.Name ' already loaded: ' PlugDesc.Path];
2243 end
2244 return;
2245 end
2246 % Managed plugin path
2247 PlugPath = bst_fullfile(bst_get('UserPluginsDir'), PlugDesc.Name);
2248 if file_exist(PlugPath)
2249 PlugDesc.isManaged = 1;
2250 % Custom installation
2251 else
2252 PluginCustomPath = bst_get('PluginCustomPath');
2253 if isfield(PluginCustomPath, PlugDesc.Name) && ~isempty(bst_fullfile(PluginCustomPath.(PlugDesc.Name))) && file_exist(bst_fullfile(PluginCustomPath.(PlugDesc.Name)))
2254 PlugPath = PluginCustomPath.(PlugDesc.Name);
2255 end
2256 PlugDesc.isManaged = 0;
2257 end
2258 % Managed install: Detect if there is a single subfolder containing all the files
2259 if PlugDesc.isManaged && ~isempty(PlugDesc.TestFile) && ~file_exist(bst_fullfile(PlugPath, PlugDesc.TestFile))
2260 dirList = dir(PlugPath);
2261 for iDir = 1:length(dirList)
2262 % Not folder or . : skip
2263 if (dirList(iDir).name(1) == '.') || ~dirList(iDir).isdir
2264 continue;
2265 end
2266 % Check if test file is in the folder
2267 if file_exist(bst_fullfile(PlugPath, dirList(iDir).name, PlugDesc.TestFile))
2268 PlugDesc.SubFolder = dirList(iDir).name;
2269 break;
2270 % Otherwise, check in any of the subfolders
2271 elseif ~isempty(PlugDesc.LoadFolders)
2272 for iSubDir = 1:length(PlugDesc.LoadFolders)
2273 if file_exist(bst_fullfile(PlugPath, dirList(iDir).name, PlugDesc.LoadFolders{iSubDir}, PlugDesc.TestFile))
2274 PlugDesc.SubFolder = dirList(iDir).name;
2275 break;
2276 end
2277 end
2278 end
2279 end
2280 end
2281 % Check if test function already available in the path
2282 TestFilePath = GetTestFilePath(PlugDesc);
2283 if ~isempty(TestFilePath)
2284 PlugDesc.isLoaded = 1;
2285 % Handle case symbolic link
2286 try
2287 PlugPath = builtin('_canonicalizepath', PlugPath);
2288 catch
2289 % Nothing here
2290 end
2291 PlugDesc.isManaged = strMatchEdge(which(PlugDesc.TestFile), PlugPath, 'start');
2292 if PlugDesc.isManaged
2293 PlugDesc.Path = PlugPath;
2294 else
2295 PlugDesc.Path = TestFilePath;
2296 end
2297 if isVerbose
2298 disp(['BST> Plugin ' PlugDesc.Name ' already loaded: ' PlugDesc.Path]);
2299 end
2300 isOk = 1;
2301 return;
2302 end
2303
2304 % === CHECK LOADABILITY ===
2305 PlugDesc.Path = PlugPath;
2306 if ~file_exist(PlugDesc.Path)
2307 errMsg = ['Plugin ' PlugDesc.Name ' not installed.' 10 'Missing folder: ' PlugDesc.Path];
2308 return;
2309 end
2310 % Set logo
2311 LogoFile = GetLogoFile(PlugDesc);
2312 if ~isempty(LogoFile)
2313 bst_progress('setimage', LogoFile);
2314 end
2315
2316 % Load required plugins
2317 if ~isempty(PlugDesc.RequiredPlugs)
2318 for iPlug = 1:size(PlugDesc.RequiredPlugs,1)
2319 % disp(['BST> Loading required plugin: ' PlugDesc.RequiredPlugs{iPlug,1}]);
2320 [isOk, errMsg] = Load(PlugDesc.RequiredPlugs{iPlug,1}, isVerbose);
2321 if ~isOk
2322 errMsg = ['Error processing dependencies: ', PlugDesc.Name, 10, errMsg];
2323 bst_progress('removeimage');
2324 return;
2325 end
2326 end
2327 end
2328
2329 % === LOAD PLUGIN ===
2330 % Add plugin folder to path
2331 if ~isempty(PlugDesc.SubFolder)
2332 PlugHomeDir = bst_fullfile(PlugPath, PlugDesc.SubFolder);
2333 else
2334 PlugHomeDir = PlugPath;
2335 end
2336 % Do not modify path in compiled mode
2337 isCompiled = bst_iscompiled();
2338 if ~isCompiled
2339 % Handle case symbolic link
2340 try
2341 PlugHomeDir = builtin('_canonicalizepath', PlugHomeDir);
2342 catch
2343 % Nothing here
2344 end
2345 addpath(PlugHomeDir);
2346 if isVerbose
2347 disp(['BST> Adding plugin ' PlugDesc.Name ' to path: ' PlugHomeDir]);
2348 end
2349 % Add specific subfolders to path
2350 if ~isempty(PlugDesc.LoadFolders)
2351 % Load all all subfolders
2352 if isequal(PlugDesc.LoadFolders, '*') || isequal(PlugDesc.LoadFolders, {'*'})
2353 if isVerbose
2354 disp(['BST> Adding plugin ' PlugDesc.Name ' to path: ', PlugHomeDir, filesep, '*']);
2355 end
2356 addpath(genpath(PlugHomeDir));
2357 % Load specific subfolders
2358 else
2359 for i = 1:length(PlugDesc.LoadFolders)
2360 subDir = PlugDesc.LoadFolders{i};
2361 if isequal(filesep, '\')
2362 subDir = strrep(subDir, '/', '\');
2363 end
2364 if ~isempty(dir([PlugHomeDir, filesep, subDir]))
2365 if isVerbose
2366 disp(['BST> Adding plugin ' PlugDesc.Name ' to path: ', PlugHomeDir, filesep, subDir]);
2367 end
2368 if regexp(subDir, '\*[/\\]*$')
2369 subDir = regexprep(subDir, '\*[/\\]*$', '');
2370 addpath(genpath([PlugHomeDir, filesep, subDir]));
2371 else
2372 addpath([PlugHomeDir, filesep, subDir]);
2373 end
2374 end
2375 end
2376 end
2377 end
2378 end
2379
2380 % === TEST FUNCTION ===
2381 % Check if test function is available on path
2382 if ~isCompiled && ~isempty(PlugDesc.TestFile) && (exist(PlugDesc.TestFile, 'file') == 0)
2383 errMsg = ['Plugin ' PlugDesc.Name ' successfully loaded from:' 10 PlugHomeDir 10 10 ...
2384 'However, the function ' PlugDesc.TestFile ' is not accessible in the Matlab path.' 10 10 ...
2385 'Try the following:' 10 ...
2386 '1. Update the plugin ' PlugDesc.Name 10 ...
2387 '2. If the issue persists, restart Matlab and Brainstorm.'];
2388 bst_progress('removeimage');
2389 return;
2390 end
2391
2392 % === CALLBACK: POST-LOAD ===
2393 [isOk, errMsg] = ExecuteCallback(PlugDesc, 'LoadedFcn');
2394
2395 % Remove logo
2396 bst_progress('removeimage');
2397 % Return success
2398 PlugDesc.isLoaded = isOk;
2399 end
2400
2401
2402 %% ===== LOAD INTERACTIVE =====
2403 % USAGE: [isOk, errMsg, PlugDesc] = LoadInteractive(PlugName/PlugDesc)
2404 function [isOk, errMsg, PlugDesc] = LoadInteractive(PlugDesc)
2405 % Open progress bar
2406 isProgress = bst_progress('isVisible');
2407 if ~isProgress
2408 bst_progress('start', 'Plugin manager', 'Loading plugin...');
2409 end
2410 % Call silent function
2411 [isOk, errMsg, PlugDesc] = Load(PlugDesc);
2412 % Handle errors
2413 if ~isOk
2414 bst_error(['Load error:' 10 10 errMsg 10], 'Plugin manager', 0);
2415 elseif ~isempty(errMsg)
2416 java_dialog('msgbox', ['Load message:' 10 10 errMsg 10], 'Plugin manager');
2417 end
2418 % Close progress bar
2419 if ~isProgress
2420 bst_progress('stop');
2421 end
2422 end
2423
2424
2425 %% ===== UNLOAD =====
2426 % USAGE: [isOk, errMsg, PlugDesc] = Unload(PlugName/PlugDesc, isVerbose)
2427 function [isOk, errMsg, PlugDesc] = Unload(PlugDesc, isVerbose)
2428 % Parse inputs
2429 if (nargin < 2) || isempty(isVerbose)
2430 isVerbose = 1;
2431 end
2432 % Initialize returned variables
2433 isOk = 0;
2434 errMsg = '';
2435 % Get installation
2436 InstPlugDesc = GetInstalled(PlugDesc);
2437 % Plugin not installed: check if folder exists
2438 if isempty(InstPlugDesc) || isempty(InstPlugDesc.Path)
2439 % Get plugin structure from name
2440 [PlugDesc, errMsg] = GetDescription(PlugDesc);
2441 if ~isempty(errMsg)
2442 return;
2443 end
2444 % Managed plugin folder
2445 PlugPath = bst_fullfile(bst_get('UserPluginsDir'), PlugDesc.Name);
2446 else
2447 PlugDesc = InstPlugDesc;
2448 PlugPath = PlugDesc.Path;
2449 end
2450 % Plugin not installed
2451 if ~file_exist(PlugPath)
2452 errMsg = ['Plugin ' PlugDesc.Name ' is not installed.' 10 'Missing folder: ' PlugPath];
2453 return;
2454 end
2455 % Get plugin structure from name
2456 [PlugDesc, errMsg] = GetDescription(PlugDesc);
2457 if ~isempty(errMsg)
2458 return;
2459 end
2460
2461 % === PROCESS DEPENDENCIES ===
2462 % Unload dependent plugins
2463 AllPlugs = GetSupported();
2464 for iPlug = 1:length(AllPlugs)
2465 if ~isempty(AllPlugs(iPlug).RequiredPlugs) && ismember(PlugDesc.Name, AllPlugs(iPlug).RequiredPlugs(:,1))
2466 Unload(AllPlugs(iPlug));
2467 end
2468 end
2469
2470 % === UNLOAD PLUGIN ===
2471 % Do not modify path in compiled mode
2472 if ~bst_iscompiled()
2473 matlabPath = str_split(path, pathsep);
2474 % Remove plugin folder and subfolders from path
2475 allSubFolders = str_split(genpath(PlugPath), pathsep);
2476 for i = 1:length(allSubFolders)
2477 if ismember(allSubFolders{i}, matlabPath)
2478 rmpath(allSubFolders{i});
2479 if isVerbose
2480 disp(['BST> Removing plugin ' PlugDesc.Name ' from path: ' allSubFolders{i}]);
2481 end
2482 end
2483 end
2484 end
2485
2486 % === TEST FUNCTION ===
2487 % Check if test function is still available on path
2488 if ~isempty(PlugDesc.TestFile) && ~isempty(which(PlugDesc.TestFile))
2489 errMsg = ['Plugin ' PlugDesc.Name ' successfully unloaded from: ' 10 PlugPath 10 10 ...
2490 'However, another version is still accessible on the Matlab path:' 10 which(PlugDesc.TestFile) 10 10 ...
2491 'Please remove this folder from the Matlab path.'];
2492 return;
2493 end
2494
2495 % === CALLBACK: POST-UNLOAD ===
2496 [isOk, errMsg] = ExecuteCallback(PlugDesc, 'UnloadedFcn');
2497 if ~isOk
2498 return;
2499 end
2500
2501 % Return success
2502 PlugDesc.isLoaded = 0;
2503 isOk = 1;
2504 end
2505
2506
2507 %% ===== UNLOAD INTERACTIVE =====
2508 % USAGE: [isOk, errMsg, PlugDesc] = UnloadInteractive(PlugName/PlugDesc)
2509 function [isOk, errMsg, PlugDesc] = UnloadInteractive(PlugDesc)
2510 % Open progress bar
2511 isProgress = bst_progress('isVisible');
2512 if ~isProgress
2513 bst_progress('start', 'Plugin manager', 'Unloading plugin...');
2514 end
2515 % Call silent function
2516 [isOk, errMsg, PlugDesc] = Unload(PlugDesc);
2517 % Handle errors
2518 if ~isOk
2519 bst_error(['Unload error:' 10 10 errMsg 10], 'Plugin manager', 0);
2520 elseif ~isempty(errMsg)
2521 java_dialog('msgbox', ['Unload message:' 10 10 errMsg 10], 'Plugin manager');
2522 end
2523 % Close progress bar
2524 if ~isProgress
2525 bst_progress('stop');
2526 end
2527 end
2528
2529
2530 %% ===== LIST =====
2531 % USAGE: strList = bst_plugin('List', Target='installed', isGui=0) % Target={'supported','installed', 'loaded'}
2532 function strList = List(Target, isGui)
2533 % Parse inputs
2534 if (nargin < 2) || isempty(isGui)
2535 isGui = 0;
2536 end
2537 if (nargin < 1) || isempty(Target)
2538 Target = 'Installed';
2539 else
2540 Target = [upper(Target(1)), lower(Target(2:end))];
2541 end
2542 % Get plugins to list
2543 strTitle = sprintf('%s plugins', Target);
2544 switch (Target)
2545 case 'Supported'
2546 PlugDesc = GetSupported();
2547 isInstalled = 0;
2548 case 'Installed'
2549 strTitle = [strTitle ' (*=Loaded)'];
2550 PlugDesc = GetInstalled();
2551 isInstalled = 1;
2552 case 'Loaded'
2553 PlugDesc = GetLoaded();
2554 isInstalled = 1;
2555 otherwise
2556 error(['Invalid target: ' Target]);
2557 end
2558 if isempty(PlugDesc)
2559 return;
2560 end
2561 % Sort by plugin names
2562 [tmp,I] = sort({PlugDesc.Name});
2563 PlugDesc = PlugDesc(I);
2564
2565 % Get Brainstorm info
2566 bstVer = bst_get('Version');
2567 bstDir = bst_get('BrainstormHomeDir');
2568 % Cut version string (short github SHA)
2569 if (length(bstVer.Commit) > 13)
2570 bstGit = ['git @', bstVer.Commit(1:7)];
2571 bstURL = ['https://github.com/brainstorm-tools/brainstorm3/archive/' bstVer.Commit '.zip'];
2572 structVer = bstGit;
2573 else
2574 bstGit = '';
2575 bstURL = '';
2576 structVer = bstVer.Version;
2577 end
2578
2579 % Max lengths
2580 headerName = ' Name';
2581 headerVersion = 'Version';
2582 headerPath = 'Install path';
2583 headerUrl = 'Downloaded from';
2584 headerDate = 'Install date';
2585 maxName = max(cellfun(@length, {PlugDesc.Name, headerName, 'brainstorm'}));
2586 maxVer = min(13, max(cellfun(@length, {PlugDesc.Version, headerVersion, bstGit})));
2587 maxUrl = max(cellfun(@length, {PlugDesc.URLzip, headerUrl, bstURL}));
2588 maxDate = 12;
2589 if isInstalled
2590 strDate = [' | ', headerDate, repmat(' ', 1, maxDate-length(headerDate))];
2591 strDateSep = ['-|-', repmat('-',1,maxDate)];
2592 maxPath = max(cellfun(@length, {PlugDesc.Path, headerPath}));
2593 strPath = [' | ', headerPath, repmat(' ', 1, maxPath-length(headerPath))];
2594 strPathSep = ['-|-', repmat('-',1,maxPath)];
2595 strBstVer = [' | ', bstVer.Date, repmat(' ', 1, maxDate-length(bstVer.Date))];
2596 strBstDir = [' | ', bstDir, repmat(' ', 1, maxPath-length(bstDir))];
2597 else
2598 strDate = '';
2599 strDateSep = '';
2600 strPath = '';
2601 strPathSep = '';
2602 strBstVer = '';
2603 strBstDir = '';
2604 end
2605 % Print column headers
2606 strList = [headerName, repmat(' ', 1, maxName-length(headerName) + 2) ...
2607 ' | ', headerVersion, repmat(' ', 1, maxVer-length(headerVersion)), ...
2608 strDate, strPath, ...
2609 ' | ' headerUrl 10 ...
2610 repmat('-',1,maxName + 2), '-|-', repmat('-',1,maxVer), strDateSep, strPathSep, '-|-', repmat('-',1,maxUrl) 10];
2611
2612 % Print Brainstorm information
2613 strList = [strList '* ', ...
2614 'brainstorm', repmat(' ', 1, maxName-length('brainstorm')) ...
2615 ' | ', bstGit, repmat(' ', 1, maxVer-length(bstGit)), ...
2616 strBstVer, strBstDir, ...
2617 ' | ' bstURL 10];
2618
2619 % Print installed plugins to standard output
2620 for iPlug = 1:length(PlugDesc)
2621 % Loaded plugin
2622 if PlugDesc(iPlug).isLoaded
2623 strLoaded = '* ';
2624 else
2625 strLoaded = ' ';
2626 end
2627 % Cut installation date: Only date, no time
2628 if (length(PlugDesc(iPlug).InstallDate) > 11)
2629 plugDate = PlugDesc(iPlug).InstallDate(1:11);
2630 else
2631 plugDate = PlugDesc(iPlug).InstallDate;
2632 end
2633 % Installed listing
2634 if isInstalled
2635 strDate = [' | ', plugDate, repmat(' ', 1, maxDate-length(plugDate))];
2636 strPath = [' | ', PlugDesc(iPlug).Path, repmat(' ', 1, maxPath-length(PlugDesc(iPlug).Path))];
2637 else
2638 strDate = '';
2639 strPath = '';
2640 end
2641 % Get installed version
2642 if (length(PlugDesc(iPlug).Version) > 13) % Cut version string (short github SHA)
2643 plugVer = ['git @', PlugDesc(iPlug).Version(1:7)];
2644 else
2645 plugVer = PlugDesc(iPlug).Version;
2646 end
2647 % Get installed version with GetVersionFcn
2648 if isempty(plugVer) && isfield(PlugDesc(iPlug),'GetVersionFcn') && ~isempty(PlugDesc(iPlug).GetVersionFcn)
2649 % Load plugin if needed
2650 tmpLoad = 0;
2651 if ~PlugDesc(iPlug).isLoaded
2652 tmpLoad = 1;
2653 Load(PlugDesc(iPlug), 0);
2654 end
2655 try
2656 if ischar(PlugDesc(iPlug).GetVersionFcn)
2657 plugVer = eval(PlugDesc(iPlug).GetVersionFcn);
2658 elseif isa(PlugDesc(iPlug).GetVersionFcn, 'function_handle')
2659 plugVer = feval(PlugDesc(iPlug).GetVersionFcn);
2660 end
2661 catch
2662 disp(['BST> Could not get installed version with callback: ' PlugDesc(iPlug).GetVersionFcn]);
2663 end
2664 % Unload plugin
2665 if tmpLoad
2666 Unload(PlugDesc(iPlug), 0);
2667 end
2668 end
2669 % Assemble plugin text row
2670 strList = [strList strLoaded, ...
2671 PlugDesc(iPlug).Name, repmat(' ', 1, maxName-length(PlugDesc(iPlug).Name)) ...
2672 ' | ', plugVer, repmat(' ', 1, maxVer-length(plugVer)), ...
2673 strDate, strPath, ...
2674 ' | ' PlugDesc(iPlug).URLzip 10];
2675 end
2676 % Display output
2677 if isGui
2678 view_text(strList, strTitle);
2679 % No string returned: display it in the command window
2680 elseif (nargout == 0)
2681 disp([10 strTitle 10 10 strList]);
2682 end
2683 end
2684
2685
2686 %% ===== MENUS: CREATE =====
2687 function j = MenuCreate(jMenu, jPlugsPrev, PlugDesc, fontSize)
2688 import org.brainstorm.icon.*;
2689 % Get all the supported plugins
2690 if isempty(PlugDesc)
2691 PlugDesc = GetSupported();
2692 end
2693 % Get Matlab version
2694 MatlabVersion = bst_get('MatlabVersion');
2695 isCompiled = bst_iscompiled();
2696 % Submenus array
2697 jSub = {};
2698 % Generate submenus array from existing menu
2699 if ~isCompiled && jMenu.getMenuComponentCount > 0
2700 for iItem = 0 : jMenu.getItemCount-1
2701 if ~isempty(regexp(jMenu.getMenuComponent(iItem).class, 'JMenu$', 'once'))
2702 jSub(end+1,1:2) = {char(jMenu.getMenuComponent(iItem).getText), jMenu.getMenuComponent(iItem)};
2703 end
2704 end
2705 end
2706 % Editing an existing menu?
2707 if isempty(jPlugsPrev)
2708 isNewMenu = 1;
2709 j = repmat(struct(), 0);
2710 else
2711 isNewMenu = 0;
2712 j = repmat(jPlugsPrev(1), 0);
2713 end
2714 % Process each plugin
2715 for iPlug = 1:length(PlugDesc)
2716 Plug = PlugDesc(iPlug);
2717 % Skip if Matlab is too old
2718 if ~isempty(Plug.MinMatlabVer) && (Plug.MinMatlabVer > 0) && (MatlabVersion < Plug.MinMatlabVer)
2719 continue;
2720 end
2721 % Skip if not supported in compiled version
2722 if isCompiled && (Plug.CompiledStatus == 0)
2723 continue;
2724 end
2725 % === Add menus for each plugin ===
2726 % One menu per plugin
2727 ij = length(j) + 1;
2728 j(ij).name = Plug.Name;
2729 % Skip if it is already a menu item
2730 if ~isNewMenu
2731 iPlugPrev = ismember({jPlugsPrev.name}, Plug.Name);
2732 if any(iPlugPrev)
2733 j(ij) = jPlugsPrev(iPlugPrev);
2734 continue
2735 end
2736 end
2737 % Category=submenu
2738 if ~isempty(Plug.Category)
2739 if isempty(jSub) || ~ismember(Plug.Category, jSub(:,1))
2740 jParent = gui_component('Menu', jMenu, [], Plug.Category, IconLoader.ICON_FOLDER_OPEN, [], [], fontSize);
2741 jSub(end+1,1:2) = {Plug.Category, jParent};
2742 else
2743 iSub = find(strcmpi(jSub(:,1), Plug.Category));
2744 jParent = jSub{iSub,2};
2745 end
2746 else
2747 jParent = jMenu;
2748 end
2749 % Compiled and included: Simple static menu
2750 if isCompiled && (Plug.CompiledStatus == 2)
2751 j(ij).menu = gui_component('MenuItem', jParent, [], Plug.Name, [], [], [], fontSize);
2752 % Do not create submenus for compiled version
2753 else
2754 % Main menu
2755 j(ij).menu = gui_component('Menu', jParent, [], Plug.Name, [], [], [], fontSize);
2756 % Version
2757 j(ij).version = gui_component('MenuItem', j(ij).menu, [], 'Version', [], [], [], fontSize);
2758 j(ij).versep = java_create('javax.swing.JSeparator');
2759 j(ij).menu.add(j(ij).versep);
2760 % Install
2761 j(ij).install = gui_component('MenuItem', j(ij).menu, [], 'Install', IconLoader.ICON_DOWNLOAD, [], @(h,ev)InstallInteractive(Plug.Name), fontSize);
2762 % Update
2763 j(ij).update = gui_component('MenuItem', j(ij).menu, [], 'Update', IconLoader.ICON_RELOAD, [], @(h,ev)UpdateInteractive(Plug.Name), fontSize);
2764 % Uninstall
2765 j(ij).uninstall = gui_component('MenuItem', j(ij).menu, [], 'Uninstall', IconLoader.ICON_DELETE, [], @(h,ev)UninstallInteractive(Plug.Name), fontSize);
2766 j(ij).menu.addSeparator();
2767 % Custom install
2768 j(ij).custom = gui_component('Menu', j(ij).menu, [], 'Custom install', IconLoader.ICON_FOLDER_OPEN, [], [], fontSize);
2769 j(ij).customset = gui_component('MenuItem', j(ij).custom, [], 'Select installation folder', [], [], @(h,ev)SetCustomPath(Plug.Name), fontSize);
2770 j(ij).custompath = gui_component('MenuItem', j(ij).custom, [], 'Path not set', [], [], [], fontSize);
2771 j(ij).custompath.setEnabled(0);
2772 j(ij).custom.addSeparator();
2773 j(ij).customdel = gui_component('MenuItem', j(ij).custom, [], 'Ignore local installation', [], [], @(h,ev)SetCustomPath(Plug.Name, 0), fontSize);
2774 j(ij).menu.addSeparator();
2775 % Load
2776 j(ij).load = gui_component('MenuItem', j(ij).menu, [], 'Load', IconLoader.ICON_GOOD, [], @(h,ev)LoadInteractive(Plug.Name), fontSize);
2777 j(ij).unload = gui_component('MenuItem', j(ij).menu, [], 'Unload', IconLoader.ICON_BAD, [], @(h,ev)UnloadInteractive(Plug.Name), fontSize);
2778 j(ij).menu.addSeparator();
2779 % Website
2780 j(ij).web = gui_component('MenuItem', j(ij).menu, [], 'Website', IconLoader.ICON_EXPLORER, [], @(h,ev)web(Plug.URLinfo, '-browser'), fontSize);
2781 j(ij).usage = gui_component('MenuItem', j(ij).menu, [], 'Usage statistics', IconLoader.ICON_TS_DISPLAY, [], @(h,ev)bst_userstat(0,Plug.Name), fontSize);
2782 % Extra menus
2783 if ~isempty(Plug.ExtraMenus)
2784 j(ij).menu.addSeparator();
2785 for iMenu = 1:size(Plug.ExtraMenus,1)
2786 j(ij).extra(iMenu) = gui_component('MenuItem', j(ij).menu, [], Plug.ExtraMenus{iMenu,1}, IconLoader.ICON_EXPLORER, [], @(h,ev)bst_call(@eval, Plug.ExtraMenus{iMenu,2}), fontSize);
2787 end
2788 end
2789 end
2790 end
2791 % === Remove menus for plugins with description ===
2792 if ~isempty(jPlugsPrev)
2793 [~, iOld] = setdiff({jPlugsPrev.name}, {PlugDesc.Name});
2794 for ix = 1 : length(iOld)
2795 % Find category menu component
2796 jMenuCat = jPlugsPrev(iOld(ix)).menu.getParent.getInvoker;
2797 % Find index in parent
2798 iDel = [];
2799 for ic = 0 : jMenuCat.getMenuComponentCount-1
2800 if jPlugsPrev(iOld(ix)).menu == jMenuCat.getMenuComponent(ic)
2801 iDel = ic;
2802 break
2803 end
2804 end
2805 % Remove from parent
2806 if ~isempty(iDel)
2807 jMenuCat.remove(iDel);
2808 end
2809 end
2810 end
2811 % Create options for adding user-defined plugins
2812 if ~isCompiled && isNewMenu
2813 menuCategory = 'User defined';
2814 jMenuUserDef = [];
2815 for iMenuItem = 0 : jMenu.getItemCount-1
2816 if ~isempty(regexp(jMenu.getMenuComponent(iMenuItem).class, 'JMenu$', 'once')) && strcmp(char(jMenu.getMenuComponent(iMenuItem).getText), menuCategory)
2817 jMenuUserDef = jMenu.getMenuComponent(iMenuItem);
2818 end
2819 end
2820 if isempty(jMenuUserDef)
2821 jMenuUserDef = gui_component('Menu', jMenu, [], menuCategory, IconLoader.ICON_FOLDER_OPEN, [], [], fontSize);
2822 end
2823 jAddUserDefMan = gui_component('MenuItem', [], [], 'Add manually', IconLoader.ICON_EDIT, [], @(h,ev)AddUserDefDesc('manual'), fontSize);
2824 jAddUserDefFile = gui_component('MenuItem', [], [], 'Add from file', IconLoader.ICON_EDIT, [], @(h,ev)AddUserDefDesc('file'), fontSize);
2825 jAddUserDefUrl = gui_component('MenuItem', [], [], 'Add from URL', IconLoader.ICON_EDIT, [], @(h,ev)AddUserDefDesc('url'), fontSize);
2826 jRmvUserDefMan = gui_component('MenuItem', [], [], 'Remove plugin', IconLoader.ICON_DELETE, [], @(h,ev)RemoveUserDefDesc, fontSize);
2827 % Insert "Add" options at the begining of the 'User defined' menu
2828 jMenuUserDef.insert(jAddUserDefMan, 0);
2829 jMenuUserDef.insert(jAddUserDefFile, 1);
2830 jMenuUserDef.insert(jAddUserDefUrl, 2);
2831 jMenuUserDef.insert(jRmvUserDefMan, 3);
2832 jMenuUserDef.insertSeparator(4);
2833 end
2834 % List
2835 if ~isCompiled && isNewMenu
2836 jMenu.addSeparator();
2837 gui_component('MenuItem', jMenu, [], 'List', IconLoader.ICON_EDIT, [], @(h,ev)List('Installed', 1), fontSize);
2838 end
2839 end
2840
2841
2842 %% ===== MENUS: UPDATE =====
2843 function MenuUpdate(jMenu, fontSize)
2844 import org.brainstorm.icon.*;
2845 global GlobalData
2846 % Get installed and supported plugins
2847 [PlugsInstalled, PlugsSupported]= GetInstalled();
2848 % Get previous menu entries
2849 jPlugs = GlobalData.Program.GUI.pluginMenus;
2850 % Regenerate plugin menu to look for new plugins
2851 jPlugs = MenuCreate(jMenu, jPlugs, PlugsSupported, fontSize);
2852 % Update menu entries
2853 GlobalData.Program.GUI.pluginMenus = jPlugs;
2854 % If compiled: disable most menus
2855 isCompiled = bst_iscompiled();
2856 % Interface scaling
2857 InterfaceScaling = bst_get('InterfaceScaling');
2858 % Update all the plugins
2859 for iPlug = 1:length(jPlugs)
2860 j = jPlugs(iPlug);
2861 PlugName = j.name;
2862 Plug = PlugsInstalled(ismember({PlugsInstalled.Name}, PlugName));
2863 PlugRef = PlugsSupported(ismember({PlugsSupported.Name}, PlugName));
2864 % Is installed?
2865 if ~isempty(Plug)
2866 isInstalled = 1;
2867 elseif ~isempty(PlugRef)
2868 Plug = PlugRef;
2869 isInstalled = 0;
2870 else
2871 disp(['BST> Error: Description not found for plugin: ' PlugName]);
2872 continue;
2873 end
2874 isLoaded = isInstalled && Plug.isLoaded;
2875 isManaged = isInstalled && Plug.isManaged;
2876 % Compiled included: no submenus
2877 if isCompiled && (PlugRef.CompiledStatus == 2)
2878 j.menu.setEnabled(1);
2879 if (InterfaceScaling ~= 100)
2880 j.menu.setIcon(IconLoader.scaleIcon(IconLoader.ICON_GOOD, InterfaceScaling / 100));
2881 else
2882 j.menu.setIcon(IconLoader.ICON_GOOD);
2883 end
2884 % Otherwise: all available
2885 else
2886 % Main menu: Available/Not available
2887 j.menu.setEnabled(isInstalled || ~isempty(Plug.URLzip));
2888 % Current version
2889 if ~isInstalled
2890 j.version.setText('<HTML><FONT color="#707070"><I>Not installed</I></FONT>');
2891 elseif ~isManaged && ~isempty(Plug.Path)
2892 j.version.setText('<HTML><FONT color="#707070"><I>Custom install</I></FONT>')
2893 elseif ~isempty(Plug.Version) && ischar(Plug.Version)
2894 strVer = Plug.Version;
2895 % If downloading from github
2896 if isGithubMaster(Plug.URLzip)
2897 % Show installation date, if available
2898 if ~isempty(Plug.InstallDate)
2899 strVer = Plug.InstallDate(1:11);
2900 % Show only the short SHA (7 chars)
2901 elseif (length(Plug.Version) >= 30)
2902 strVer = Plug.Version(1:7);
2903 end
2904 end
2905 j.version.setText(['<HTML><FONT color="#707070"><I>Installed version: ' strVer '</I></FONT>'])
2906 elseif isInstalled
2907 j.version.setText('<HTML><FONT color="#707070"><I>Installed</I></FONT>');
2908 end
2909 % Main menu: Icon
2910 if isCompiled && isInstalled
2911 menuIcon = IconLoader.ICON_GOOD;
2912 elseif isLoaded % Loaded
2913 menuIcon = IconLoader.ICON_GOOD;
2914 elseif isInstalled % Not loaded
2915 menuIcon = IconLoader.ICON_BAD;
2916 else
2917 menuIcon = IconLoader.ICON_NEUTRAL;
2918 end
2919 if (InterfaceScaling ~= 100)
2920 j.menu.setIcon(IconLoader.scaleIcon(menuIcon, InterfaceScaling / 100));
2921 else
2922 j.menu.setIcon(menuIcon);
2923 end
2924 % Install
2925 j.install.setEnabled(~isInstalled);
2926 if ~isInstalled && ~isempty(PlugRef.Version) && ischar(PlugRef.Version)
2927 j.install.setText(['<HTML>Install <FONT color="#707070"><I>(' PlugRef.Version ')</I></FONT>'])
2928 else
2929 j.install.setText('Install');
2930 end
2931 % Update
2932 j.update.setEnabled(isManaged);
2933 if isInstalled && ~isempty(PlugRef.Version) && ischar(PlugRef.Version)
2934 j.update.setText(['<HTML>Update <FONT color="#707070"><I>(' PlugRef.Version ')</I></FONT>'])
2935 else
2936 j.update.setText('Update');
2937 end
2938 % Uninstall
2939 j.uninstall.setEnabled(isManaged);
2940 % Custom install
2941 j.custom.setEnabled(~isManaged);
2942 if ~isempty(Plug.Path)
2943 j.custompath.setText(Plug.Path);
2944 else
2945 j.custompath.setText('Path not set');
2946 end
2947 % Load/Unload
2948 j.load.setEnabled(isInstalled && ~isLoaded && ~isCompiled);
2949 j.unload.setEnabled(isLoaded && ~isCompiled);
2950 % Web
2951 j.web.setEnabled(~isempty(Plug.URLinfo));
2952 % Extra menus: Update availability
2953 if ~isempty(Plug.ExtraMenus)
2954 for iMenu = 1:size(Plug.ExtraMenus,1)
2955 if (size(Plug.ExtraMenus,2) == 3) && ~isempty(Plug.ExtraMenus{3})
2956 if (strcmpi(Plug.ExtraMenus{3}, 'loaded') && isLoaded) ...
2957 || (strcmpi(Plug.ExtraMenus{3}, 'installed') && isInstalled) ...
2958 || (strcmpi(Plug.ExtraMenus{3}, 'always'))
2959 j.extra(iMenu).setEnabled(1);
2960 else
2961 j.extra(iMenu).setEnabled(0);
2962 end
2963 end
2964 end
2965 end
2966 end
2967 end
2968 j.menu.repaint()
2969 j.menu.getParent().repaint()
2970 end
2971
2972
2973 %% ===== SET CUSTOM PATH =====
2974 function SetCustomPath(PlugName, PlugPath)
2975 % Parse inputs
2976 if (nargin < 2) || isempty(PlugPath)
2977 PlugPath = [];
2978 end
2979 % Custom plugin paths
2980 PluginCustomPath = bst_get('PluginCustomPath');
2981 % Get plugin description
2982 PlugDesc = GetSupported(PlugName);
2983 if isempty(PlugDesc)
2984 return;
2985 end
2986 % Get installed plugin
2987 PlugInst = GetInstalled(PlugName);
2988 isInstalled = ~isempty(PlugInst);
2989 isManaged = isInstalled && PlugInst.isManaged;
2990 if isManaged
2991 bst_error(['Plugin ' PlugName ' is already installed by Brainstorm, uninstall it first.'], 0);
2992 return;
2993 end
2994 % Ask install path to user
2995 isWarning = 1;
2996 if isempty(PlugPath)
2997 PlugPath = uigetdir(PlugInst.Path, ['Select ' PlugName ' directory.']);
2998 if isequal(PlugPath, 0)
2999 PlugPath = [];
3000 end
3001 % If removal is requested
3002 elseif isequal(PlugPath, 0)
3003 PlugPath = [];
3004 isWarning = 0;
3005 end
3006 % If the directory did not change: nothing to do
3007 if (isInstalled && isequal(PlugInst.Path, PlugPath)) || (~isInstalled && isempty(PlugPath))
3008 return;
3009 end
3010 % Unload previous version
3011 if isInstalled && ~isempty(PlugInst.Path) && PlugInst.isLoaded
3012 Unload(PlugName);
3013 end
3014 % Check if this is a valid plugin folder
3015 if isempty(PlugPath) || ~file_exist(PlugPath)
3016 PlugPath = [];
3017 end
3018 if ~isempty(PlugPath) && ~isempty(PlugDesc.TestFile)
3019 isValid = 0;
3020 if file_exist(bst_fullfile(PlugPath, PlugDesc.TestFile))
3021 isValid = 1;
3022 elseif ~isempty(PlugDesc.LoadFolders)
3023 for iFolder = 1:length(PlugDesc.LoadFolders)
3024 if file_exist(bst_fullfile(PlugPath, PlugDesc.LoadFolders{iFolder}, PlugDesc.TestFile))
3025 isValid = 1;
3026 end
3027 end
3028 end
3029 if ~isValid
3030 PlugPath = [];
3031 end
3032 end
3033 % Save path
3034 PluginCustomPath.(PlugName) = PlugPath;
3035 bst_set('PluginCustomPath', PluginCustomPath);
3036 % Load plugin
3037 if ~isempty(PlugPath)
3038 [isOk, errMsg, PlugDesc] = Load(PlugName);
3039 % Ignored warnings
3040 elseif ~isWarning
3041 isOk = 1;
3042 errMsg = [];
3043 % Invalid path
3044 else
3045 isOk = 0;
3046 if ~isempty(PlugDesc.TestFile)
3047 errMsg = ['The file ' PlugDesc.TestFile ' could not be found in selected folder.'];
3048 else
3049 errMsg = 'No valid folder was found.';
3050 end
3051 end
3052 % Handle errors
3053 if ~isOk
3054 bst_error(['An error occurred while configuring plugin ' PlugName ':' 10 10 errMsg 10], 'Plugin manager', 0);
3055 elseif ~isempty(errMsg)
3056 java_dialog('msgbox', ['Configuration message:' 10 10 errMsg 10], 'Plugin manager');
3057 elseif isWarning
3058 java_dialog('msgbox', ['Plugin ' PlugName ' successfully loaded.']);
3059 end
3060 end
3061
3062
3063 %% ===== ARCHIVE SOFTWARE ENVIRONMENT =====
3064 % USAGE: Archive(OutputFile=[ask])
3065 function Archive(OutputFile)
3066 % Parse inputs
3067 if (nargin < 1) || isempty(OutputFile)
3068 OutputFile = [];
3069 end
3070 % Get date string
3071 c = clock();
3072 strDate = sprintf('%02d%02d%02d', c(1)-2000, c(2), c(3));
3073 % Get output filename
3074 if isempty(OutputFile)
3075 % Get default directories
3076 LastUsedDirs = bst_get('LastUsedDirs');
3077 % Default output filename
3078 OutputFile = bst_fullfile(LastUsedDirs.ExportScript, ['bst_env_' strDate '.zip']);
3079 % File selection
3080 OutputFile = java_getfile('save', 'Export environment', OutputFile, 'single', 'files', ...
3081 {{'.zip'}, 'Zip files (*.zip)', 'ZIP'}, 1);
3082 if isempty(OutputFile)
3083 return
3084 end
3085 % Save new default export path
3086 LastUsedDirs.ExportScript = bst_fileparts(OutputFile);
3087 bst_set('LastUsedDirs', LastUsedDirs);
3088 end
3089
3090 % ===== TEMP FOLDER =====
3091 bst_progress('start', 'Export environment', 'Creating temporary folder...');
3092
3093 % ===== COPY BRAINSTORM =====
3094 bst_progress('text', 'Copying: brainstorm...');
3095 % Get Brainstorm path and version
3096 bstVer = bst_get('Version');
3097 bstDir = bst_get('BrainstormHomeDir');
3098 % Create temporary folder for storing all the files to package
3099 TmpDir = bst_get('BrainstormTmpDir', 0, 'bstenv');
3100 % Get brainstorm3 destination folder: add version number
3101 if ~isempty(bstVer.Version) && ~any(bstVer.Version == '?')
3102 envBst = bst_fullfile(TmpDir, ['brainstorm', bstVer.Version]);
3103 else
3104 [tmp, bstName] = bst_fileparts(bstDir);
3105 envBst = bst_fullfile(TmpDir, bstName);
3106 end
3107 % Add git commit hash
3108 if (length(bstVer.Commit) >= 30)
3109 envBst = [envBst, '_', bstVer.Commit(1:7)];
3110 end
3111 % Copy brainstorm3 folder
3112 isOk = file_copy(bstDir, envBst);
3113 if ~isOk
3114 error(['Cannot copy folder: "' bstDir '" to "' envBst '"']);
3115 end
3116
3117 % ===== COPY DEFAULTS =====
3118 bst_progress('text', 'Copying: user defaults...');
3119 % Get user defaults folder
3120 userDef = bst_get('UserDefaultsDir');
3121 envDef = bst_fullfile(envBst, 'defaults');
3122 isOk = file_copy(userDef, envDef);
3123 if ~isOk
3124 error(['Cannot merge folder: "' userDef '" into "' envDef '"']);
3125 end
3126
3127 % ===== COPY USER PROCESSES =====
3128 bst_progress('text', 'Copying: user processes...');
3129 % Get user process folder
3130 userProc = bst_get('UserProcessDir');
3131 envProc = bst_fullfile(envBst, 'toolbox', 'process', 'functions');
3132 isOk = file_copy(userProc, envProc);
3133 if ~isOk
3134 error(['Cannot merge folder: "' userProc '" into "' envProc '"']);
3135 end
3136
3137 % ===== COPY PLUGINS ======
3138 % Get list of plugins to package
3139 PlugDesc = GetInstalled();
3140 % Destination plugin directory
3141 envPlugins = bst_fullfile(envBst, 'plugins');
3142 % Copy each installed plugin
3143 for iPlug = 1:length(PlugDesc)
3144 bst_progress('text', ['Copying plugin: ' PlugDesc(iPlug).Name '...']);
3145 envPlug = bst_fullfile(envPlugins, PlugDesc(iPlug).Name);
3146 isOk = file_copy(PlugDesc(iPlug).Path, envPlug);
3147 if ~isOk
3148 error(['Cannot copy folder: "' PlugDesc(iPlug).Path '" into "' envProc '"']);
3149 end
3150 end
3151 % Copy user-defined JSON files
3152 PlugJson = dir(fullfile(bst_get('UserPluginsDir'), 'plugin_*.json'));
3153 for iPlugJson = 1:length(PlugJson)
3154 bst_progress('text', ['Copying use-defined plugin JSON file: ' PlugJson(iPlugJson).name '...']);
3155 plugJsonFile = bst_fullfile(PlugJson(iPlugJson).folder, PlugJson(iPlugJson).name);
3156 envPlugJson = bst_fullfile(envPlugins, PlugJson(iPlugJson).name);
3157 isOk = file_copy(plugJsonFile, envPlugJson);
3158 if ~isOk
3159 error(['Cannot copy file: "' plugJsonFile '" into "' envProc '"']);
3160 end
3161 end
3162
3163 % ===== SAVE LIST OF VERSIONS =====
3164 strList = bst_plugin('List', 'installed', 0);
3165 % Open file versions.txt
3166 VersionFile = bst_fullfile(TmpDir, 'versions.txt');
3167 fid = fopen(VersionFile, 'wt');
3168 if (fid < 0)
3169 error(['Cannot save file: ' VersionFile]);
3170 end
3171 % Save Brainstorm plugins list
3172 fwrite(fid, strList);
3173 % Save Matlab ver command
3174 strMatlab = evalc('ver');
3175 fwrite(fid, [10 10 strMatlab]);
3176 % Close file
3177 fclose(fid);
3178
3179 % ===== ZIP FILES =====
3180 bst_progress('text', 'Zipping environment...');
3181 % Zip files with bst_env_* being the first level
3182 zip(OutputFile, TmpDir, bst_fileparts(TmpDir));
3183 % Delete the temporary files
3184 file_delete(TmpDir, 1, 1);
3185 % Close progress bar
3186 bst_progress('stop');
3187 end
3188
3189
3190 %% ============================================================================
3191 % ===== PLUGIN-SPECIFIC FUNCTIONS ============================================
3192 % ============================================================================
3193
3194 %% ===== LINK TOOLBOX-SPM =====
3195 % USAGE: bst_plugin('LinkSpmToolbox', Action)
3196 % 0=Delete/1=Create/2=Check a symbolic link for a Toolbox in SPM12 toolbox folder
3197 function LinkSpmToolbox(Action, ToolboxName)
3198 % Get SPM12 plugin
3199 PlugSpm = GetInstalled('spm12');
3200 if isempty(PlugSpm)
3201 error('Plugin SPM12 is not loaded.');
3202 elseif ~PlugSpm.isLoaded
3203 [isOk, errMsg, PlugSpm] = Load('spm12');
3204 if ~isOk
3205 error('Plugin SPM12 cannot be loaded.');
3206 end
3207 end
3208 % Get SPM plugin path
3209 if ~isempty(PlugSpm.SubFolder)
3210 spmToolboxDir = bst_fullfile(PlugSpm.Path, PlugSpm.SubFolder, 'toolbox');
3211 else
3212 spmToolboxDir = bst_fullfile(PlugSpm.Path, 'toolbox');
3213 end
3214 if ~file_exist(spmToolboxDir)
3215 error(['Could not find SPM12 toolbox folder: ' spmToolboxDir]);
3216 end
3217 % Toolbox plugin path
3218 spmToolboxDirTarget = bst_fullfile(spmToolboxDir, ToolboxName);
3219 % Get toolbox plugin
3220 PlugToolbox = GetInstalled(ToolboxName);
3221
3222 % Check link
3223 if (Action == 2)
3224 % Link exists and works: return here
3225 if file_exist(bst_fullfile(spmToolboxDirTarget, PlugToolbox.TestFile))
3226 return;
3227 % Link doesn't exist: Create it
3228 else
3229 Action = 1;
3230 end
3231 end
3232 % If folder already exists
3233 if file_exist(spmToolboxDirTarget)
3234 % If setting install and SPM is not managed by Brainstorm: do not risk deleting user's install
3235 if (Action == 1) && ~PlugSpm.isManaged
3236 error([upper(ToolboxName) ' seems already set up: ' spmToolboxDirTarget]);
3237 end
3238 % All the other cases: delete existing toolbox folder
3239 if ispc
3240 rmCall = ['rmdir /q /s "' spmToolboxDirTarget '"'];
3241 else
3242 rmCall = ['rm -rf "' spmToolboxDirTarget '"'];
3243 end
3244 disp(['BST> Deleting existing SPM12 toolbox: ' rmCall]);
3245 [status,result] = system(rmCall);
3246 if (status ~= 0)
3247 error(['Error deleting link: ' result]);
3248 end
3249 end
3250 % Create new link
3251 if (Action == 1)
3252 if isempty(PlugToolbox) || ~PlugToolbox.isLoaded
3253 error(['Plugin ' upper(ToolboxName) ' is not loaded.']);
3254 end
3255 % Return if installation is not complete yet (first load before installation ends)
3256 if isempty(PlugToolbox.InstallDate)
3257 return
3258 end
3259 % Define source and target for the link
3260 if ~isempty(PlugToolbox.SubFolder)
3261 linkTarget = bst_fullfile(PlugToolbox.Path, PlugToolbox.SubFolder);
3262 else
3263 linkTarget = PlugToolbox.Path;
3264 end
3265 linkFile = spmToolboxDirTarget;
3266 % Create link
3267 if ispc
3268 linkCall = ['mklink /D "' linkFile '" "' linkTarget '"'];
3269 else
3270 linkCall = ['ln -s "' linkTarget '" "' linkFile '"'];
3271 end
3272 disp(['BST> Creating symbolic link: ' linkCall]);
3273 [status,result] = system(linkCall);
3274 if (status ~= 0)
3275 error(['Error creating link: ' result]);
3276 end
3277 end
3278 end
3279
3280
3281 %% ===== SET PROGRESS LOGO =====
3282 % USAGE: SetProgressLogo(PlugDesc/PlugName) % Set progress bar image
3283 % SetProgressLogo([]) % Remove progress bar image
3284 function SetProgressLogo(PlugDesc)
3285 % Remove image
3286 if (nargin < 1) || isempty(PlugDesc)
3287 bst_progress('removeimage');
3288 bst_progress('removelink');
3289 % Set image
3290 else
3291 % Get plugin description
3292 if ischar(PlugDesc)
3293 PlugDesc = GetSupported(PlugDesc);
3294 end
3295 % Set logo file
3296 if isempty(PlugDesc.LogoFile)
3297 PlugDesc.LogoFile = GetLogoFile(PlugDesc);
3298 end
3299 if ~isempty(PlugDesc.LogoFile)
3300 bst_progress('setimage', PlugDesc.LogoFile);
3301 end
3302 % Set link
3303 if ~isempty(PlugDesc.URLinfo)
3304 bst_progress('setlink', PlugDesc.URLinfo);
3305 end
3306 end
3307 end
3308
3309
3310 %% ===== NOT SUPPORTED APPLE SILICON =====
3311 % Return list of plugins not supported on Apple silicon
3312 function pluginNames = PluginsNotSupportAppleSilicon()
3313 pluginNames = { 'duneuro', 'mcxlab-cuda'};
3314 end
3315
3316 %% ===== MATCH STRING EDGES =====
3317 % Check if a string 'strA' starts (or ends) with string B
3318 function result = strMatchEdge(a, b, edge)
3319 b = regexptranslate('escape', b);
3320 if strcmpi(edge, 'start')
3321 result = ~isempty(regexp(a, ['^', b], 'once'));
3322 elseif strcmpi(edge, 'end')
3323 result = ~isempty(regexp(a, [b, '$'], 'once'));
3324 else
3325 result = 0;
3326 end
3327 end
Plugin definition
The plugins registered in Brainstorm are listed in bst_plugin.m / GetSupported. Each one is an entry in the PlugDesc array, following the structure defined in db_template('plugdesc'). The fields allowed are described below.
Mandatory fields:
Name: String, Plugin name = subfolder in the Brainstorm user folder
Version: String, version of the plugin (eg. '1.2', '21a', 'github-master', 'latest')
URLzip: Download URL (zip file accessible over HTTP/HTTPS/FTP)
URLinfo: Information URL: Software website
Optional fields:
Category: String, sub-menu in which the plugin is listed
AutoUpdate: If 1, plugin is updated automatically when there is a new version available
ExtraMenus: Cell matrix {Nx2} with list of entries to add to the plugins menu, eg. {'Download page', 'web(http://...'')'; 'Tutorial', 'web(http://...)'}
TestFile: Function/file name to check the existence of the plugin outside of the Brainstorm user folder
ReadmeFile: Text filename (relative to the plugin path) - If empty, try using brainstorm3/doc/plugin/<Name>_readme.txt
LogoFile: Logo filename (relative to the plugin path) - If empty, try using brainstorm3/doc/plugin/<Name>_logo.[gif|png]
MinMatlabVer: Minimum Matlab version, as returned by bst_get('MatlabVersion')
RequiredPlugs: Cell-array of required plugin names, to install/load before this one: {Nx2}=>{'plugname','version';...} or {Nx1}=>{'plugname';...}
UnloadPlugs: Cell-array of incompatible plugin names, to remove from path before adding
LoadFolders: Cell-array of subfolders to add to the path when setting the plugin up (use {'*'} to load all subfolders)
GetVersionFcn: String to eval to get the version (after installation)
InstalledFcn: String to eval or function handle to call after installing the plugin
UninstalledFcn: String to eval or function handle to call after uninstalling the plugin
LoadedFcn: String to eval or function handle to call after loading the plugin
UnloadedFcn: String to eval or function handle to call after unloading the plugin
Fields set when installing or loading the plugin:
SubFolder: If all the code is in a subfolder: detect this at installation time
Path: Set at runtime: Installation path for this plugin
Processes: List of process functions to be added to the pipeline manager
isLoaded: Set at runtime: 0=Not loaded, 1=Loaded (folder and specific subfolders added to Matlab path)
isManaged: Set at runtime: 0=Installed by the user, 1=Installed automatically by Brainstorm