toolbox/process/bst_report.m
function varargout = bst_report( varargin )
% BST_REPORT: Manages the reports generated by the process interface.
% 
% USAGE:  bst_report('Start',   sInputs)
%         bst_report('Start',   FileNames)
%         bst_report('Warning', sProcess, sInputs, strMsg)
%         bst_report('Error',   sProcess, sInputs, strMsg)
%         bst_report('Info',    sProcess, sInputs, strMsg)
%         bst_report('Process', sProcess, sInputs, strMsg)
%         bst_report('Add',     strType, sProcess, sInputs, strMsg)
%         bst_report('Add',     cNew)
%         bst_report('Save',    sInputs,   ReportFile=[])
%         bst_report('Save',    FileNames, ReportFile=[])
%         bst_report('Open',    ReportFile=[ask], isFullReport=1)
%         bst_report('Reset')
%         bst_report('Export',  ReportFile, HtmlFile=[ask])
%         bst_report('Export',  ReportFile, HtmlDir)
%         bst_report('Email',   ReportFile, username, to, subject, isFullReport=1, isOnlyText=0)
%         bst_report('Close')
%         bst_report('Recall', ReportFile=[ask])
%         bst_report('ClearHistory', isUserConfirm=1)
%         bst_report('PrintToHtml', Reports, isFullReport=1, isOnlyText=0)
% 
%         bst_report('Snapshot', 'registration', AnyFile,      Comment,  Modality, Orientation='left')   : left,right,top,bottom,back,front
%         bst_report('Snapshot', 'ssp',          RawFile,      Comment)
%         bst_report('Snapshot', 'noiscov',      AnyFile,      Comment)
%         bst_report('Snapshot', 'ndatacov',     AnyFile,      Comment)
%         bst_report('Snapshot', 'headmodel',    AnyFile,      Comment)
%         bst_report('Snapshot', 'data',         DataFile,     Comment, Modality, Time=start, RowName=[All])
%         bst_report('Snapshot', 'topo',         DataFile,     Comment, Modality, Time=start, Freq=0)
%         bst_report('Snapshot', 'topo',         DataFile,     Comment, Modality, [start,stop,nImages], Freq=0)
%         bst_report('Snapshot', 'sources',      ResultsFile,  Comment, Time=start, DataThreshold=.3, Orientation='left', SurfSmooth=30%, Freq=0)
%         bst_report('Snapshot', 'sources',      ResultsFile,  Comment, [start,stop,nImages], DataThreshold=.3, Orientation='left', SurfSmooth=30%, Freq=0)   : Produces a contact sheet view
%         bst_report('Snapshot', 'mriviewer',    ResultsFile,  Comment, Time=start, DataThreshold=.3, Freq=0, XYZmni=[])
%         bst_report('Snapshot', 'spectrum',     TimefreqFile, Comment, RowName=[All], Freq=0)
%         bst_report('Snapshot', 'dipoles',      DipolesFile,  Comment, Goodness=0, Orientation='left')
%         bst_report('Snapshot', 'timefreq',     TimefreqFile, Comment, RowName=[All], Time=start, Freq=0)
%         bst_report('Snapshot', 'connectimage', ConnectNFile, Comment, Time=start, Freq=0);
%         bst_report('Snapshot', 'connectgraph', ConnectNFile, Comment, Threshold, Time=start, Freq=0);
%         bst_report('Snapshot', jFrame,         AnyFile,      Comment, WinPos=[200,200,400,250])
%         bst_report('Snapshot', hFig,           AnyFile,      Comment, WinPos=[200,200,400,250])
%
% NOTES: 
%    - sProcess can be replaced by the name of the process function
%    - sInputs can be replaced by a cell array of filenames, or a filename

% @=============================================================================
% This function is part of the Brainstorm software:
% https://neuroimage.usc.edu/brainstorm
% 
% Copyright (c) University of Southern California & McGill University
% This software is distributed under the terms of the GNU General Public License
% as published by the Free Software Foundation. Further details on the GPLv3
% license can be found at http://www.gnu.org/copyleft/gpl.html.
% 
% FOR RESEARCH PURPOSES ONLY. THE SOFTWARE IS PROVIDED "AS IS," AND THE
% UNIVERSITY OF SOUTHERN CALIFORNIA AND ITS COLLABORATORS DO NOT MAKE ANY
% WARRANTY, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF
% MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, NOR DO THEY ASSUME ANY
% LIABILITY OR RESPONSIBILITY FOR THE USE OF THIS SOFTWARE.
%
% For more information type "brainstorm license" at command prompt.
% =============================================================================@
%
% Authors: Francois Tadel, 2012-2022

eval(macro_method);
end


%% ===== START =====
function Start(sInputs)
    % If there were no inputs
    if (nargin < 1) || isempty(sInputs)
        sInputs = [];
    end
    % Reset current report
    Reset();
    % Get current protocol description
    ProtocolInfo = bst_get('ProtocolInfo');
    % Get Brainstorm version
    bstVersion = bst_get('Version');
    % Add Brainstorm version
    Add('version', [], [], bstVersion.Version);
    % Add start entry
    Add('start', [], sInputs, ProtocolInfo);
end


%% ===== ADD REPORTS =====
% USAGE:  bst_report('Add', strType, sProcess, sInputs, strMsg)
function Add(strType, sProcess, sInputs, strMsg)
    global GlobalData;
    % Initialize reports list
    if isempty(GlobalData)
        return;
    end
    if isempty(GlobalData.ProcessReports.Reports) || ~iscell(GlobalData.ProcessReports.Reports) || (size(GlobalData.ProcessReports.Reports,2) ~= 5)
        Reset();
    end
    % No input
    if isempty(strType)
        return
    % Syntax error
    elseif (iscell(strType) && (size(strType,2) ~= 5)) || (~iscell(strType) && (nargin ~= 4))
        disp('REPORT> ERROR: Invalid syntax');
        return
    end

    % If process is a name
    if ~isempty(sProcess) && ischar(sProcess) && ~strcmpi(strType, 'image')
        sProcess = panel_process_select('GetProcess', sProcess);
    end
    % Transform input structures to a filename list
    FileNames = GetFilesList(sInputs, 0);
    % Time string
    strTime = datestr(now);
    cNew = {strType, sProcess, FileNames, strMsg, strTime};

    % Add to list of registered reports
    GlobalData.ProcessReports.Reports = cat(1, GlobalData.ProcessReports.Reports, cNew);
    % If current report viewer is open: update it
    if ~isempty(GlobalData.ProcessReports.jFrameReport) && GlobalData.ProcessReports.jFrameReport.isVisible() && isequal(GlobalData.ProcessReports.CurrentFile, 'current')
        Open('current');
    end
end


%% ===== PROCESS =====
function Process(sProcess, sInputs)
    Add('process', sProcess, sInputs, '');
end

%% ===== WARNING =====
function Warning(sProcess, sInputs, strMsg)
    Add('warning', sProcess, sInputs, strMsg);
end

%% ===== ERROR =====
function Error(sProcess, sInputs, strMsg)
    Add('error', sProcess, sInputs, strMsg);
end

%% ===== INFO =====
function Info(sProcess, sInputs, strMsg)
    Add('info', sProcess, sInputs, strMsg);
end


