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.

example1.gif

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:

list.gif

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.

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:

Optional fields:

Fields set when installing the plugin:

Fields set when loading the plugin:

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:

  1. Manually: Provide the mandatory fields of the plugin definition. All other fields will be set to their default values.

  2. From file: Provide the path to a JSON file with the plugin definition.

  3. 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.

user_def_menu.gif

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





Feedback on the documentation (typos, unclear sections, missing information)
For questions, bug reports, and feature requests, please use the Brainstorm Forum.
Email address (if you expect an answer):


Tutorials/Plugins (last edited 2026-10-23 14:49:29 by RaymundoCassani)