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