function [cancel,figh] = multi( label, varargin )
%wai_tbar.multi: add, remove or update an entry on the multi waitbar
%
%   wait_bar.multi(TAG,VALUE) adds a waitbar for the specified label, or
%   if it already exists updates the value. TAG must be a string and
%   VALUE a number between zero and one or the string 'Close' to remove the
%   entry Setting value equal to 0 or 'Reset' will cause the progress bar
%   to reset and the time estimate to be re-initialized.
%
%   wait_bar.multi(TAG,COMMAND,VALUE,...)  or
%   wait_bar.multi(TAG,VALUE,COMMAND,VALUE,...)
%   passes one or more command/value pairs for changing the named waitbar
%   entry. Possible commands include:
%   'Value'       Set the value of the named waitbar entry. The
%                 corresponding value must be a number between 0 and 1.
%   'Increment'   Increment the value of the named waitbar entry. The
%                 corresponding value must be a number between 0 and 1.
%   'Color'       Change the color of the named waitbar entry. The
%                 value must be an RGB triple, e.g. [0.1 0.2 0.3], or a
%                 single-character color name, e.g. 'r', 'b', 'm'.
%   'Relabel'     Change the label of the named waitbar entry. The
%                 value must be the new name.
%   'Reset'       Set the named waitbar entry back to zero and reset its
%                 timer. No value need be specified.
%   'CanCancel'   [on|off] should a "cancel" button be shown for this bar
%                 (default 'off').
%   'CancelFcn'   Function to call in the event that the user cancels.
%   'ResetCancel' Reset the "cancelled" flag for an entry (ie. if you
%                 decide not to cancel).
%   'Close'       Remove the named waitbar entry.
%   'Busy'        Puts this waitbar in "busy mode" where a small bar
%                 bounces back and forth. Return to normal progress display
%                 using the 'Reset' command.
%
%   cancel = wait_bar.multi(LABEL,VALUE) also returns whether the user has
%   clicked the "cancel" button for this entry (true or false). Two
%   mechanisms are provided for cancelling an entry if the 'CanCancel'
%   setting is 'on'. The first is just to check the return argument and if
%   it is true abort the task. The second is to set a 'CancelFcn' that is
%   called when the user clicks the cancel button, much as is done for
%   MATLAB's built-in WAITBAR. In either case, you can use the
%   'ResetCancel' command if you don't want to cancel after all.
%
%   wait_bar.multi('CLOSEALL') closes the waitbar window.
%
%   Example:
%   wait_bar.multi( 'CloseAll' );
%   wait_bar.multi( 'Task 1', 0 );
%   wait_bar.multi( 'Task 2', 0.5, 'Color', 'b' );
%   wait_bar.multi( 'Task 3', 'Busy');
%   wait_bar.multi( 'Task 1', 'Value', 0.1 );
%   wait_bar.multi( 'Task 2', 'Increment', 0.2 );
%   wait_bar.multi( 'Task 3', 'Reset' ); % Disables "busy" mode
%   wait_bar.multi( 'Task 3', 'Value', 0.3 );
%   wait_bar.multi( 'Task 2', 'Close' );
%   wait_bar.multi( 'Task 3', 'Close' );
%   wait_bar.multi( 'Task 1', 'Close' );
%
%   Example:
%   wait_bar.multi( 'Task 1', 0, 'CancelFcn', @(a,b) disp( ['Cancel ',a] ) );
%   for ii=1:100
%      abort = wait_bar.multi( 'Task 1', ii/100 );
%      if abort
%         % Here we would normally ask the user if they're sure
%         break
%      else
%         pause( 1 )
%      end
%   end
%   wait_bar.multi( 'Task 1', 'Close' )
%
%   Example:
%   wait_bar.multi( 'CloseAll' );
%   wait_bar.multi( 'Red...',    7/7, 'Color', [0.8 0.0 0.1] );
%   wait_bar.multi( 'Orange...', 6/7, 'Color', [1.0 0.4 0.0] );
%   wait_bar.multi( 'Yellow...', 5/7, 'Color', [0.9 0.8 0.2] );
%   wait_bar.multi( 'Green...',  4/7, 'Color', [0.2 0.9 0.3] );
%   wait_bar.multi( 'Blue...',   3/7, 'Color', [0.1 0.5 0.8] );
%   wait_bar.multi( 'Indigo...', 2/7, 'Color', [0.4 0.1 0.5] );
%   wait_bar.multi( 'Violet...', 1/7, 'Color', [0.8 0.4 0.9] );

