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: The package is downloaded in the Brainstorm user folder: $HOME/.brainstorm/plugins/

Uninstall: Delete the plugin folder and all its subfolders.

Load: Adds all the subfolders needed by the plugin to the Matlab path, plus other optional taks.

Unload: Removes all the plugin folders from the Matlab path.

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.

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

Command-line management

The calls to install or manage plugins are all documented in the header of bst_plugin.m:

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

Tutorials/Plugins (last edited 2021-03-10 17:38:28 by FrancoisTadel)