%% ===== SNAPSHOT =====
% USAGE:  bst_report('Snapshot', SnapType,       FileName,     Comment, ...)
%   img = bst_report('Snapshot', 'registration', AnyFile,      Comment,  Modality, Orientation='left')   : left,right,top,bottom,back,front
%         bst_report('Snapshot', 'ssp',          RawFile,      Comment)
%         bst_report('Snapshot', 'noiscov',      AnyFile,      Comment)
%         bst_report('Snapshot', 'ndatacov',     AnyFile,      Comment)
%         bst_report('Snapshot', 'headmodel',    AnyFile,      Comment)
%         bst_report('Snapshot', 'data',         DataFile,     Comment, Modality, Time=start, RowName=[All])
%         bst_report('Snapshot', 'topo',         DataFile,     Comment, Modality, Time=start, Freq=0)
%         bst_report('Snapshot', 'topo',         DataFile,     Comment, Modality, [start,stop,nImages], Freq=0)
%         bst_report('Snapshot', 'sources',      ResultsFile,  Comment, Time=start, DataThreshold=.3, Orientation='left', SurfSmooth=30%, Freq=0)
%         bst_report('Snapshot', 'sources',      ResultsFile,  Comment, [start,stop,nImages], DataThreshold=.3, Orientation='left', SurfSmooth=30%, Freq=0)   : Produces a contact sheet view
%         bst_report('Snapshot', 'mriviewer',    ResultsFile,  Comment, Time=start, DataThreshold=.3, Freq=0, XYZmni=[])
%         bst_report('Snapshot', 'spectrum',     TimefreqFile, Comment, RowName=[All], Freq=0)
%         bst_report('Snapshot', 'dipoles',      DipolesFile,  Comment, Goodness=0, Orientation='left')
%         bst_report('Snapshot', 'timefreq',     TimefreqFile, Comment, RowName=[All], Time=start, Freq=0)
%         bst_report('Snapshot', 'connectimage', ConnectFile,  Comment, Time=start, Freq=0)
%         bst_report('Snapshot', 'connectgraph', ConnectFile,  Comment, Threshold=0, Time=start, Freq=0)
%         bst_report('Snapshot', jFrame,         AnyFile,      Comment, WinPos=[200,200,400,250])
%         bst_report('Snapshot', hFig,           AnyFile,      Comment, WinPos=[200,200,400,250])
%
% Optional output img is a cell array of cell arrays with image(s) data for each FileName
% Note: All the input files can be either strings (one file) or cell-array of strings (many files)
function img = Snapshot(SnapType, FileName, Comment, varargin)
    % No file in input: nothing to do
    if (nargin < 1)
        return;
    end
    if (nargin < 2)
        FileName = [];
    end
    if (nargin < 3)
        Comment = [];
    end
    % Recustive call if inputs are cell arrays of filenames
    if ~isempty(FileName) && iscell(FileName)
        if nargout > 0
            % Output: cell array of cell arrays with image(s) for each FileName
            imgs = cell(1, length(FileName));
            for iFile = 1:length(FileName)
                imgs{iFile} = Snapshot(SnapType, FileName{iFile}, Comment, varargin{:});
            end
            img = imgs;
        else
            for iFile = 1:length(FileName)
                Snapshot(SnapType, FileName{iFile}, Comment, varargin{:});
            end
        end
        return;
    end
    % Get current window layout
    curLayout = bst_get('Layout', 'WindowManager');
    if ~isempty(curLayout)
        bst_set('Layout', 'WindowManager', 'None');
    end
    % Hide brainstorm window
    if bst_get('isGUI')
        jBstFrame = bst_get('BstFrame');
        jBstFrame.setVisible(0);
    end
    % Hide progress bar
    isProgress = bst_progress('isVisible');
    if isProgress
        bst_progress('hide');
    end
    % Default figure position
    winPos = [200, 200, 400, 250];
    imgLegend = {};
    % If the first argument is a figure handle
    if ~ischar(SnapType)
        if all(isgraphics(SnapType))
            hFig = SnapType;
            SnapType = 'figure';
            if (length(varargin) >= 1)
                winPos = varargin{1};
            end
        elseif isa(SnapType, 'javax.swing.JFrame') || all(arrayfun(@(x) isa(x, 'javax.swing.JFrame'), SnapType))
            jFrame = SnapType;
            SnapType = 'jframe';
        else
            return;
        end
    end
    % Hide scouts
    ScoutsOptions = panel_scout('GetScoutsOptions');
    if ~isempty(ScoutsOptions) && ~strcmpi(ScoutsOptions.showSelection, 'none')
        panel_scout('SetScoutShowSelection', 'none');
    end
    % Use short file name
    if ~isempty(FileName)
        FileName = file_short(FileName);
    end
                
    % Show figures
    try
        switch (SnapType)
            case 'data'
                % Get arguments
                Modality = varargin{1};
                if (length(varargin) >= 2) && ~isempty(varargin{2})
                    Time = varargin{2};
                else
                    Time = [];
                end
                % RowName or all rows
                if (length(varargin) >= 3) && ~isempty(varargin{3})
                    RowName = varargin{3};
                else
                    RowName = [];
                end
                % Display recordings
                switch (file_gettype(FileName))
                    case {'data', 'pdata'}
                        hFig = view_timeseries(FileName, Modality, RowName);
                    case {'matrix', 'pmatrix'}
                        hFig = view_matrix(FileName, 'timeseries');
                end
                % Set current time
                if (length(Time) == 1)
                    panel_time('SetCurrentTime', Time);
                end
                
            case 'topo'
                % Get arguments
                Modality = varargin{1};
                if (length(varargin) >= 2) && ~isempty(varargin{2})
                    Time = varargin{2};
                else
                    Time = [];
                end
                if (length(varargin) >= 3) && ~isempty(varargin{3}) && ~isequal(varargin{3}, 0)
                    Freq = varargin{3};
                else
                    Freq = [];
                end
                % Display topography
                hFig = view_topography(FileName, Modality, '2DSensorCap');
                % Set frequency
                if ~isempty(Freq)
                    panel_freq('SetCurrentFreq', Freq, 0);
                end
                % Contact sheet
                if (length(Time) == 3)
                    % Set figure position / size
                    hFigSrc = hFig;
                    %set(hFigSrc, 'Position', winPos);
                    set(hFigSrc, 'Position', [200, 200, 200, 220]);
                    winPos = [];
                    % Hide colorbars
                    sColormapMeg = bst_colormaps('GetColormap', 'MEG');
                    if sColormapMeg.DisplayColorbar
                        bst_colormaps('SetDisplayColorbar', 'MEG', 0);
                    end
                    sColormapEeg = bst_colormaps('GetColormap', 'EEG');
                    if sColormapEeg.DisplayColorbar
                        bst_colormaps('SetDisplayColorbar', 'EEG', 0);
                    end
                    % Calculate/display contact sheet
                    hFig = view_contactsheet(hFigSrc, 'time', 'fig', [], Time(3), Time(1:2));
                    % Restore colorbars
                    if sColormapMeg.DisplayColorbar
                        bst_colormaps('SetDisplayColorbar', 'MEG', 1);
                    end
                    if sColormapEeg.DisplayColorbar
                        bst_colormaps('SetDisplayColorbar', 'EEG', 1);
                    end
                    % Close source figure
                    close(hFigSrc);
                % Set current time
                elseif (length(Time) == 1)
                    panel_time('SetCurrentTime', Time);
                end

            case 'noisecov'
                % Get the channel file
                [sStudy, iStudy] = bst_get('AnyFile', FileName);
                [sChannel, iStudyChan] = bst_get('ChannelForStudy', iStudy);
                sChanStudy = bst_get('Study', iStudyChan);
                % Load file
                if isempty(sChanStudy) || (length(sChanStudy.NoiseCov) < 1) || isempty(sChanStudy.NoiseCov(1).FileName)
                    error('Noise covariance file does not exist.');
                end
                NoiseCovMat = load(file_fullpath(sChanStudy.NoiseCov(1).FileName));
                % Display as image
                hFig = view_image(NoiseCovMat.NoiseCov, 'jet');
    
            case 'ndatacov'
                % Get the channel file
                [sStudy, iStudy] = bst_get('AnyFile', FileName);
                [sChannel, iStudyChan] = bst_get('ChannelForStudy', iStudy);
                sChanStudy = bst_get('Study', iStudyChan);
                % Load file
                if isempty(sChanStudy) || (length(sChanStudy.NoiseCov) < 2) || isempty(sChanStudy.NoiseCov(2).FileName)
                    error('Data covariance file does not exist.');
                end
                NoiseCovMat = load(file_fullpath(sChanStudy.NoiseCov(2).FileName));
                % Display as image
                hFig = view_image(NoiseCovMat.NoiseCov, 'jet');
                
            case 'headmodel'
                % Get the channel file
                [sStudy, iStudy] = bst_get('AnyFile', FileName);
                [sChannel, iStudyChan] = bst_get('ChannelForStudy', iStudy);
                sChanStudy = bst_get('Study', iStudyChan);
                % Get subject 
                sSubject = bst_get('Subject', sStudy.BrainStormSubject);
                % View spheres
                hFig = view_spheres(sChanStudy.HeadModel(sChanStudy.iHeadModel).FileName, sChanStudy.Channel.FileName, sSubject);
                
            case 'registration'
                % Get arguments
                Modality = varargin{1};
                if (length(varargin) >= 2) && ~isempty(varargin{2})
                    Orient = varargin{2};
                else
                    Orient = 'left';
                end
                % Get the channel file
                [sStudy, iStudy] = bst_get('AnyFile', FileName);
                sChannel = bst_get('ChannelForStudy', iStudy);
                % Display registration
                hFig = channel_align_manual(sChannel.FileName, Modality, 0);
                % Set orientation
                if ~isempty(hFig)
                    figure_3d('SetStandardView', hFig, Orient);
                end
    
            case 'sources'
                % Get arguments
                if (length(varargin) >= 1) && ~isempty(varargin{1})
                    Time = varargin{1};
                else
                    Time = [];
                end
                if (length(varargin) >= 2) && ~isempty(varargin{2})
                    DataThreshold = varargin{2};
                else
                    DataThreshold = 0.3;
                end
                if (length(varargin) >= 3) && ~isempty(varargin{3})
                    Orient = varargin{3};
                else
                    Orient = 'left';
                end
                if (length(varargin) >= 4) && ~isempty(varargin{4})
                    SurfSmooth = varargin{4};
                else
                    SurfSmooth = 0.3;
                end
                if (length(varargin) >= 5) && ~isempty(varargin{5}) && ~isequal(varargin{5}, 0)
                    Freq = varargin{5};
                else
                    Freq = [];
                end
                % Check data type
                if ~ismember(file_gettype(FileName), {'link','results','presults','timefreq','ptimefreq'})
                    error('File must contain source information.');
                end
                % Call surface viewer
                hFig = view_surface_data([], FileName, [], 'NewFigure');
                % Set surface threshold
                iSurf = 1;
                panel_surface('SetDataThreshold', hFig, iSurf, DataThreshold);
                panel_surface('SetSurfaceSmooth', hFig, iSurf, SurfSmooth, 0);
                % Set orientation
                figure_3d('SetStandardView', hFig, Orient);
                % Set frequency
                if ~isempty(Freq)
                    panel_freq('SetCurrentFreq', Freq, 0);
                end

                % Contact sheet
                if (length(Time) == 3)
                    % Set figure position / size
                    hFigSrc = hFig;
                    % set(hFigSrc, 'Position', winPos);
                    set(hFigSrc, 'Position', [200, 200, 200, 220]);
                    winPos = [];
                    % Hide colorbar
                    sColormapSrc = bst_colormaps('GetColormap', 'source');
                    if sColormapSrc.DisplayColorbar
                        bst_colormaps('SetDisplayColorbar', 'source', 0);
                    end
                    sColormapStat = bst_colormaps('GetColormap', 'stat2');
                    if sColormapStat.DisplayColorbar
                        bst_colormaps('SetDisplayColorbar', 'stat2', 0);
                    end
                    % Calculate/diaply contact sheet
                    hFig = view_contactsheet(hFigSrc, 'time', 'fig', [], Time(3), Time(1:2));
                    % Restore colorbar
                    if sColormapSrc.DisplayColorbar
                        bst_colormaps('SetDisplayColorbar', 'source', 1);
                    end
                    if sColormapStat.DisplayColorbar
                        bst_colormaps('SetDisplayColorbar', 'stat2', 1);
                    end
                    % Close source figure
                    close(hFigSrc);
                % Set current time
                elseif (length(Time) == 1)
                    panel_time('SetCurrentTime', Time);
                end
                
            case 'mriviewer'
                % Get arguments
                if (length(varargin) >= 1) && ~isempty(varargin{1})
                    Time = varargin{1};
                else
                    Time = [];
                end
                if (length(varargin) >= 2) && ~isempty(varargin{2})
                    DataThreshold = varargin{2};
                else
                    DataThreshold = 0.3;
                end
                if (length(varargin) >= 3) && ~isempty(varargin{3}) && ~isequal(varargin{3}, 0)
                    Freq = varargin{3};
                else
                    Freq = [];
                end
                if (length(varargin) >= 4) && ~isempty(varargin{4}) && ~isequal(varargin{4}, 0)
                    XYZmni = varargin{4};
                else
                    XYZmni = [];
                end
                % Check data type
                if ~ismember(file_gettype(FileName), {'link','results','presults','timefreq','ptimefreq'})
                    error('File must contain source information.');
                end
                % Call surface viewer
                hFig = view_mri([], FileName, [], 1);
                % Set surface threshold
                iSurf = 1;
                panel_surface('SetDataThreshold', hFig, iSurf, DataThreshold);
                % Set frequency
                if ~isempty(Freq)
                    panel_freq('SetCurrentFreq', Freq, 0);
                end
                % Set current time
                if (length(Time) == 1)
                    panel_time('SetCurrentTime', Time);
                end
                % Set coordinates
                if ~isempty(XYZmni)
                    figure_mri('SetLocation', 'mni', hFig, [], XYZmni ./ 1000);
                end
                % Larger default figure size
                winPos = [100 100 450 500];

            case 'ssp'
                % Load the channel file
                [sStudy, iStudy] = bst_get('AnyFile', FileName);
                sChannel = bst_get('ChannelForStudy', iStudy);
                ChannelMat = in_bst_channel(sChannel.FileName);
                % Load sFile descriptor
                DataMat = in_bst_data(FileName, 'ChannelFlag');
                % Loop through all the projectors
                hFig = [];
                for iProj = 1:length(ChannelMat.Projector)
                    sCat = ChannelMat.Projector(iProj);
                    % Type of components
                    isICA = isequal(sCat.SingVal, 'ICA');
                    % Keep only the selected components
                    if ~isempty(sCat.CompMask)
                        iSelComp = find(sCat.CompMask);
                    else
                        %iSelComp = 1;
                        continue;
                    end
                    % If projector not selected or no components: skip
                    if ~sCat.Status || isempty(iSelComp)
                        continue;
                    end
                    % Get all the modalities altered by this component
                    iAllChan = find(sCat.Components(:,1));
                    AllMod = intersect(unique({ChannelMat.Channel(iAllChan).Type}), {'EEG','MEG','MEG GRAD','MEG MAG','ECOG','SEEG','NIRS'});
                    if all(ismember({'MEG GRAD','MEG MAG'}, AllMod))
                        AllMod = union({'MEG'}, setdiff(AllMod, {'MEG GRAD','MEG MAG'}));
                    end
                    % Loop on the modalities
                    for iMod = 1:length(AllMod)
                        % Get all the channels for this modality
                        iChannels = good_channel(ChannelMat.Channel, DataMat.ChannelFlag, AllMod{iMod});
                        % ICA: Get the topography to display
                        if isICA
                            % Field Components stores the mixing matrix W
                            W = sCat.Components(iChannels,:)';
                            Topo = pinv(W);
                        % SSP: Limit the maximum number of components to display
                        else
                            % Field Components stores the spatial components U
                            U = sCat.Components(iChannels, :);
                            Topo = U;
                        end
                        % Loop on the components
                        for iComp = 1:length(iSelComp)
                            % Plot component topography for current modality
                            hFig(end+1) = view_topography(FileName, AllMod{iMod}, '2DSensorCap', Topo(:,iSelComp(iComp)), [], 'NewFigure');
                            imgLegend{end+1} = [AllMod{iMod} ' #' num2str(iSelComp(iComp)) ' ' sCat.Comment];
                        end
                    end
                end
                
            case 'spectrum'
                % RowName or all rows
                if (length(varargin) >= 1) && ~isempty(varargin{1})
                    RowName = varargin{1};
                else
                    RowName = [];
                end
                % Get arguments
                if (length(varargin) >= 2) && ~isempty(varargin{2}) && ~isequal(varargin{2}, 0)
                    Freq = varargin{2};
                else
                    Freq = [];
                end
                % Display frequency spectrum
                hFig = view_spectrum(FileName, 'Spectrum', RowName);
                % Set frequency
                if ~isempty(Freq)
                    panel_freq('SetCurrentFreq', Freq, 0);
                end

            case 'timefreq'
                % RowName or all rows
                if (length(varargin) >= 1) && ~isempty(varargin{1})
                    RowName = varargin{1};
                else
                    RowName = [];
                end
                % Time and frequency
                if (length(varargin) >= 2) && ~isempty(varargin{2})
                    Time = varargin{2};
                else
                    Time = [];
                end
                if (length(varargin) >= 3) && ~isempty(varargin{3}) && ~isequal(varargin{3}, 0)
                    Freq = varargin{3};
                else
                    Freq = [];
                end
                % Display all TF maps
                if isempty(RowName)
                    hFig = view_timefreq(FileName, 'AllSensors');
                else
                    hFig = view_timefreq(FileName, 'SingleSensor', RowName);
                end
                % Set time
                if (length(Time) == 1)
                    panel_time('SetCurrentTime', Time);
                end
                % Set frequency
                if ~isempty(Freq)
                    panel_freq('SetCurrentFreq', Freq, 0);
                end

            case 'connectimage'
                % Time and frequency
                if (length(varargin) >= 1) && ~isempty(varargin{1})
                    Time = varargin{1};
                else
                    Time = [];
                end
                if (length(varargin) >= 2) && ~isempty(varargin{2}) && ~isequal(varargin{2}, 0)
                    Freq = varargin{2};
                else
                    Freq = [];
                end
                % Display connectivity matrix as an image
                hFig = view_connect(FileName, 'Image');
                % Set time
                if (length(Time) == 1)
                    panel_time('SetCurrentTime', Time);
                end
                % Set frequency
                if ~isempty(Freq)
                    panel_freq('SetCurrentFreq', Freq, 0);
                end

            case 'connectgraph'
                % Threshold
                if (length(varargin) >= 1) && ~isempty(varargin{1})
                    Threshold = varargin{1};
                else
                    Threshold = [];
                end
                % Time and frequency
                if (length(varargin) >= 2) && ~isempty(varargin{2})
                    Time = varargin{2};
                else
                    Time = [];
                end
                if (length(varargin) >= 3) && ~isempty(varargin{3}) && ~isequal(varargin{3}, 0)
                    Freq = varargin{3};
                else
                    Freq = [];
                end
                % Display connectivity matrix as an image
                hFig = view_connect(FileName, 'GraphFull');
                % Update amplitude threshold
                sOptions = panel_display('GetDisplayOptions');
                sOptions.DataThreshold = Threshold / 100;
                panel_display('SetThresholdOptions', sOptions);
                % Set time
                if (length(Time) == 1)
                    panel_time('SetCurrentTime', Time);
                end
                % Set frequency
                if ~isempty(Freq)
                    panel_freq('SetCurrentFreq', Freq, 0);
                end

            case 'dipoles'
                % Goodness
                if (length(varargin) >= 1) && ~isempty(varargin{1})
                    Goodness = varargin{1};
                else
                    Goodness = [];
                end
                % Orientation
                if (length(varargin) >= 2) && ~isempty(varargin{2})
                    Orient = varargin{2};
                else
                    Orient = [];
                end
                % Display dipoles
                hFig = view_dipoles(FileName, 'mri3d');
                % Set goodness of fit slider
                if ~isempty(Goodness)
                    panel_dipoles('SetGoodness', Goodness);
                end
                % Set orientation
                if ~isempty(Orient)
                    figure_3d('SetStandardView', hFig, Orient);
                end

            case 'jframe'
                for i = 1 : length(jFrame)
                    % Get as image from jFrame
                    jFrame(i).setVisible(1);
                    jFrame(i).setExtendedState(0);  % Restore if minimized (0 = NORMAL)
                    jFrame(i).toFront();
                    jFrame(i).setAlwaysOnTop(true);
                    pause(0.2);
                    % Java coordinates: [left top width height]
                    javaPosition = [jFrame(i).getLocation().getX(), jFrame(i).getLocation().getY(), ...
                                    jFrame(i).getSize().getWidth(), jFrame(i).getSize().getHeight()];
                    % Matlab coordinates [left bottom width height]
                    matlabPosition = javaPosition;
                    screenSize = get(0,'ScreenSize'); % screen_height = screenSize(4)
                    matlabPosition(2) = screenSize(4) - (javaPosition(2) + javaPosition(4));
                    [imgs{i}, ~] = getscreen(matlabPosition);
                end
                hFig = [];

            case 'figure'
                % Nothing to do

            otherwise
                hFig = [];
        end
    catch
        % Output error message to console
        strErr = bst_error();
        disp(['BST_REPORT> ERROR:' 10 strErr]);
        % Log error message
        Error('process_snapshot', ['"' SnapType '" "' FileName '"'], strErr);
        hFig = [];
    end
    % Get images from figures
    for i = 1:length(hFig)
        drawnow;
        % Set figure size
        if ~isempty(winPos)
            set(hFig(i), 'Position', winPos);
            pause(0.2);
        end
        % Redraw figure
        figure(hFig(i));
        drawnow;
        % Capture window contents
        if ~isempty(imgLegend)
            img = out_figure_image(hFig(i), [], imgLegend{i});
        else
            img = out_figure_image(hFig(i));
        end
        % Append images
        imgs{i} = img;
        % Close figure
        if ~strcmpi(SnapType, 'figure')
            close(hFig(i));
        end
    end
    % Add images to report
    for i = 1:length(imgs)
        Add('image', Comment, FileName, imgs{i});
    end
    % Restore scouts
    if ~isempty(ScoutsOptions) && ~strcmpi(ScoutsOptions.showSelection, 'none')
        panel_scout('SetScoutShowSelection', ScoutsOptions.showSelection);
    end
    % Restore Brainstorm window
    if bst_get('isGUI')
        jBstFrame.setVisible(1);
    end
    % Restore progress bar
    if isProgress
        bst_progress('show');
    end
    % Restore initial layout manager
    if ~isempty(curLayout)
        bst_set('Layout', 'WindowManager', curLayout);
    end
    % Output: cell array with image(s)
    img = imgs;