%   Based on the work by Ben Tordoff
%
%   Copyright © 2007-2025 The MathWorks, Inc.
%
%   All rights reserved.
%
%   Redistribution and use in source and binary forms, with or without
%   modification, are permitted provided that the following conditions are met:
%
%   * Redistributions of source code must retain the above copyright notice, this
%     list of conditions and the following disclaimer.
%
%   * Redistributions in binary form must reproduce the above copyright notice,
%     this list of conditions and the following disclaimer in the documentation
%     and/or other materials provided with the distribution
%
%   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
%   AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
%   IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
%   DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
%   FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
%   DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
%   SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
%   CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
%   OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
%   OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

persistent FIGH;
cancel = false;

% Check basic inputs
error( nargchk( 1, inf, nargin ) ); %#ok<NCHKN> - kept for backwards compatibility
if ~ischar( label ) && ~isa( label, 'string' )
    error( 'multiWaitbar:BadArg', 'LABEL must be the name of the progress entry (i.e. a string)' );
end

% Try to get hold of the figure
if isempty( FIGH ) || ~ishandle( FIGH )
    FIGH = findall( 0, 'Type', 'figure', 'Tag', 'multiWaitbar:Figure' );
    if isempty(FIGH)
        FIGH = iCreateFig();
    else
        FIGH = handle( FIGH(1) );
    end
end

% Check for close all and stop early
if any( strcmpi( label, {'CLOSEALL','CLOSE ALL'} ) )
    iDeleteFigure(FIGH);
    figh = [];
    return;
end
if nargout>1
    figh = FIGH;
end

% Make sure we're on-screen
if ~strcmpi( FIGH.Visible, 'on' )
    FIGH.Visible = 'on';
end

% Make sure the timer is still valid - it can be found and deleted
% externally.
if ~isvalid( getappdata( FIGH, 'BusyTimer' ) )
    setappdata( FIGH, 'BusyTimer', iCreateTimer(FIGH) );
end

% Get the list of entries and see if this one already exists
entries = getappdata( FIGH, 'ProgressEntries' );
if isempty(entries)
    idx = [];
else
    idx = find( strcmp( label, {entries.Tag} ), 1, 'first' );
end
bgcol = getappdata( FIGH, 'DefaultProgressBarBackgroundColor' );

% If it doesn't exist, create it
needs_redraw = false;
entry_added = isempty(idx);
if entry_added
    % Create a new entry
    defbarcolor = getappdata( FIGH, 'DefaultProgressBarColor' );
    entries = iAddEntry( FIGH, entries, label, 0, defbarcolor, bgcol );
    idx = numel( entries );
end

% Check if the user requested a cancel
if nargout
    cancel = entries(idx).Cancel;
end

% Parse the inputs. We shortcut the most common case as an efficiency
force_update = false;
if nargin==2 && isnumeric( varargin{1} )
    entries(idx).LastValue = entries(idx).Value;
    entries(idx).Value = max( 0, min( 1, varargin{1} ) );
    entries(idx).Busy = false;
    needs_update = true;
