classdef ToolTip < handle
    %ToolTip Display a string when mouse lingers in a figure.
    %  T=ToolTip creates an object that configures a tool tip display for
    %  the current figure window.  Mouse motion and button press/release
    %  hides the tool tip, while keeping the mouse motionless for a period
    %  of time causes the tool tip to appear.  To activate the tool tip,
    %  start(T) must be called.
    %
    %  Tip is displayed after ShowAfter seconds with no motion, if the tip
    %  string is not empty.
    %
    %  Tip is removed immmediately when mouse moves, or after an optional
    %  maximum display time.
    %
    % Only one ToolTip object is created per Parent figure; subsequent
    % attempts to create a ToolTip object with the same Parent will return
    % a reference to the existing ToolTip object.
    
    % TODO:
    %
    %
    % BUGS
    %  - Set KeepOnTop=true when flash is addressed for tooltip panel
    %    stacking-order (only needed for Location='pointer')
    %
    %  - delete name cannot be used; instance will be marked dead
    %    regardless of what we do in override
    %
    %  - listeners should not embed a copy of the object instance, as that
    %    client instance can be deleted yet the object remain (?)
    %      - find the instance on each listener invocation
    
    
    properties (AbortSet)
        BackgroundColor = [255 255 225]./255 % Tooltip Yellow
        FontSize = 9
        ForegroundColor = [0 0 0]
        
        %Location of tool tip string.
        %  Options include 'none', 'top', 'bottom' and 'pointer'.
        Location = 'pointer'
    end
    
    properties (SetAccess=private)
        Parent
    end
    
    properties (AbortSet)
        ShowAfter = 1  % String becomes visible after ShowAfter seconds
        String = 'Tool tip string';
        RemainFor = 4    % String gets hidden after RemainFor seconds
    end
    
    properties (Hidden)
        % Mouse-event reaction period, in seconds
        %
        % Reciprocal is # times we react to mouse events per second
        MouseUpdatePeriod = 0.2
        
        % True enforces tooltip visual to stack above all other graphical
        % children in the figure.
        KeepOnTop = false
    end
    
    properties (Access = private)
        hTipText
        hTipPanel
        pLoc % cached value of location currently in use
        pMotion = false
        pExtent
        hListeners
        hShowAfterTimer
        hRemainForTimer
        
        % States:
        %   0=Timers stopped, hidden
        %   1=ShowAfter timer running, hidden
        %   2=ShowAfter finished, shown, RemainFor timer running
        pState = 0
    end
    
    methods
        function obj = ToolTip(parent)
            % One-time setup of Parent
            if nargin < 1
                parent = gcf;
            end
            obj.Parent = parent;
            obj = init(obj); % return instance from figure
        end
        
        function release(obj)
            % Remove a client reference to ToolTip.
            %
            %  - We keep a single instance per figure, so clients should
            %    NOT call delete; clients should call release(obj)
            %  - We stop any active banner displays
            %
            %  - Ideally, ToolTip instances should be
            %    reference-counted to know when all clients are done with
            %    the ToolTip instance in the figure; we do NOT do
            %    this currently
            %
            %  - Alternative (what we do today): don't remove BannerMessage
            %    instance from figure; since this was needed once in a
            %    figure, it's likely to be needed again
            
            % Stop any active tooltip displays.  They might NOT be from
            % this client, but there's no way to tell, and they are meant
            % to be temporary.
            stop(obj);
        end
        
        function delete(obj)
            % Remove the single instance from the figure
            
            destroyInstance(obj);
        end
        
        function start(obj,str)
            % Show string after ShowAfter delay.
            % Optional input sets new string.
            % Repeated calls restart the timer.

            % User or timer called us.
            %
            if obj.pState == 1
                % ShowAfter running (tip hidden)
                % - timer could expire while we are here, so we still
                %   make a "hide" call no matter what and set the state to
                %   1 just in case the callback fires
                stop(obj.hShowAfterTimer); % do 1st
                stop(obj.hRemainForTimer); % do 2nd (just in case)
                obj.pState = 1; % ShowAfter running (just in case)
                
            elseif obj.pState == 2
                % RemainFor running (tip shown)
                % - timer could expire while we are here, but worst case is
                %   that the timer callback sets state 1 already and hides
                %   the tip
                stop(obj.hRemainForTimer);
                obj.pState = 1; % ShowAfter running
                
            else % pState == 0 (disabled)
                obj.pState = 1; % ShowAfter running
                
                % Configure both timers, only in state 0 (disabled)
                obj.hShowAfterTimer.Duration = obj.ShowAfter;
                obj.hRemainForTimer.Duration = obj.RemainFor;
            end
            
            % Doing hideTip when ShowAfter=0 is not be needed, but 0 is
            % unusual (not recommended), and we got here from a hide state
            % so it won't be visible.
            hideTip(obj);
            
            % Update location if it changed
            if ~strcmpi(obj.pLoc,obj.Location)
                updateBannerWidgetPos(obj);
            end
            
            if nargin > 1
                obj.String = str;
                if isempty(str)
                    % Skip show/hide cycle, assume remainfor expired.
                    %  - keep state 1, don't start timer
                    return
                end
            end
            start(obj.hShowAfterTimer);
        end
        
        function stop(obj)
            % Make String invisible immediately.
            % Disable tooltip from reappearing.
            
            stopAllTimers(obj);
            hideTip(obj);
            obj.pState = 0; % Enter Stopped state
        end
        
        function set.String(obj,val)
            if ~iscellstr(val)
                validateattributes(val,{'char', 'string'},{}, ...
                    'ToolTip','String');
            end
            obj.String = val;
            updateString(obj); % side-effect
        end
        
        function set.FontSize(obj,val)
            validateattributes(val,{'double'}, ...
                {'real','scalar','>',0}, 'ToolTip','FontSize');
            obj.FontSize = val;
            updateFontSize(obj);
        end
        
        function set.Location(obj,val)
            obj.Location = validatestring(val,{'Top','Bottom','Pointer','None'}, ...
                'ToolTip','Location');
            
            % handling of multi-line strings differs based on location
            updateString(obj);
            
            resize(obj);
        end
        
        function set.ShowAfter(obj,val)
            validateattributes(val,{'double'}, ...
                {'real','scalar','nonnegative'}, ...
                'ToolTip','ShowAfter');
            
            if obj.pState > 0 %#ok<MCSUP>
                error('ToolTip:ReadOnlyWhileEnabled', ...
                    'Cannot change timer settings while ToolTip is enabled.');
            end
            obj.ShowAfter = val;
        end

        function set.RemainFor(obj,val)
            validateattributes(val,{'double'}, ...
                {'real','scalar','nonnegative'}, ...
                'ToolTip','RemainFor');
            
            if obj.pState > 0 %#ok<MCSUP>
                error('ToolTip:ReadOnlyWhileEnabled', ...
                    'Cannot change timer settings while ToolTip is enabled.');
            end
            obj.RemainFor = val;
        end
        
        function set.ForegroundColor(obj,val)
            internal.ColorConversion.validatecolorspec(val, ...
                'ToolTip','ForegroundColor');
            obj.ForegroundColor = val;
            updateColor(obj);
        end
        
        function set.BackgroundColor(obj,val)
            internal.ColorConversion.validatecolorspec(val, ...
                'ToolTip','BackgroundColor');
            obj.BackgroundColor = val;
            updateColor(obj);
        end
    end
    
    methods (Access = private)
        function retObj = init(obj)
            % Initialize for new client.
            %
            % If a ToolTip object is already associated with the parent
            % figure, reuse that object - only one ToolTip is needed per
            % figure.  Do NOT reinitalize or change state in this case.
            
            name = 'internal_ToolTip';
            p = obj.Parent;
            retObj = getappdata(p,name);
            if isempty(retObj) || ~isvalid(retObj)
                setappdata(p,name,obj);
                
                initWidgets(obj);
                initTimers(obj);
                initListeners(obj);
                
                obj.pState = 0; % in Stopped/Disabled state
                resize(obj);
                retObj = obj;
            end
        end
        
        function destroyFigureInstance(obj)
            % Added by Vish
            ax = obj.Parent;         
            check_ax_del = isa(ax,'matlab.ui.Figure') && ishghandle(ax);
            if check_ax_del % check if figure got deleted
                % Test if internal_ToolTip property exists in appdata
                if isappdata(ax,'internal_ToolTip')
                    % Remove singleton instance from figure
                    rmappdata(obj.Parent,'internal_ToolTip');
                end
            end
        end
        
        function removeFromFigure(obj)
            % Remove instance from figure, and destroy instance.
            
            destroyInstance(obj);
        end
        
        function destroyInstance(obj)
            % Delete this instance including all resources.
            %
            % Called from delete when ref-counting, etc, are considered.
            %
            % Removes instance from figure.
            
            deleteListeners(obj);
            deleteWidgets(obj);
            deleteTimers(obj);
            
            destroyFigureInstance(obj);
        end
        
        function hideTip(obj)
            if ~isempty(obj.hTipPanel) && ishghandle(obj.hTipPanel)
                obj.hTipPanel.Visible = 'off';
            end
        end
        
        function showTip(obj)
            % Make tip visible if a non-empty string is present
            
            if ~isempty(obj.String) ...
                    && ~isempty(obj.hTipPanel) && ishghandle(obj.hTipPanel)
                
                % Reposition tooltip if Location is 'pointer'
                if strcmpi(obj.Location,'pointer')
                    p = obj.Parent;
                    orig = p.Units;
                    p.Units = 'pixels';
                    xy = p.CurrentPoint(1,1:2);
                    p.Units = orig;
                    
                    ptrRows = size(p.PointerShapeCData,1);
                    wh = getExtent(obj); % [dx dy]
                    
                    % We need to reduce the height of the widget further.
                    % The cursor pointer icon may be offset from cursor
                    % location by a vertical offset.  Besides, we want a
                    % small vertical gutter space.
                    twoBorders = 2;
                    obj.hTipPanel.Position = [ ...
                        xy(1)+twoBorders ...
                        xy(2)-wh(2)-ptrRows-3-twoBorders ...
                        wh(1)+1+twoBorders ...
                        wh(2)+twoBorders];
                    
                    updatePointerOnScreenPos(obj);
                    
                    if obj.KeepOnTop
                        orderTipAboveOtherChildren(obj);
                    end
                end
                
                % Make string visible
                obj.hTipPanel.Visible = 'on';
            end
        end
        
        function deleteTimers(obj)
            % Destroy timers
            stopAllTimers(obj);
            delete(obj.hShowAfterTimer);
            delete(obj.hRemainForTimer);
        end
        
        function stopAllTimers(obj)
            % Stops all timers.
            % Returns state prior to stop.
            
            % Shut down the ShowAfter timer first, as we don't want it to
            % complete and "flash up" the text if it's not already shown.
            if ~isempty(obj.hShowAfterTimer) && isvalid(obj.hShowAfterTimer)
                stopAndWait(obj.hShowAfterTimer);
            end
            if ~isempty(obj.hRemainForTimer) && isvalid(obj.hRemainForTimer)
                stopAndWait(obj.hRemainForTimer);
            end
        end
        
        function initTimers(obj)
            obj.hShowAfterTimer = internal.TimeoutTimer( ...
                @(~,~)showAfterTimeoutFcn(obj), obj.ShowAfter);
            obj.hRemainForTimer = internal.TimeoutTimer( ...
                @(~,~)remainForTimeoutFcn(obj), obj.RemainFor);
        end
        
        function showAfterTimeoutFcn(obj,doShow)
            % ShowAfter timer expired.
            % Show tool tip.
            % Start RemainFor timer.
            
            if nargin < 2 || doShow
                showTip(obj);
            end
            obj.pState = 2; % Tip shown, RemainFor running
            
            % If RemainFor is inf, no timer needed
            d = obj.RemainFor;
            if ~isinf(d)
                % Start RemainFor timer:
                t = obj.hRemainForTimer;
                t.Duration = d;
                start(t);
            end
        end
        
        function remainForTimeoutFcn(obj)
            % RemainFor timer expired.
            % Hide tool tip.
            % Remain hidden, waiting for motion.
            
            hideTip(obj);
            obj.pState = 1; % Tip hidden, RemainFor finished
        end
        
        function resetEvent(obj,ev,coalesce)
            % Callback from mouse motion event.
            %
            % Optionally coalesce events to decrease timer-reset
            % invocations and increase performance during mouse motion.
            %
            % Similar to start,
            %  - Show string after ShowAfter delay.
            %  - Repeated calls restart the timer.
            %
            % Unlike start,
            %  - assume no change in ShowAfter duration
            %  - assume no change in tip
            %  - we reduce number of timer restarts, since we are in
            %    the mouse loop and processing delay is highly noticeable
            %
            % To reduce restarts,
            %  - do not reset if elapsed time is less than holdoff
            %  - this leads to a shortening of the "show after" interval
            %    by a maximum of just under one holdoff time.

            % We could be in any state.
            %
            % We should never get here in pState=0.
            %
            % Assume we're in state 1 and find the elapsed time right away.
            %
            % State 2 means there's been no mouse motion for a long time
            % and we can afford the extra time to get elapsed.  If we
            % didn't need it, the time is not noticed.
            
            % Record point so we can get it without clicking
            obj.Parent.CurrentPoint = ev.Point;
            
            if obj.pState == 1     % Tip hidden, ShowAfter running
                if coalesce
                    % Restart timer only every MouseUpdatePeriod seconds
                    
                    % Get elapsed time of the ShowAfter timer, compare to a
                    % holdoff delay.  We reduce number of start-invocations in
                    % this manner.
                    e = elapsed(obj.hShowAfterTimer);
                    if e > obj.MouseUpdatePeriod
                        % Restart timer, with correction for holdoff delay
                        %fprintf('restarted @ %g mS\n',e*1000)
                        if e < obj.ShowAfter
                            % Will execute showAfterTimeoutFcn
                            start(obj.hShowAfterTimer,-e);
                        else
                            % Manually execute showAfterTimeoutFcn, but
                            % suppress display of the tip string.  We may see a
                            % momentary "glitch" of the tip otherwise, where it
                            % shows then immediately hides.
                            showAfterTimeoutFcn(obj,false);
                        end
                    end
                else
                    % Restart time every event
                    %
                    % Will execute showAfterTimeoutFcn
                    start(obj.hShowAfterTimer);
                end
                
            elseif obj.pState == 2 % Tip shown, RemainFor running
                % We've been motionless for a while.
                % Hide the visible tip, restart timer.
                
                stop(obj.hRemainForTimer);
                hideTip(obj);
                obj.pState = 1; % ShowAfter running
                start(obj.hShowAfterTimer);
            end
        end
        
        function deleteWidgets(obj)
            
            h = obj.hTipPanel;
            if ~isempty(h) && ishghandle(h)
                delete(h);
                obj.hTipPanel = [];
                obj.hTipText = []; % gets deleted with parent panel
            end
        end
        
        function initWidgets(obj)
            
            obj.hTipPanel = uipanel( ...
                'Parent',obj.Parent, ...
                'Tag','ToolTipPanel', ...
                'HandleVisibility','off', ...
                'BorderType','line', ...
                'Units','pixels', ...
                'Position',[1 1 1 1], ...
                'BackgroundColor',obj.BackgroundColor, ...
                'HighlightColor','k', ...
                'Visible','off');
            
            % key point: set the (x,y) start to (1,1) for hTipText, as it
            % doesn't get set/touched anywhere else.  However dx and dy
            % don't matter.
            obj.hTipText = uicontrol( ...
                'Parent',obj.hTipPanel, ...
                'Tag','ToolTipText', ...
                'HandleVisibility','off', ...
                'Style','text', ...
                'BackgroundColor',obj.BackgroundColor, ...
                'ForegroundColor',obj.ForegroundColor, ...
                'HorizontalAlignment','left', ...
                'FontSize',obj.FontSize, ...
                'Units','pixels', ...
                'Position',[1 1 1 1], ...
                'Visible','on');
            
            updateFontSize(obj,false);
            updateString(obj); % formats the string
        end
        
        function initListeners(obj)
            lis.FigDestroy = addlistener(obj.Parent,'ObjectBeingDestroyed',@(~,~)removeFromFigure(obj));
            lis.Resize = addlistener(obj.Parent,'SizeChanged',@(~,~)resize(obj));
            lis.MouseMotion = addlistener(obj.Parent, ...
                'WindowMouseMotion',@(~,ev)resetEvent(obj,ev,true));
            lis.MousePress = addlistener(obj.Parent, ...
                'WindowMousePress',@(~,ev)resetEvent(obj,ev,false));
            lis.MouseRelease = addlistener(obj.Parent, ...
                'WindowMouseRelease',@(~,ev)resetEvent(obj,ev,false));
            obj.hListeners = lis;
        end
        
        function deleteListeners(p)
            % Delete all listeners
            try %#ok<TRYNC>
                % can't use "if isvalid(p)", since we're likely within a
                % delete() call and isvalid() is false by definition.
                p.hListeners = internal.polariCommon.deleteListenerStruct(p.hListeners);
            end
        end
        
        function resize(obj)
            % Resize differs for pointer vs top/bottom
            
            if strcmpi(obj.Location,'pointer')
                updatePointerOnScreenPos(obj);
            else
                % Banners: 'top', 'bottom'
                obj.pExtent = [];
                obj.hTipText.String = obj.String;
                
                updateBannerWidgetPos(obj);
                updateWidgetEllipsis(obj);
            end
        end
        
        function updateColor(obj)
            obj.hTipText.ForegroundColor = obj.ForegroundColor;
            obj.hTipText.BackgroundColor = obj.BackgroundColor;
            obj.hTipPanel.BackgroundColor = obj.BackgroundColor;
        end
        
        function ext = setExtent(obj)
            % Returns ext = [dx dy] ... NOT [x y dx dy].
            %
            % This is an expensive operation in terms of execution time due
            % to the time for extent retrieval by HG.
            
            % Cell-string and char matrix need fixups for extent. Use
            % processed string, which is in hTipText.String, which may have
            % turned multi-line into single-line
            
            ht = obj.hTipText;
            s = ht.String; % updated string is here
            if iscellstr(s)
                % Maximum dx, sum of dy
                dx = 0;
                dy = 0;
                Nlines = numel(s); % one line of text per entry
                for i = 1:Nlines
                    ht.String = s{i};
                    e_i = ht.Extent;
                    dx = max(dx,e_i(3));
                    if ismac
                        dy = dy + e_i(4) - 3;
                    else
                        % Correction in dy is 6 pixels per row
                        % (3 pix above, 3 pix below)
                        dy = dy + e_i(4) - 6;
                    end
                end
                ext = [dx dy];
                ht.String = s; % restore
            else
                Nlines = size(s,1); % one line per row of matrix
                if Nlines == 1
                    % Simple one-row string
                    ext = ht.Extent(3:4) - [0 3];
                else
                    % Matrix - maximum dx, sum of dy
                    dx = 0;
                    dy = 0;
                    for i = 1:Nlines
                        ht.String = strtrim(s(i,:));
                        e_i = ht.Extent;
                        dx = max(dx,e_i(3));
                        if ismac
                            dy = dy + e_i(4) - 3;
                        else
                            dy = dy + e_i(4) - 6;
                        end
                    end
                    ext = [dx dy];
                    ht.String = s; % restore
                end
            end
            obj.pExtent = ext;
        end
        
        function ext = getExtent(obj)
            % Returns [dx dy], NOT [x y dx dy]
            ext = obj.pExtent;
            if isempty(ext)
                ext = setExtent(obj);
            end
        end
        
        function orderTipAboveOtherChildren(obj)
            % Keep tooltip display on top of all other graphical children.
            
            p = obj.Parent;
            
            % Get all graphical children of parent, hidden or not
            shh = 'showhiddenhandles';
            shh_o = get(0,shh);
            set(0,shh,'on')
            ch = p.Children;
            set(0,shh,shh_o);
            
            % Find tooltip panel in child list
            me = obj.hTipPanel;
            sel = ch == me;
            idx = find(sel);
            assert(~isempty(idx));
            
            % Stack tooltip panel above all others
            ch(idx) = [];         % remove me from here
            p.Children = [me;ch]; % put me on top
        end
        
        function updateFontSize(obj,updateExtent)
            
            h = obj.hTipText;
            h.FontSize = obj.FontSize;
            if nargin < 2 || updateExtent
                % We need to update text extent due to change in font size,
                % and we also need to update the position of the text
                % within hTipPanel due to the change in extent.
                %
                % updateString() does both of these:
                updateString(obj);
                
                % Position of hTipPanel might need to change based on new
                % size of panel (due to new size of font):
                resize(obj);
            end
        end
        
        function updateString(obj)
            % Update widget string
            
            % If Location is 'top' or 'bottom', make tooltip one line, even
            % when a string matrix or cell-string.
            is_ptr = strcmpi(obj.Location,'pointer');
            one_line = ~is_ptr;
            
            % Prepend space to make it look good
            str = obj.String;
            if iscellstr(str)
                if one_line
                    % Banner style requires single line
                    %
                    % Concatenate text lines with spaces
                    Nlines = numel(str);
                    str = [str(:)';repmat({' '},1,Nlines)];
                    str = cat(2,str{:});
                    str = str(1:end-1); % truncate trailing space
                end
            elseif ~isvector(str) % char matrix
                if one_line
                    str = [str repmat(' ',size(str,1),1)];
                    str = str';
                    str = str(:)';
                    str = str(1:end-1); % truncate trailing space
                end
            end
            h = obj.hTipText;
            h.String = str; % widget, not public
            
            % Force extent update
            ext = setExtent(obj); % force extent update, [dx dy]
            
            if is_ptr
                % Update tooltip size
                
                % Update text widget size (dx,dy)
                h.Position(3:4) = ext+1;
                
                % Update parent panel
                % - needs +2 pixels for border line
                obj.hTipPanel.Position(3:4) = ext+3;
                
                updatePointerOnScreenPos(obj);
            else
                % Banners ('top','bottom')
                updateWidgetEllipsis(obj);
            end
        end
        
        function updateBannerWidgetPos(obj)
            % Adjust banner ('top' or 'bottom' Location) height
            
            ext = getExtent(obj);  % [dx dy]
            h = ext(2);
            
            % Get figure size in pixels
            p = obj.Parent;
            orig = p.Units;
            p.Units = 'pixels';
            ppos = p.Position;
            p.Units = orig;
            
            % Text gets offset automatically past border of parent panel,
            % so (1,1) is up and right of lower-left border line.
            obj.pLoc = obj.Location;
            switch lower(obj.Location)
                case 'top'
                    obj.hTipPanel.Position = [1 ppos(4)-(h+2)+1 ppos(3)   h+2];
                    obj.hTipText.Position  = [1 1               ppos(3)-2 h];
                case 'bottom'
                    obj.hTipPanel.Position = [1 1 ppos(3)   h+2];
                    obj.hTipText.Position  = [1 1 ppos(3)-2 h];
            end
        end
        
        function updatePointerOnScreenPos(obj)
            % Move panel to be at pointer location and modify to fit
            % on-screen as needed.
            %
            % Does NOT resize for text extent.
            
            % Get figure size in pixels
            p = obj.Parent;
            orig = p.Units;
            p.Units = 'pixels';
            ppos = p.Position;
            p.Units = orig;
            
            % Does panel bottom extend past figure?
            % If so, put it to left of pointer
            h = obj.hTipPanel;
            hpos = h.Position;
            [ptr_dy,ptr_dx] = size(get(p,'PointerShapeCData'));
            fixup = false;
            if hpos(2) <= 0
                %twoBorders = 2;
                h.Position(2) = max(1,hpos(2) + ptr_dy);
                h.Position(1) = hpos(1) + ptr_dx;
                fixup = true;
            end
            
            % Does right-end of widget extend past figure?
            hpos = h.Position;
            h_right = hpos(1)+hpos(3);       % widget right coord
            hpast_right = h_right - ppos(3); % pixels past fig right coord
            if hpast_right > 0
                % Update widget
                if fixup
                    h.Position(1) = hpos(1) - ptr_dx-2 - hpos(3);
                else
                    h.Position(1) = hpos(1) - hpast_right;
                end
            end
        end
        
        function updateWidgetEllipsis(obj)
            % Analyze widget width and string length.
            % Shrink string and append '...' to make it fit
            
            % Reset string in case it was shortened previously
            h = obj.hTipText;
            
            % check text width vs widget width
            ext = getExtent(obj); % [dx dy]
            dx_text = ext(1);
            dx_widget = h.Position(3);
            if dx_text >= dx_widget
                str = h.String;
                Ns = numel(str);
                if Ns > 6
                    % remove 3 chars to be replaced by '...', may not shorten
                    % it but now we can add the suppression chars
                    str = [str(1:end-3) '...'];
                    while numel(str) > 5 % '...' plus 3 chars to remove
                        % remove 3 chars each time
                        str(end-5:end-3) = '';
                        h.String = str;  % update widget
                        ext = setExtent(obj);
                        if ext(1) < dx_widget
                            return
                        end
                    end
                end
                
                % Can't shrink further - clear the text:
                h.String = ''; % clear widget
            end
        end
    end
end