end


%% ===== SAVE REPORT =====
% USAGE:  ReportFile = Save(sInputs=[],   ReportFile=[])
%         ReportFile = Save(FileNames=[], ReportFile=[])
function ReportFile = Save(sInputs, ReportFile)
    global GlobalData;
    % Empty report
    if isempty(GlobalData.ProcessReports.Reports)
        ReportFile = [];
    end
    % No inputs 
    if (nargin < 1) || isempty(sInputs)
        sInputs = [];
    end
    % Generate filename if not provided
    if (nargin < 2) || isempty(ReportFile)
        % Check if there us something to save
        if isempty(GlobalData.ProcessReports.Reports)
            ReportFile = [];
            return;
        end
        % Reports folder
        reportsDir = bst_get('UserReportsDir');
        % Date and time
        c = clock;
        strTime = sprintf('_%02.0f%02.0f%02.0f_%02.0f%02.0f', c(1)-2000, c(2:5));
        % Get entry of start
        iStart = find(strcmpi(GlobalData.ProcessReports.Reports(:,1), 'start'), 1);
        if ~isempty(iStart)
            ProtocolName = file_standardize(GlobalData.ProcessReports.Reports{iStart,4}.Comment);
            strProtocol = ['_' ProtocolName];
        else
            strProtocol = '';
        end
        % Full file name
        ReportFile = file_unique(bst_fullfile(reportsDir, ['report' strProtocol strTime '.mat']));
    end
    % Add stop entry
    Add('stop', [], sInputs, []);
    % Save 
    ReportMat.Reports = GlobalData.ProcessReports.Reports;
    bst_save(ReportFile, ReportMat, 'v7');
    % Reset 
    Reset();
