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