else
    [params,values] = iParseInputs( varargin{:} );

    needs_update = false;
    for ii=1:numel( params )
        switch upper( params{ii} )
            case 'BUSY'
                entries(idx).Busy = true;
                needs_update = true;

            case 'VALUE'
                entries(idx).LastValue = entries(idx).Value;
                entries(idx).Value = max( 0, min( 1, values{ii} ) );
                entries(idx).Busy = false;
                needs_update = true;

            case {'INC','INCREMENT'}
                entries(idx).LastValue = entries(idx).Value;
                entries(idx).Value = max( 0, min( 1, entries(idx).Value + values{ii} ) );
                entries(idx).Busy = false;
                needs_update = true;

            case {'COLOR','COLOUR'}
                entries(idx).CData = iMakeColors( values{ii}, 16 );
                needs_update = true;
                force_update = true;

            case {'RELABEL', 'UPDATELABEL'}
                % Make sure we have a string as the value and that it
                % doesn't already appear
                if ~ischar( values{ii} ) && ~isStringScalar( values{ii} )
                    error( 'multiWaitbar:BadString', 'Value for ''Relabel'' must be a string.' );
                end

                entries(idx).Label = values{ii};
                if ~isempty(entries(idx).CancelButton)
                    set( entries(idx).CancelButton, 'Callback', @(src,evt) iCancelEntry(src, entries(idx).Tag) );
                end
                needs_update = true;
                force_update = true;

            case {'CANCANCEL'}
                if ~ischar( values{ii} ) || ~any( strcmpi( values{ii}, {'on','off'} ) )
                    error( 'multiWaitbar:BadString', 'Parameter ''CanCancel'' must be a ''on'' or ''off''.' );
                end
                entries(idx).CanCancel = strcmpi( values{ii}, 'on' );
                entries(idx).Cancel = false;
                needs_redraw = true;

            case {'RESETCANCEL'}
                entries(idx).Cancel = false;
                needs_redraw = true;

            case {'CANCELFCN'}
                if ~isa( values{ii}, 'function_handle' )
                    error( 'multiWaitbar:BadFunction', 'Parameter ''CancelFcn'' must be a valid function handle.' );
                end
                entries(idx).CancelFcn = values{ii};
                if ~entries(idx).CanCancel
                    entries(idx).CanCancel = true;
                end
                needs_redraw = true;

            case {'CLOSE','DONE'}
                if ~isempty(idx)
                    % Remove the selected entry
                    entries = iDeleteEntry( entries, idx );
                end
                if isempty( entries )
                    iDeleteFigure( FIGH );
                    % With the window closed, there's nothing else to do
                    return;
                else
                    needs_redraw = true;
                end
                % We can't continue after clearing the entry, so jump out
                break;

            otherwise
                error( 'multiWaitbar:BadArg', 'Unrecognized command: ''%s''', params{ii} );

        end
    end
end

% Now work out what to update/redraw
if needs_redraw
    setappdata( FIGH, 'ProgressEntries', entries );
    iRedraw( FIGH );
    % NB: Redraw includes updating all bars, so never need to do both
elseif needs_update
    [entries(idx),needs_redraw] = iUpdateEntry( entries(idx), force_update );
    setappdata( FIGH, 'ProgressEntries', entries );
    % NB: if anything was updated onscreen, "needs_redraw" is now true.
end
if entry_added || needs_redraw
    % If the shape or size has changed, do a full redraw, including events
    drawnow();
end

% If we have any "busy" entries, start the timer, otherwise stop it.
myTimer = getappdata( FIGH, 'BusyTimer' );
if any([entries.Busy])
    if strcmpi(myTimer.Running,'off')
        start(myTimer);
    end
else
    if strcmpi(myTimer.Running,'on')
        stop(myTimer);
    end
end

end % multiWaitbar


%-------------------------------------------------------------------------%
function [params, values] = iParseInputs( varargin )
% Parse the input arguments, extracting a list of commands and values
idx = 1;
params = {};
values = {};
if nargin==0
    return;
end
if isnumeric( varargin{1} )
    params{idx} = 'Value';
    values{idx} = varargin{1};
    idx = idx + 1;
end

