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