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