while idx <= nargin
    param = varargin{idx};
    if ~ischar( param )
        error( 'multiWaitbar:BadSyntax', 'Additional properties must be supplied as property-value pairs' );
    end
    params{end+1,1} = param; %#ok<AGROW>
    values{end+1,1} = []; %#ok<AGROW>
    switch upper( param )
        case {'DONE','CLOSE','RESETCANCEL'}
            % No value needed, and stop
            break;
        case {'BUSY'}
            % No value needed but parsing should continue
            idx = idx + 1;
        case {'RESET','ZERO','SHOW'}
            % All equivalent to saying ('Value', 0)
            params{end} = 'Value';
            values{end} = 0;
            idx = idx + 1;
        otherwise
            if idx==nargin
                error( 'multiWaitbar:BadSyntax', 'Additional properties must be supplied as property-value pairs' );
            end
            values{end,1} = varargin{idx+1};
            idx = idx + 2;
    end
end
if isempty( params )
    error( 'multiWaitbar:BadSyntax', 'Must specify a value or a command' );
end
end % iParseInputs

%-------------------------------------------------------------------------%
function fobj = iCreateFig()
% Create the progress bar group window
bgcol = get(0,'DefaultUIControlBackgroundColor');
f = figure( ...
    'Name', 'Progress', ...
    'Tag', 'multiWaitbar:Figure', ...
    'Color', bgcol, ...
    'MenuBar', 'none', ...
    'ToolBar', 'none', ...
    'WindowStyle', 'normal', ... % We don't want to be docked!
    'HandleVisibility', 'off', ...
    'IntegerHandle', 'off', ...
    'Visible', 'off', ...
    'NumberTitle', 'off' );
% Resize and centre on the first screen
screenSize = get(0,'ScreenSize');
figSz = [360 42];
figPos = ceil((screenSize(1,3:4)-figSz)/2);
fobj = handle( f );
fobj.Position = [figPos, figSz];
setappdata( fobj, 'ProgressEntries', [] );
% Make sure we have the image
defbarcolor = [0.8 0.0 0.1];
barbgcol = uint8( 255*0.75*bgcol );
setappdata( fobj, 'DefaultProgressBarBackgroundColor', barbgcol );
setappdata( fobj, 'DefaultProgressBarColor', defbarcolor );
setappdata( fobj, 'DefaultProgressBarSize', [350 16] );
setappdata( fobj, 'MaxEntryRows', 10 );
% Create the timer to use for "Busy" mode, being sure to delete any
% existing ones
delete( timerfind('Tag', 'MultiWaitbarTimer') );
myTimer = iCreateTimer(f);
setappdata( fobj, 'BusyTimer', myTimer );

% Setup the resize function after we've finished setting up the figure to
% avoid excessive redraws
fobj.ResizeFcn = @iRedraw;
fobj.CloseRequestFcn = @iCloseFigure;
end % iCreateFig

%-------------------------------------------------------------------------%
function t = iCreateTimer(fig)
t = timer( ...
    'TimerFcn', @(src,evt) iTimerFcn(fig), ...
    'Period', 0.02, ...
    'ExecutionMode', 'FixedRate', ...
    'Tag', 'MultiWaitbarTimer' );
end

%-------------------------------------------------------------------------%
function cdata = iMakeColors( baseColor, height )
% Creates a shiny bar from a single base color
lightColor = [1 1 1];
badColorErrorID = 'multiWaitbar:BadColor';
badColorErrorMsg = 'Colors must be a three element vector [R G B] or a single character (''r'', ''g'' etc.)';

if ischar(baseColor)
    switch upper(baseColor)
        case 'K'
            baseColor = [0.1 0.1 0.1];
        case 'R'
            baseColor = [0.8 0 0];
        case 'G'
            baseColor = [0 0.6 0];
        case 'B'
            baseColor = [0 0 0.8];
        case 'C'
            baseColor = [0.2 0.8 0.9];
        case 'M'
            baseColor = [0.6 0 0.6];
        case 'Y'
            baseColor = [0.9 0.8 0.2];
        case 'W'
            baseColor = [0.9 0.9 0.9];
        otherwise
            error( badColorErrorID, badColorErrorMsg );
    end
