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