Plugins

Authors: Francois Tadel

Brainstorm connects with features from many third-party libraries of methods. The external software can be downloaded or updated automatically by Brainstorm when needed. This tutorial presents the API to register and manage plugins.

Interactive management

The Brainstorm interface offers menus to Install/Update/Uninstall plugins.

example1.gif

Install: Downloads and unzips the package and all its dependencies in the Brainstorm user folder:
$HOME/.brainstorm/plugins/

Uninstall: Deletes the plugin folder, all its subfolders, and all the other plugins that depend on it.

Load: Adds all the subfolders needed by the plugin to the Matlab path, plus other optional tasks. Loading a plugin causes the recursive load of all the other plugins required by this plugin.

Unload: Removes all the plugin folders from the Matlab path, and all the plugins that depend on it.

Update: Some plugins are designed to update themselves automatically whenever a new version is available online, or requested by Brainstorm. Others plugins must be updated manually. Updating is equivalent to uninstalling without removing the dependencies, and then nstalling again.

Manual install: If you already have a given plugin installed on your computer (eg. FieldTrip, SPM12) and don't want Brainstorm to manage the download/update or the Matlab path for you, reference it with the menu: Custom install > Set installation folder.

List: You can list all the installed plugins with the menu List:

list.gif

Brainstorm plugins by category

Generic toolboxes:

Anatomy processing:

Inverse modeling:

Forward modeling:

Simulation:

Statistics:

I/O Libraries for specific file formats:

fNIRS:

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:

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

Tutorials/Plugins (last edited 2021-11-01 14:20:26 by FrancoisTadel)