else
    if numel(baseColor) ~= 3
        error( badColorErrorID, badColorErrorMsg );
    end
    if isa( baseColor, 'uint8' )
        baseColor = double( baseColor ) / 255;
    elseif isa( baseColor, 'double' )
        if any(baseColor>1) || any(baseColor<0)
            error( 'multiWaitbar:BadColorValue', 'Color values must be in the range 0 to 1 inclusive.' );
        end
    else
        error( badColorErrorID, badColorErrorMsg );
    end
end

% By this point we should have a double precision 3-element vector.
cols = repmat( baseColor, [height, 1] );

breaks = max( 1, round( height * [1 25 50 75 88 100] / 100 ) );
cols(breaks(1),:) = 0.6*baseColor;
cols(breaks(2),:) = lightColor - 0.4*(lightColor-baseColor);
cols(breaks(3),:) = baseColor;
cols(breaks(4),:) = min( baseColor*1.2, 1.0 );
cols(breaks(5),:) = min( baseColor*1.4, 0.95 ) + 0.05;
cols(breaks(6),:) = min( baseColor*1.6, 0.9 ) + 0.1;

y = 1:height;
cols(:,1) = max( 0, min( 1, interp1( breaks, cols(breaks,1), y, 'pchip' ) ) );
cols(:,2) = max( 0, min( 1, interp1( breaks, cols(breaks,2), y, 'pchip' ) ) );
cols(:,3) = max( 0, min( 1, interp1( breaks, cols(breaks,3), y, 'pchip' ) ) );
cdata = uint8( 255 * cat( 3, cols(:,1), cols(:,2), cols(:,3) ) );
end % iMakeColors


%-------------------------------------------------------------------------%
function cdata = iMakeBackground( baseColor, height )
% Creates a shaded background
if isa( baseColor, 'uint8' )
    baseColor = double( baseColor ) / 255;
end

ratio = 1 - exp( -0.5-2*(1:height)/height )';
cdata = uint8( 255 * cat( 3, baseColor(1)*ratio, baseColor(2)*ratio, baseColor(3)*ratio ) );
end % iMakeBackground

%-------------------------------------------------------------------------%
function entries = iAddEntry( parent, entries, label, value, color, bgcolor )
% Add a new entry to the progress bar

% Create bar coloring
psize = getappdata( parent, 'DefaultProgressBarSize' );
cdata = iMakeColors( color, 16 );
% Create background image
barcdata = iMakeBackground( bgcolor, psize(2) );

% Work out the size in advance
mypanel = uipanel( 'Parent', parent, 'Units', 'Pixels', 'BorderType', 'line' );
labeltext = uicontrol( 'Style', 'Text', ...
    'String', label, ...
    'Parent', parent, ...
    'HorizontalAlignment', 'Left' );
etatext = uicontrol( 'Style', 'Text', ...
    'String', '', ...
    'Parent', parent, ...
    'HorizontalAlignment', 'Right' );
% From R2025a onwards we must use UIImage rather than a checkbox.
if matlab_ver_less_than('25.1')
    progresswidget = uicontrol( 'Style', 'Checkbox', ...
        'String', '', ...
        'Parent', parent, ...
        'Position', [5 5 psize], ...
        'CData', barcdata );
else
    progresswidget = uiimage( ...
        'Parent', parent, ...
        'Position', [5 5 psize], ...
        'ImageSource', barcdata );
end
cancelwidget = uicontrol( 'Style', 'PushButton', ...
    'String', '', ...
    'FontWeight', 'Bold', ...
    'Parent', parent, ...
    'Position', [5 5 16 16], ...
    'CData', iMakeCross( 8 ), ...
    'Callback', @(src,evt) iCancelEntry( src, label ), ...
    'Visible', 'off' );

newentry = struct( ...
    'Tag', label, ...
    'Label', label, ...
    'Value', value, ...
    'LastValue', inf, ...
    'Created', tic(), ...
    'LabelText', labeltext, ...
    'ETAText', etatext, ...
    'ETAString', '', ...
    'Progress', progresswidget, ...
    'ProgressSize', psize, ...
    'Panel', mypanel, ...
    'BarCData', barcdata, ...
    'CData', cdata, ...
    'BackgroundCData', barcdata, ...
    'CanCancel', false, ...
    'CancelFcn', [], ...
    'CancelButton', cancelwidget, ...
    'Cancel', false, ...
    'Busy', false );
