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