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