if isempty( entries )
    entries = newentry;
else
    entries = [entries;newentry];
end
% Store in figure before the redraw
setappdata( parent, 'ProgressEntries', entries );
if strcmpi( get( parent, 'Visible' ), 'on' )
    iRedraw( parent, [] );
    % Redraw may have modified the entries
    entries = getappdata( parent, 'ProgressEntries');
else
    set( parent, 'Visible', 'on' );
end
end % iAddEntry

%-------------------------------------------------------------------------%
function entries = iDeleteEntry( entries, idx )
delete( entries(idx).LabelText );
delete( entries(idx).ETAText );
delete( entries(idx).CancelButton );
delete( entries(idx).Progress );
delete( entries(idx).Panel );
entries(idx,:) = [];
end % iDeleteEntry

%-------------------------------------------------------------------------%
function entries = iCancelEntry( src, name )
figh = ancestor( src, 'figure' );
entries = getappdata( figh, 'ProgressEntries' );
if isempty(entries)
    % The entries have been lost - nothing can be done.
    return
end
idx = find( strcmp( name, {entries.Tag} ), 1, 'first' );

% Set the cancel flag so that the user is told on next update
entries(idx).Cancel = true;
setappdata( figh, 'ProgressEntries', entries );

% If a user function is supplied, call it
if ~isempty( entries(idx).CancelFcn )
    feval( entries(idx).CancelFcn, name, 'Cancelled' );
end

end % iCancelEntry


%-------------------------------------------------------------------------%
function [entry,updated] = iUpdateEntry( entry, force )
% Update one progress bar

% Deal with busy entries separately
if entry.Busy
    entry = iUpdateBusyEntry(entry);
    updated = true;
    return;
end

% Some constants
marker_weight = 0.8;

% Check if the label needs updating
updated = force;
val = entry.Value;
lastval = entry.LastValue;

% Now update the bar
psize = entry.ProgressSize;
filled = max( 1, round( val*psize(1) ) );
lastfilled = max( 1, round( lastval*psize(1) ) );

% We do some careful checking so that we only redraw what we have to. This
% makes a small speed difference, but every little helps!
if force || (filled<lastfilled)
    % Create the bar background
    startIdx = 1;
    bgim = entry.BackgroundCData(:,ones( 1, ceil(psize(1)-filled) ),:);
    barim = iMakeBarImage(entry.CData, startIdx, filled);
    progresscdata = [barim,bgim];

    % Add light/shadow around the markers
    markers = round( (0.1:0.1:val)*psize(1) );
    markers(markers<=startIdx | markers>(filled-2)) = [];
    highlight = [marker_weight*entry.CData, 255 - marker_weight*(255-entry.CData)];
    for ii=1:numel( markers )
        progresscdata(:,markers(ii)+[-1,0],:) = highlight;
    end
    updated = true;
elseif filled > lastfilled
    % Just need to update the existing data
    progresscdata = entry.BarCData;
    startIdx = max(1,lastfilled-1);
    % Repmat is the obvious way to fill the bar, but BSXFUN is often
    % faster. Indexing is obscure but faster still.
    progresscdata(:,startIdx:filled,:) = iMakeBarImage(entry.CData, startIdx, filled);

    % Add light/shadow around the markers
    markers = round( (0.1:0.1:val)*psize(1) );
    markers(markers<startIdx | markers>(filled-2)) = [];
    highlight = [marker_weight*entry.CData, 255 - marker_weight*(255-entry.CData)];
    for ii=1:numel( markers )
        progresscdata(:,markers(ii)+[-1,0],:) = highlight;
    end
    updated = true;
end

if updated
    % Set the image into the entry and in the widget
    entry.BarCData = progresscdata;
    if isa(entry.Progress, "matlab.ui.control.Image")
        entry.Progress.ImageSource = progresscdata;
    else
        set( entry.Progress, 'cdata', progresscdata );
    end
