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