end


%% ===== OPEN =====
% USAGE:  jReport = bst_report('Open', ReportFile, isFullReport=1)
%         jReport = bst_report('Open', ReportFile)
%         jReport = bst_report('Open', 'last')
%         jReport = bst_report('Open', 'current')
%         jReport = bst_report('Open', 'loaded')
%         jReport = bst_report('Open', 'previous')
%         jReport = bst_report('Open', 'next')
%         jReport = bst_report('Open')
function jReport = Open(ReportFile, isFullReport)
    global GlobalData;
    % Headless mode: Cancel call
    jReport = [];
    if (GlobalData.Program.GuiLevel == -1)
        return;
    end
    % Default: view the file anyway
    if (nargin < 2) || isempty(isFullReport)
        isFullReport = 1;
    end
    % Progress bar
    bst_progress('start', 'Report viewer', 'Opening report...');
    % Ask file to the user
    if (nargin < 1) || isempty(ReportFile)
        ProtocolInfo = bst_get('ProtocolInfo');
        ProtocolName = file_standardize(ProtocolInfo.Comment);
        reportsDir = bst_get('UserReportsDir');
        ReportFile = java_getfile('open', 'Open process report', reportsDir, 'single', 'files', ...
                                  {{['_report_' ProtocolName]}, 'Brainstorm reports (report_*.mat)', 'BST'}, 1);
        if isempty(ReportFile)
            bst_progress('stop');
            return
        end
        ReportMat = load(ReportFile);
        Reports = ReportMat.Reports;
        ReportComment = ReportFile;
    % Open report with a relative instruction
    elseif any(strcmpi(ReportFile, {'last', 'current', 'previous', 'next', 'loaded'}))
        [Reports, ReportFile, ReportComment] = GetReport(ReportFile);
    % Load specified report
    else
        ReportMat = load(ReportFile);
        Reports = ReportMat.Reports;
        ReportComment = ReportFile;
    end
    % If nothing was loaded
    if isempty(Reports)
        bst_progress('stop');
        return
    end
    
    % ===== VIEW DOCUMENT =====
    % Print report to HTML
    html = PrintToHtml(Reports, isFullReport);
    if isempty(html)
        bst_progress('stop');
        return;
    end
    % Open HTML viewer
    jReport = HtmlViewer(html, ['Report: ' ReportComment]);
    % Save current file
    GlobalData.ProcessReports.CurrentFile = ReportFile;
    % Close progress bar
    bst_progress('stop');