end

% As an optimization, don't update any text if the bar didn't move and the
% percentage hasn't changed
decval = round( val*100 );
lastdecval = round( lastval*100 );

if ~updated && (decval == lastdecval)
    return
end

% Now work out the remaining time
minTime = 3; % secs
if val <= 0
    % Zero value, so clear the eta
    entry.Created = tic();
    elapsedtime = 0;
    etaString = '';
else
    elapsedtime = round(toc( entry.Created )); % in seconds

    % Only show the remaining time if we've had time to estimate
    if elapsedtime < minTime
        % Not enough time has passed since starting, so leave blank
        etaString = '';
    else
        % Calculate a rough ETA
        eta = elapsedtime * (1-val) / val;
        etaString = iGetTimeString( eta );
    end
end

if ~isequal( etaString, entry.ETAString )
    set( entry.ETAText, 'String', etaString );
    entry.ETAString = etaString;
    updated = true;
end

% Update the label too
if force || elapsedtime > minTime
    if force || (decval ~= lastdecval)
        labelstr = [entry.Label, sprintf( ' (%d%%)', decval )];
        set( entry.LabelText, 'String', labelstr );
        updated = true;
    end
end

end % iUpdateEntry

function eta = iGetTimeString( remainingtime )
if remainingtime > 172800 % 2 days
    eta = sprintf( '%d days', round(remainingtime/86400) );
else
    if remainingtime > 7200 % 2 hours
        eta = sprintf( '%d hours', round(remainingtime/3600) );
    else
        if remainingtime > 120 % 2 mins
            eta = sprintf( '%d mins', round(remainingtime/60) );
        else
            % Seconds
            remainingtime = round( remainingtime );
            if remainingtime > 1
                eta = sprintf( '%d secs', remainingtime );
            elseif remainingtime == 1
                eta = '1 sec';
            else
                eta = ''; % Nearly done (<1sec)
            end
        end
    end
end
end % iGetTimeString


%-------------------------------------------------------------------------%
function entry = iUpdateBusyEntry( entry )
% Update a "busy" progress bar
% Make sure the widget is still OK
if ~ishandle(entry.Progress)
    return
end
% Work out the new position. Since the bar is 0.1 long and needs to bounce,
% the position varies from 0 up to 0.9 then back down again. We achieve
% this with judicious use of "mod" with 1.8.
entry.Value = mod(entry.Value+0.01,1.8);
val = entry.Value;
if val>0.9
    % Moving backwards
    val = 1.8-val;
end
psize = entry.ProgressSize;
startIdx = max( 1, round( val*psize(1) ) );
endIdx = max( 1, round( (val+0.1)*psize(1) ) );
barLength = endIdx - startIdx + 1;

% Create the image
bgim = entry.BackgroundCData(:,ones( 1, psize(1) ),:);
barim = iMakeBarImage(entry.CData, 1, barLength);
bgim(:,startIdx:endIdx,:) = barim;

% Put it into the widget
entry.BarCData = bgim;
if isa(entry.Progress,"matlab.ui.control.Image")
    entry.Progress.ImageSource = bgim;
else
    set(entry.Progress, 'cdata', bgim);
end
end % iUpdateBusyEntry


%-------------------------------------------------------------------------%
function barim = iMakeBarImage(strip, startIdx, endIdx)
shadow1_weight = 0.4;
shadow2_weight = 0.7;
barLength = endIdx - startIdx + 1;
% Repmat is the obvious way to fill the bar, but BSXFUN is often
% faster. Indexing is obscure but faster still.
barim = strip(:,ones(1, barLength),:);
% Add highlight to the start of the bar
if startIdx <= 2 && barLength>=2
    barim(:,1,:) = 255 - shadow1_weight*(255-strip);
    barim(:,2,:) = 255 - shadow2_weight*(255-strip);
end
% Add shadow to the end of the bar
if endIdx>=4 && barLength>=2
    barim(:,end,:) = shadow1_weight*strip;
    barim(:,end-1,:) = shadow2_weight*strip;
end
end % iMakeBarImage

%-------------------------------------------------------------------------%
function iCloseFigure( fig, evt ) %#ok<INUSD>
% Closing the figure just makes it invisible
set( fig, 'Visible', 'off' );
end % iCloseFigure

%-------------------------------------------------------------------------%
function iDeleteFigure( fig )
% Actually destroy the figure
busyTimer = getappdata( fig, 'BusyTimer' );
if isvalid( busyTimer )
    stop( busyTimer );
end
delete( busyTimer );
delete( fig );
end % iDeleteFigure

%-------------------------------------------------------------------------%
function iRedraw( fig, evt ) %#ok<INUSD>
entries = getappdata( fig, 'ProgressEntries' );
fobj = handle( fig );
p = fobj.Position;
% p = get( fig, 'Position' );
border = 5;
textheight = 16;
barheight = 16;
panelheight = 10;
maxRows = getappdata( fig, 'MaxEntryRows' );
N = max( 1, numel( entries ) );
Nrows = min(maxRows, N);
Ncols = ceil(N ./ Nrows);

% Check the height is correct
heightperentry = textheight+barheight+panelheight;
requiredheight = 2*border + Nrows*heightperentry - panelheight;
if ~isequal( p(4), requiredheight )
    p(2) = p(2) + p(4) - requiredheight;
    p(4) = requiredheight;
    % In theory setting the position should re-trigger this callback, but
    % in practice it doesn't, probably because we aren't calling "drawnow".
    set( fig, 'Position', p )
end
width = floor((p(3) - Ncols*2*border) ./ Ncols);
setappdata( fig, 'DefaultProgressBarSize', [width barheight] );

for ii=1:numel( entries )
    col = ceil(ii./Nrows)-1;
    row = ii - col.*Nrows - 1;
    xpos = border + (width+2*border).*col;
    ypos = p(4) - border - heightperentry.*row;

    set( entries(ii).Panel, 'Position', [xpos-border ypos+panelheight/2-heightperentry width+2*border+1 heightperentry] );
    set( entries(ii).LabelText, 'Position', [xpos ypos-textheight width*0.75 textheight] );
    set( entries(ii).ETAText, 'Position', [xpos+width*0.75 ypos-textheight width*0.25 textheight] );
    ypos = ypos - textheight;

    if entries(ii).CanCancel
        set( entries(ii).Progress, 'Position', [xpos ypos-barheight max(1,width-barheight+1) barheight] );
        entries(ii).ProgressSize = max(1,[width-barheight barheight]);
        set( entries(ii).CancelButton, 'Visible', 'on', 'Position', [p(3)-border-barheight ypos-barheight barheight barheight] );
    else
        set( entries(ii).Progress, 'Position', [xpos ypos-barheight width+1 barheight] );
        entries(ii).ProgressSize = [width barheight];
        set( entries(ii).CancelButton, 'Visible', 'off' );
    end
    entries(ii) = iUpdateEntry( entries(ii), true );
end
setappdata( fig, 'ProgressEntries', entries );
end % iRedraw

function cdata = iMakeCross( sz )
% Create a cross-shape icon of size sz*sz*3

cdata = diag(ones(sz,1),0) + diag(ones(sz-1,1),1) + diag(ones(sz-1,1),-1);
cdata = cdata + flip(cdata,2);

% Convert zeros to nans (transparent) and non-zeros to zero (black)
cdata(cdata == 0) = nan;
cdata(~isnan(cdata)) = 0;

% Convert to RGB
cdata = cat( 3, cdata, cdata, cdata );
end % iMakeCross


function iTimerFcn(fig)
% Timer callback for updating stuff every so often
entries = getappdata( fig, 'ProgressEntries' );
for ii=1:numel(entries)
    if entries(ii).Busy
        entries(ii) = iUpdateBusyEntry(entries(ii));
    end
end
setappdata( fig, 'ProgressEntries', entries );
end % iTimerFcn