end


%% ===== PRINT TO HTML =====
function html = PrintToHtml(Reports, isFullReport, isOnlyText)
    html = '';
    % If no inputs: nothing to export
    if (nargin < 3) || isempty(isOnlyText)
        isOnlyText = 0;
    end
    if (nargin < 2) || isempty(isFullReport)
        isFullReport = 1;
    end
    if (nargin < 1) || isempty(Reports)
        return;
    end
    
    % ===== LIST EVENTS =====
    iVersion  = find(strcmpi(Reports(:,1), 'version'), 1);
    iStart    = find(strcmpi(Reports(:,1), 'start'), 1);
    iStop     = find(strcmpi(Reports(:,1), 'stop'), 1);
    iProcess  = find(strcmpi(Reports(:,1), 'process'));
    iErrors   = find(strcmpi(Reports(:,1), 'error'));
    iWarnings = find(strcmpi(Reports(:,1), 'warning'));
    iImages   = find(strcmpi(Reports(:,1), 'image'));
    iMessages = setdiff(1:size(Reports,1), [iVersion(:); iStart(:); iStop(:); iImages(:)]');
    
    % Start tag
    FilesInit = {};
    if ~isempty(iStart)
        ProtocolInfo = Reports{iStart,4};
        FilesInit = GetFilesList(Reports{iStart,3}, 1, 1);
    else
        ProtocolInfo = [];
    end
    % Stop tag
    FilesFinal = {};
    if ~isempty(iStop)
        FilesFinal = GetFilesList(Reports{iStop,3}, 1, 1);
    end
    % If only the listing of the errors is required
    if ~isFullReport
        % If no errors, no warnings: no output
        if isempty(iErrors) && isempty(iWarnings) && isempty(iImages)
            return;
        % One error: show an error message instead
        elseif (length(iErrors) == 1) && isempty(iWarnings)
            % Get error description
            if isstruct(Reports{iErrors(1),2}) && ~isempty(Reports{iErrors(1),2})
                ProcName    = func2str(Reports{iErrors(1),2}.Function);
                subGroups   = Reports{iErrors(1),2}.SubGroup;
                if iscellstr(subGroups)
                    groupComment = subGroups{1};
                    for iGroup = 2:length(subGroups)
                        groupComment = [groupComment ' > ' subGroups{iGroup}];
                    end
                else
                    groupComment = subGroups;
                end
                ProcComment = [groupComment, ' > ', Reports{iErrors(1),2}.Comment];
            else
                ProcName    = 'Unknown';
                ProcComment = 'Unknown';
            end
            FileNames  = GetFilesList(Reports{iErrors(1),3}, 1);
            strMsg     = Reports{iErrors(1),4};
            % Display error dialog
            errMsg = ['[' ProcName ']  ', ProcComment 10 10];
            errMsg = [errMsg strMsg 10];
            if (length(FileNames) == 1) && ~isempty(FileNames{1})
                errMsg = [errMsg 10 'File: ' FileNames{1} 10];
            end
            bst_error(errMsg, 'Error report', 0);
            return;
        end
    end

    % Report version: HTML with images
    if ~isOnlyText
        % ===== HEADER ====
        % Get interface scaling
        f = bst_get('InterfaceScaling') / 100;
        % HTML header
        html = ['<HTML>' 10 ...
                '<STYLE type="text/css">' 10 ...
                'h2 {font-size: ' num2str(11 * f) 'px; font-weight: bold; padding-top: 12px; padding-bottom: 12px;}' 10 ...
                'td {padding: 2px 2px 2px 2px; }' 10 ...
                '.bord td {border-width: 1px; border-style: solid; border-color: #bbbbbb; background-color: #f2f2f2; font: normal ' num2str(8 * f) 'px Verdana, Arial, Helvetica, sans-serif; }' 10 ...
                '.link {text-decoration: none; color: #0000a0;}' 10 ...
                '</STYLE>' 10 10 ...
                '<BODY style="margin: 5px 10px 10px 10px; font: normal ' num2str(8 * f) 'px Verdana, Arial, Helvetica, sans-serif; background-color: #e8e8e8;">' 10 ...
                '<TITLE>Brainstorm process report</TITLE>' 10];
        % Brainstorm version
        if ~isempty(iVersion)
            html = [html '<B>Brainstorm version:</B> ' Reports{iVersion,4} '<BR><BR>'];
        end
        % Elapsed time
        if ~isempty(iStart) && ~isempty(iStop)
            % Get elapsed time string 'Xd Xh Xm Xs'
            strElapsed = GetElapsedStr(Reports, iStart, iStop);
            % Time line
            strElapsed = [strElapsed '</TD></TR>' 10];
            html = [html 'Start: ' Reports{iStart,5} ' &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Elapsed: ' strElapsed];
        end
    
        % ===== ERRORS =====
        %if ~isempty(iMessages)
            % Start errors table
            html = [html sprintf('<H2>%d errors and %d warnings</H2>', length(iErrors), length(iWarnings)) 10];
            html = [html '<TABLE width="100%" class="bord">' 10];
            % Loop over each error
            prevProcName = [];
            for i = 1:length(iMessages)
                iMsg = iMessages(i);
                % Get entry values
                strType  = Reports{iMsg,1};
                if ~isempty(Reports{iMsg,2})
                    ProcName = func2str(Reports{iMsg,2}.Function);
                else
                    ProcName = 'Unknown process';
                end
                % Skip process_snapshot (already displayed at the end)
                if strcmpi(strType, 'process') && strcmpi(ProcName, 'process_snapshot')
                    continue;
                end
                % Get messages to display
                FileNames = GetFilesList(Reports{iMsg,3}, 1);
                strMsg    = Reports{iMsg,4};
                strTime   = Reports{iMsg,5};
                % Process name (only if different as previous)
                if ~strcmpi(ProcName, prevProcName) || strcmpi(strType, 'process')
                    html = [html '<TR><TD colspan="4" style="background-color: #D0D0D0;">' ProcName '</TD></TR>'];
                    prevProcName = ProcName;
                end
                % Type of message
                switch lower(strType)
                    case 'warning',  txtColor = '#FFA500';
                    case 'error',    txtColor = '#FF0000';
                    case 'process',  html = [html 10]; continue;
                    otherwise,       txtColor = '#000000';
                end
                html = [html '<TR><TD><FONT color="' txtColor '">&nbsp;&nbsp;&nbsp;&nbsp;' strType '</FONT></TD>'];
                % File name
                if isempty(FileNames) || isempty(FileNames{1})
                    strFile = '<FONT color="#555555">[No input]</FONT>';
                elseif (length(FileNames) == 1)
                    strFile = PrintFilesList(FileNames, ProtocolInfo, 1);
                else
                    strFile = [num2str(length(FileNames)) ' files [...]'];
                end
                html = [html '<TD>' strFile '</TD>'];
                % Error message
                html = [html '<TD>' strrep(char(strMsg),char(10),'<BR>') '</TD>'];
                % Time
                html = [html '<TD>' strTime '</TD>'];
                % End line
                html = [html '</TR>' 10];
            end
        %end    
        % Close errors table
        html = [html '</TABLE>' 10 10];
        
        % ===== IMAGES =====
        if ~isempty(iImages)
            html = [html '<H2>Snapshots</H2>' 10];
            % Create Base64 encoder
            encoder = sun.misc.BASE64Encoder();
            % Loop on all the images
            for i = 1:length(iImages)
                iMsg      = iImages(i);
                Comment   = Reports{iMsg,2};
                FileNames = GetFilesList(Reports{iMsg,3}, 1);
                imgRgb    = Reports{iMsg,4};
                % Convert RGB in 255 to Java color integer
                sz = size( imgRgb );
                imgRgb = double(imgRgb) / 255;
                imgRgb = transpose( reshape( permute( imgRgb, [3 2 1] ), [sz(3) sz(1)*sz(2)] ) );
                imgInt = typecast(bitshift( uint32( 255 ), 24 ), 'int32') + ...
                         typecast(bitshift( uint32( 255*imgRgb(:,1) ), 16 ), 'int32') + ...
                         typecast(bitshift( uint32( 255*imgRgb(:,2) ), 8 ), 'int32') + ...
                         typecast(bitshift( uint32( 255*imgRgb(:,3) ), 0 ), 'int32');
                % Create an image (BufferedImage)
                jImage = java.awt.image.BufferedImage(sz(2), sz(1), java.awt.image.BufferedImage.TYPE_INT_ARGB);
                jImage.setRGB(0, 0, sz(2), sz(1), imgInt(:), 0, sz(2));
                % Convert image to PNG
                jByteStream = java.io.ByteArrayOutputStream();
                javax.imageio.ImageIO.write(jImage, 'png', jByteStream);
                % Encode PNG image in Base64
                jStringImage = encoder.encode(jByteStream.toByteArray());
                % Display image in HTML
                html = [html, Comment];
                if ~isempty(FileNames)
                    html = [html, '  --  ' 10, PrintFilesList(FileNames, ProtocolInfo, 1)];
                else
                    html = [html, '<BR>'];
                end
                html = [html, '<IMG src="data:image/png;base64,' char(jStringImage) '" /><BR><BR>'];
            end
        end
        
        % ===== ALL FILES =====
        % Initial files
        html = [html '<H2>Initial files</H2>' 10];
        if ~isempty(FilesInit) && ~isequal(FilesInit, {[]})
            html = [html PrintFilesList(FilesInit, ProtocolInfo, 0)];
        else
            html = [html '<FONT color="#555555">[No input]</FONT><BR>'];
        end
        % Final files
        if ~isempty(FilesFinal) && ~isequal(FilesFinal, FilesInit)
            html = [html '<H2>Final files</H2>' 10];
            if ~isempty(FilesFinal) && ~isequal(FilesFinal, {[]})
                html = [html PrintFilesList(FilesFinal, ProtocolInfo, 0)];
            else
                html = [html '<FONT color="#555555">[No output files]</FONT><BR>'];
            end
        end
        % HTML footer
        html = [html '</BODY></HTML>'];

    % Report version: Only text
    else
        % Brainstorm version
        if ~isempty(iVersion)
            html = [html 'Brainstorm version: ' Reports{iVersion,4} 10 10];
        end        
        % Elapsed time
        if ~isempty(iStart) && ~isempty(iStop)
            % Get elapsed time string 'Xd Xh Xm Xs'
            strElapsed = GetElapsedStr(Reports, iStart, iStop);
            html = [html 'Start: ' Reports{iStart,5} '        Elapsed: ' strElapsed 10 10];
        end
        % Errors and warnings
        html = [html sprintf('%d errors and %d warnings', length(iErrors), length(iWarnings)) 10 10];
        % Entries
        for iEntry = 1:size(Reports,1)
            if ~isempty(Reports{iEntry,1}) && ~isempty(Reports{iEntry,5})
                html = [html, Reports{iEntry,5}, ' : ', Reports{iEntry,1}, repmat(' ', 1, 7-length(Reports{iEntry,1}))];
                if ~isempty(Reports{iEntry,2})
                    if isstruct(Reports{iEntry,2})
                        html = [html, 9, ' - ' func2str(Reports{iEntry,2}.Function)];
                    elseif ischar(Reports{iEntry,2})
                        html = [html, 9, ' - ' Reports{iEntry,2}];
                    end
                end
                html = [html, 10];
            end
        end
    end
end


%% ===== GET FILES LIST ====
function FileNames = GetFilesList(entry, isOneList, isStandard)
    % Parse inputs 
    if (nargin < 3) || isempty(isStandard)
        isStandard = 0;
    end
    % Transform input structures to a filename list
    if isempty(entry)
        FileNames = [];
    % Entry = 'filename.mat'
    elseif ischar(entry)
        FileNames = {entry};
    % Entry = {'filename1.mat', 'filename2.mat'}
    elseif iscell(entry) && ischar(entry{1})
        FileNames = entry;
    % Entry = {sFiles1, sFiles2}
    elseif iscell(entry) && isstruct(entry{1})
        if isOneList
            try
                FileNames = [entry{:}];
            catch
                FileNames = [];
            end
            FileNames = {entry.FileName};
        else
            FileNames = {{entry{1}.FileName}, {entry{2}.FileName}};
        end
    % Entry = {{'filenameA1.mat', 'filenameA2.mat'}, {'filenameB1.mat', 'filenameB2.mat'}}
    elseif iscell(entry) && iscell(entry{1})
        if isOneList
            try
                FileNames = [entry{:}];
            catch
                FileNames = [];
            end
        else
            FileNames = entry;            
        end
    % Entry = sFiles
    elseif isstruct(entry)
        FileNames = {entry.FileName};
    else
        FileNames = [];
    end
    % Standardize file names
    if isStandard && ~isempty(FileNames)
        FileNames = strrep(FileNames, '\', '/');
    end
end


%% ===== LIST FILES =====
function str = PrintFilesList(FileNames, ProtocolInfo, isWrap)
    str = [];
    NMAX = 10;
    for iFile = 1:min(length(FileNames), NMAX)
        % No protocol information
        if isempty(ProtocolInfo)
            isDeleted = 0;
        % File is a link
        elseif ~isempty(strfind(FileNames{iFile}, 'link|'))
            [ResultFile, DataFile] = file_resolve_link(FileNames{iFile});
            isDeleted =  ~file_exist(ResultFile) || ~file_exist(DataFile);
        % Any other type of file
        else
            isDeleted = ~file_exist(bst_fullfile(ProtocolInfo.STUDIES, FileNames{iFile}));
        end       
        % Trim filenames that are too long
        strFile = FileNames{iFile};
        if isWrap && (length(strFile) > 45)
            strFile = [strFile(1:45), '...'];
        end
        % Display differently the deleted files
        if isDeleted
            str = [str '<FONT color="#777777">' strFile ' &nbsp;&nbsp;&nbsp;[deleted]</FONT><BR>' 10];
        else
            str = [str '<A class="link" href="file://' FileNames{iFile} '">' strFile '</A><BR>' 10];
        end
    end
    % Long lists: just prints the number of files
    if (length(FileNames) > NMAX)
        str = [str, sprintf('<FONT color="#777777">[%d more files...]</FONT><BR>', length(FileNames)-NMAX)];
    end
end


%% ===== HTML VIEWER =====
function jFrame = HtmlViewer(strHtml, figTitle)
    global GlobalData;
    % Initializations
    import java.awt.*;
    import javax.swing.*;
    import org.brainstorm.icon.*;
    % Create a new window
    isNewWindow = isempty(GlobalData.ProcessReports.jFrameReport);
    if isNewWindow
        % Create frame
        jFrame = java_create('javax.swing.JFrame');
        jFrame.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
        jFrame.setPreferredSize(Dimension(900, 700));
        % Get main panel
        jPanelMain = jFrame.getContentPane();
        % Toolbar
        jToolbar = gui_component('Toolbar', jPanelMain);
            gui_component('ToolbarButton', jToolbar,[],[], IconLoader.ICON_ARROW_LEFT,  'Previous report', @(h,ev)Open('previous'));
            gui_component('ToolbarButton', jToolbar,[],[], IconLoader.ICON_ARROW_RIGHT, 'Next report',     @(h,ev)Open('next'));
            jToolbar.addSeparator();
            gui_component('ToolbarButton', jToolbar,[],[], IconLoader.ICON_FOLDER_OPEN, 'Load report file',  @(h,ev)Open());
            gui_component('ToolbarButton', jToolbar,[],[], IconLoader.ICON_SAVE, 'Export report',  @(h,ev)Export());
            jToolbar.addSeparator();
            gui_component('ToolbarButton', jToolbar,[],[], IconLoader.ICON_PROCESS, 'Reload pipeline', @(h,ev)Recall('loaded'));
            jToolbar.addSeparator();
            gui_component('ToolbarButton', jToolbar, [], 'Clear history', [], 'Delete all the saved reports for this user', @(h,ev)ClearHistory(), []);
        jPanelMain.add(jToolbar, BorderLayout.NORTH);
        
        % Create HTML viewer component
        jTextHtml = JEditorPane();
        jTextHtml.setEditable(false);
        % Set callback for clicks on hyperlinks
        java_setcb(jTextHtml, 'HyperlinkUpdateCallback', @LinkClickCallback);
        % Add an html editor kit
        %kit = javax.swing.text.html.HTMLEditorKit();
        kit = org.brainstorm.html.BstHTMLEditorKit();
        jTextHtml.setEditorKit(kit);
        % Set content
        doc = kit.createDefaultDocument();
        jTextHtml.setDocument(doc);
        % Enclose in scroll panel
        jScrollText = JScrollPane(jTextHtml);
        jPanelMain.add(jScrollText, BorderLayout.CENTER);
        % Set location and size
        jFrame.pack();
        % Save some objects
        GlobalData.ProcessReports.jFrameReport = jFrame;
        GlobalData.ProcessReports.jTextHtml    = jTextHtml;
    else
        jFrame    = GlobalData.ProcessReports.jFrameReport;
        jTextHtml = GlobalData.ProcessReports.jTextHtml;
    end
    % Set title
    jFrame.setTitle(figTitle);
    % Set text 
    jTextHtml.setText(strHtml);
    % Display figure
    jFrame.setVisible(1);
end

%% ===== HYPERLINK CLICK =====
function LinkClickCallback(h, ev)
    % If user clicked link
    if (ev.getEventType() == ev.getEventType().ACTIVATED)
        % Get file name
        FileName = strrep(char(ev.getURL()), 'file://', '');
        % Force the exploration mode of the database to be "functional / subject"
        panel_protocols('SetExplorationMode', 'StudiesSubj');
        drawnow;
        % Find corresponding node in the tree
        bstNode = panel_protocols('GetNode', [], FileName);
        if isempty(bstNode)
            return;
        end
        % Select node in the tree
        panel_protocols('ExpandPath', bstNode, 1 );
        % Double click on the node
        bst_call(@tree_callbacks, bstNode, 'doubleclick');
    end
end


%% ===== CLOSE VIEWER =====
function Close() %#ok<*DEFNU>
    global GlobalData;
    % If the figure is available: hide it
    if ~isempty(GlobalData.ProcessReports.jFrameReport) && GlobalData.ProcessReports.jFrameReport.isVisible()
        GlobalData.ProcessReports.jFrameReport.setVisible(0);
    end
end

%% ===== GET REPORT FILE =====
function [Reports, ReportFile, ReportComment] = GetReport(action)
    global GlobalData;
    % Get all the available reports
    ProtocolInfo = bst_get('ProtocolInfo');
    ProtocolName = file_standardize(ProtocolInfo.Comment);
    reportsDir = bst_get('UserReportsDir');
    allReports = dir(bst_fullfile(reportsDir, ['report_' ProtocolName, '_*.mat']));
    AllFiles   = cellfun(@(c)bst_fullfile(reportsDir, c), {allReports.name}, 'UniformOutput', 0);
    % Get current file
    CurrentFile = GlobalData.ProcessReports.CurrentFile;
    % Default values
    ReportFile = [];
    Reports = [];
    ReportComment = [];
    % No other files
    if isempty(AllFiles) && ~strcmpi(action, 'current')
        return;
    end
    
    % Select report
    switch lower(action)
        case {'last', 'current'}
            % Processed later
        case 'loaded'
            ReportFile = CurrentFile;
        case 'previous'
            if ~isempty(CurrentFile)
                iCur = find(strcmpi(CurrentFile, AllFiles));
                if isempty(iCur)
                    action = 'last';
                elseif (iCur == 1)
                    ReportFile = CurrentFile;
                else
                    ReportFile = AllFiles{iCur - 1};
                end
            end
        case 'next'
            if ~isempty(CurrentFile)
                % If current file is the current report: do not change
                if strcmpi(CurrentFile, 'current')
                    action = 'current';
                end
                % Find current file in saved reports
                iCur = find(strcmpi(CurrentFile, AllFiles));
                % File not found
                if isempty(iCur)
                    % Ignore, nothing to do
                % Last report
                elseif (iCur == length(AllFiles))
                    % If there is a something in the current report, display it, if not, display again the same
                    if ~isempty(GlobalData.ProcessReports.Reports)
                        action = 'current';
                    else
                        ReportFile = CurrentFile;
                    end
                else
                    ReportFile = AllFiles{iCur + 1};
                end
            end
    end 

    % Nothing to display
    if strcmpi(action, 'current') && isempty(GlobalData.ProcessReports.Reports) && isempty(AllFiles)
        return;
    % Current report
    elseif strcmpi(action, 'current') && ~isempty(GlobalData.ProcessReports.Reports)
        % Nothing to display
        Reports = GlobalData.ProcessReports.Reports;
        ReportFile = 'current';
        ReportComment = 'Current report';
    % Else: Saved report
    else
        % Nothing selected: display the last file
        if isempty(ReportFile) || strcmpi(action, 'last')
            ReportFile = AllFiles{end};
        end
        % Load saved report
        ReportMat = load(ReportFile);
        Reports = ReportMat.Reports;
        ReportComment = ReportFile;
    end
end


%% ===== CLEAR HISTORY =====
function ClearHistory(isUserConfirm)
    % Parse inputs
    if (nargin < 1) || isempty(isUserConfirm) || (isUserConfirm ~= 0)
        isUserConfirm = 1;
    end
    if isUserConfirm
        % Ask for confirmation
        isConfirm = java_dialog('confirm', 'Delete all the saved process reports?', 'Clear process history');
        if ~isConfirm
            return;
        end
    end
    % Get all the available reports
    ProtocolInfo = bst_get('ProtocolInfo');
    if isempty(ProtocolInfo)
        return
    end
    ProtocolName = file_standardize(ProtocolInfo.Comment);
    reportsDir = bst_get('UserReportsDir');
    % If directory exists
    if isdir(reportsDir)
        % List contents of folder
        allReports = dir(bst_fullfile(reportsDir, ['report_' ProtocolName, '_*.mat']));
        % Delete contents of directory
        for iFile = 1:length(allReports)
            file_delete(bst_fullfile(reportsDir, allReports(iFile).name), 1);
        end
    end
end


%% ===== RECALL PIPELINE =====
% USAGE:  Recall('current')  : Load the last/current pipeline 
%         Recall('loaded')   : Load the pipeline loaded in the report viewer
function Recall(target)
    % Get target report
    Reports = GetReport(target);
    if isempty(Reports)
        return
    end
    % Get the start and process entries
    iStart   = find(strcmpi(Reports(:,1), 'start'), 1);
    iProcess = find(strcmpi(Reports(:,1), 'process'));
    if isempty(iStart) || isempty(iProcess)
        return;
    end
    % Process bar
    bst_progress('start', 'Process history', 'Loading pipeline...');
    % Get the input files
    FileNames = GetFilesList(Reports{iStart,3}, 0);
    % Get the processes
    sProcesses = [Reports{iProcess,2}];
    
    % === PROCESS 1 ===
    if (sProcesses(1).nInputs == 1)
        % Get current files list
        sCurFiles = panel_nodelist('GetFiles', 'Process1');
        if isequal(sCurFiles, -1)
            bst_progress('stop');
            return;
        end
        % Update files list if modified
        if (isempty(sCurFiles) && ~isempty(FileNames)) || (~isempty(sCurFiles) && isstruct(sCurFiles) && ~isequal({sCurFiles.FileName}, FileNames))
            % Reset files lists
            panel_nodelist('ResetAllLists');
            % Import files: leave file list empty
            if isempty(FileNames) || strcmpi(FileNames{1}, 'import') || isempty(FileNames{1})
                FileNames = {};
            % Check for existing files
            else
                isExist = [];
                for iFile = 1:length(FileNames)
                    sStudy = bst_get('AnyFile', FileNames{iFile});
                    isExist(iFile) = ~isempty(sStudy);
                end
                FileNames(~isExist) = [];
                if isempty(FileNames)
                    bst_error('The input files do not exist anymore.', 'Reload pipeline', 0);
                    return;
                end
                % Set the files in the file list
                panel_nodelist('AddFiles', 'Process1', FileNames);
            end
        end
        bst_progress('stop');
        % Show the process1 tab
        panel_nodelist('SetSelectedPanel', 'Process1');
        % Load the processes in the pipeline editor
        panel_process_select('ShowPanel', FileNames, sProcesses);
        
    % === PROCESS 2 ===
    elseif (sProcesses(1).nInputs == 2)
        % If there is only one list of files: error
        if ~iscell(FileNames) || (length(FileNames) ~= 2) || ~iscell(FileNames{1})
            bst_error('This pipeline cannot be reloaded.', 'Reload pipeline', 0);
            return;
        end
        % Split the two lists of files
        FileNamesA = FileNames{1};
        FileNamesB = FileNames{2};
        % Get current files list
        sCurFilesA = panel_nodelist('GetFiles', 'Process2A');
        sCurFilesB = panel_nodelist('GetFiles', 'Process2B');
        if isequal(sCurFilesA, -1) || isequal(sCurFilesB, -1)
            bst_progress('stop');
            return;
        end
        
        % Update files list if modified
        if (isempty(sCurFilesA) && ~isempty(FileNamesA)) || (~isempty(sCurFilesA) && isstruct(sCurFilesA) && ~isequal({sCurFilesA.FileName}, FileNamesA)) || ...
           (isempty(sCurFilesB) && ~isempty(FileNamesB)) || (~isempty(sCurFilesB) && isstruct(sCurFilesB) && ~isequal({sCurFilesB.FileName}, FileNamesB))     
            % Reset files lists
            panel_nodelist('ResetAllLists');
            % Import files: leave file list empty
            if isempty(FileNamesA) || strcmpi(FileNamesA{1}, 'import') || isempty(FileNamesA{1})
                FileNamesA = {};
                FileNamesB = {};
            % Check for existing files
            else
                isExist = zeros(1,length(FileNamesA) + length(FileNamesB));
                for iFile = 1:length(FileNamesA)
                    sStudy = bst_get('AnyFile', FileNamesA{iFile});
                    isExist(iFile) = ~isempty(sStudy);
                end
                for iFile = 1:length(FileNamesB)
                    sStudy = bst_get('AnyFile', FileNamesB{iFile});
                    isExist(length(FileNamesA) + iFile) = ~isempty(sStudy);
                end
                if ~all(isExist)
                    bst_error('The input files do not exist anymore.', 'Reload pipeline', 0);
                    return;
                end
                % Set the files in the file list
                panel_nodelist('AddFiles', 'Process2A', FileNamesA);
                panel_nodelist('AddFiles', 'Process2B', FileNamesB);
            end
        end
        bst_progress('stop');
        % Show the process2 tab
        panel_nodelist('SetSelectedPanel', 'Process2');
        % Load the processes in the pipeline editor
        panel_process_select('ShowPanel', {FileNamesA,FileNamesB}, sProcesses);
    else
        bst_error('Unknown error.', 'Reload pipeline', 0);
        return;
    end
end


%% ===== EXPORT =====
% USAGE:  bst_report('Export', ReportFile, HtmlFile=[ask])
%         bst_report('Export', ReportFile, HtmlDir)
function HtmlFile = Export(ReportFile, HtmlFile, FileFormat)
    global GlobalData;

    % === PARSE INPUTS ===
    % Get report file to export
    if (nargin < 1) || isempty(ReportFile)
        ReportFile = GlobalData.ProcessReports.CurrentFile;
    end
    if isempty(ReportFile)
        disp('BST> The current report is empty.');
        HtmlFile = [];
        return;
    end
    % Get output file
    if (nargin < 2) || isempty(HtmlFile)
        HtmlFile = [];
    end
    % Get output format (HTML or PNG)
    if (nargin < 3) || isempty(FileFormat)
        FileFormat = 'HTML';
    end
    
    % === GET OUTPUT FILE ===
    % If input is a directory
    if ~isempty(HtmlFile) && isdir(HtmlFile)
        % Default filename: based on the report file
        [fPath, fBase, fExt] = bst_fileparts(ReportFile);
        HtmlFile = bst_fullfile(HtmlFile, [fBase, '.html']);
    % If filename is not set
    elseif isempty(HtmlFile)
        % Get default directories and formats
        LastUsedDirs = bst_get('LastUsedDirs');
        % Default filename: based on the report file
        [fPath, fBase, fExt] = bst_fileparts(ReportFile);
        HtmlFile = bst_fullfile(LastUsedDirs.ExportImage, [fBase, '.html']);
        % Select file
        [HtmlFile, FileFormat] = java_getfile('save', ...
            'Export report...', ... % Window title
            HtmlFile, ... % Default file
            'single', 'files_and_dirs', ... % Selection mode
            {'.html', 'HTML file (*.html)', 'HTML';, ... % Output formats
             '.png',  'PNG images (*.png)', 'PNG'}, ...
             1); % Default format
        % If no file was selected: exit
        if isempty(HtmlFile)
            return;
        end
        % Save new default export path
        LastUsedDirs.ExportImage = bst_fileparts(HtmlFile);
        bst_set('LastUsedDirs', LastUsedDirs);
    end
    
    % Load report file
    if isequal(ReportFile, 'current')
        ReportsMat = GlobalData.ProcessReports;
    else
        ReportsMat = load(ReportFile, 'Reports');
    end
    
    % === SAVE FILE ===
    % Save report images as PNG
    if strcmp(FileFormat, 'PNG')
        % Progress bar
        bst_progress('start', 'Report viewer', 'Exporting report images in PNG...');
        
        [folder, filename, fileext] = bst_fileparts(HtmlFile);
        nRows = size(ReportsMat.Reports, 1);
        iImage = 1;

        % Loop through entries in report
        for iRow = 1:nRows
            % Save images
            if strcmp(ReportsMat.Reports{iRow,1}, 'image')
                imwrite(ReportsMat.Reports{iRow,4}, ...
                    bst_fullfile(folder, ...
                    [filename sprintf('_%#02d', iImage) fileext]));
                iImage = iImage + 1;
            end
        end
        
        % Warn user if no image was found
        if iImage == 1
            java_dialog('warning', 'No images found in this report.', 'Export report');
        end
    % Default: save report as HTML
    else
        % Progress bar
        bst_progress('start', 'Report viewer', 'Exporting report to HTML...');
        % Print report to HTML
        html = PrintToHtml(ReportsMat.Reports, 1);
        % Open file for writing
        fid = fopen(HtmlFile, 'wt');
        if (fid == 0)
            error(['Could not write file: ' HtmlFile]);
        end
        % Write file
        fwrite(fid, html);
        % Close file
        fclose(fid);
        % End message
        disp(['Report exported to: ' HtmlFile]);
    end
    
    % Close progress bar
    bst_progress('stop');
end

%% ===== SEND EMAIL =====
% USAGE:  [isOk, resp] = bst_report('Email', ReportFile, username, to, subject, isFullReport=1, isOnlyText=0)
function [isOk, resp] = Email(ReportFile, username, to, subject, isFullReport, isOnlyText)
    % Parse inputs
    if (nargin < 6) || isempty(isOnlyText)
        isOnlyText = 0;
    end
    if (nargin < 5) || isempty(isFullReport)
        isFullReport = 1;
    end
    % Minimum Matlab version: 2014b
    if ~exist('webread', 'file')
        error('Sending email requires Matlab >= 2014b.');
    end
    % Get report
    if exist(ReportFile, 'file')
        ReportMat = load(ReportFile);
        Reports = ReportMat.Reports;
    else
        if isempty(ReportFile) || ~any(strcmpi(ReportFile, {'last', 'current', 'previous', 'next', 'loaded'}))
            ReportFile = 'current';
        end
        Reports = GetReport(ReportFile);
    end
    html = '';
    % RESTful arguments
    restArgs = {'g', '7gA9b3EW54', 'u', username, 't', to, 's', subject, 'b', html};
    if ~bst_verlessthan(901)
        restArgs{end+1} = weboptions('CertificateFilename','');
    end
    % Prepare report and send it
    html = PrintToHtml(Reports, isFullReport, isOnlyText);
    restArgs{10} = html;
    try
        resp = webwrite('https://neuroimage.usc.edu/bst/send_email.php', restArgs{:});
    catch ME
        % Try to send as OnlyText report if Error413: "Request Entity Too Large"
        if ~isempty(strfind(lower(ME.identifier), 'http413')) && ~isOnlyText
            html = PrintToHtml(Reports, isFullReport, 1);
            restArgs{10} = html;
            try
                resp = webwrite('https://neuroimage.usc.edu/bst/send_email.php', restArgs{:});
            catch
                resp = 'bad';
            end
        else
            resp = 'bad';
        end
    end
    % Return status
    isOk = isequal(resp, 'ok');
end

%% == GET ELAPSED TIME ===
function strElapsed = GetElapsedStr(Reports, iStart, iStop)
    strElapsed = '';
    if nargin < 3 || isempty(Reports) || isempty(iStart) || isempty(iStop)
        return
    end
    % Get time elapsed between start and stop
    eTime = datevec(datenum(Reports{iStop,5}) - datenum(Reports{iStart,5}));
    % Format elapsed time
    strElapsed = [];
    if (eTime(3) > 0)
        strElapsed = [strElapsed num2str(eTime(3)) 'd '];
    end
    if (eTime(4) > 0)
        strElapsed = [strElapsed num2str(eTime(4)) 'h '];
    end
    if (eTime(5) > 0)
        strElapsed = [strElapsed num2str(eTime(5)) 'm '];
    end
    % Time line
    strElapsed = [strElapsed num2str(eTime(6)) 's'];
end

%% ===== RESET CURRENT REPORT =====
% USAGE:  bst_report('Reset')
function Reset()
    global GlobalData;
    GlobalData.ProcessReports.Reports = {};
end

