classdef FigureScreenReaderManager
    
    % Class to synthesize aria-labels for screen reader support for
    % embedded figures in MATLAB Online and alt-text for mlx doc examples
    
    % Copyright 2020-2021 The MathWorks, Inc.
    
    methods (Static)
        
        function updateFigureAriaLabel(f)
            % Create aria-labels for figure in MATLAB Online
            % TODO: Refactor this to generate content more closely matching
            % the alt-text generated for mlx-examples
            % (updateFigureAltTextForDoc)
            import matlab.graphics.internal.FigureScreenReaderManager;
            
            % This method is called from matlab.graphics.internal.drawnow.callback
            % so we need to protect against changes in f made before
            % the drawnow is processed
            if ~matlab.graphics.internal.FigureScreenReaderManager.isSupportedFigure(f)
                return
            end
            
            if ~isprop(f,'AriaLabelInMOL_')
                pAriaLabelInMOL_ = addprop(f,'AriaLabelInMOL_');
                pAriaLabelInMOL_.Transient = true;
                pAriaLabelInMOL_.Hidden = true;
            end
            if ~isprop(f,'AriaLiveInMOL_')
                pAriaLiveInMOL_ = addprop(f,'AriaLiveInMOL_');
                pAriaLiveInMOL_.Transient = true;
                pAriaLiveInMOL_.Hidden = true;
            end           
            % First describe the figure and its immediate children
            ariaLabel = FigureScreenReaderManager.figureDescription(f); 
            % Then describe gca (if there is one)
            if ~isempty(f.CurrentAxes)
                ariaLabel = ariaLabel + " " + FigureScreenReaderManager.gcaDescription(f.CurrentAxes);
            end
           
            ariaLabelChars = char(ariaLabel);
            if ~isequal(f.AriaLabelInMOL_,ariaLabel)
                FigureScreenReaderManager.updateAriaLabel(f, ariaLabel);
                f.AriaLabelInMOL_ = ariaLabel;
            end

            if isequal(f.AriaLiveInMOL_,ariaLabel)
                % If the last aria-live string matches the ariaLabel,
                % toggle a terminating '.' character to force the ariaLive
                % text to change even if the aria label is the same. This
                % is needed so that the aria-live is annonced when the
                % figure content has changed but the arialabel is the
                % same, e.g. repeated calls to plot(randn(10))
                if numel(ariaLabelChars)>0 && ariaLabelChars(end)~='.'
                    ariaLabelChars = [ariaLabelChars '.'];
                                end
            end
            f.AriaLiveInMOL_ = string(ariaLabelChars);
            matlab.graphics.internal.FigureScreenReaderManager.updateFigureAriaLiveTextContent(f, ariaLabelChars);
        end
        
        function updateFigureAriaLiveTextContent(f, textContent)
            import matlab.graphics.internal.FigureScreenReaderManager;
            
            if ~matlab.graphics.internal.FigureScreenReaderManager.isSupportedFigure(f)
                return
            end
            
            if ~isprop(f,'AriaLiveTextContent')
                pAriaLiveTextContent = addprop(f,'AriaLiveTextContent');
                pAriaLiveTextContent.Transient = true;
                pAriaLiveTextContent.Hidden = true;
            end
            
            % Store the text content as a hidden property on the figure
            % This is helpful for testing
            f.AriaLiveTextContent = textContent;
            
            FigureScreenReaderManager.updateAriaLiveTextContent(f, textContent);
        
        end
        
        function altTextJSON = updateFigureAltTextForDoc(f)
            % Creates the alt-text for mlx doc examples.See the spec:
            % https://mathworks.sharepoint.com/sites/ltc/matlab/dt/Shared%20Documents/Accessibility/altTextGrammar.docx?d=w5de7331931ff46f28d3b276db86618dd
            
            import matlab.graphics.internal.FigureScreenReaderManager;
            % First describe the figure and its immediate children then 
            % describe each of the axes figure children one by one (if any
            % are present)
            try
                allAxes = getVisibleAndImageAxes(f);
                if ~FigureScreenReaderManager.validateFigureContent(f,allAxes)
                    altTextJSON = mls.internal.toJSON(struct('String',"MATLAB figure",'Tex',[], 'LaTex',[]));
                    return
                end
                altText = FigureScreenReaderManager.figureDescriptionForDoc(f, allAxes);
                [axesDescription, texStrings, latexStrings] = ...
                    FigureScreenReaderManager.axesDescriptionForDoc(allAxes);
                altText = altText + axesDescription;
                altText = altText + FigureScreenReaderManager.chartDescriptionForDoc(f);
                altText = removeInvalidXMLChars(altText);
                altTextJSON = mls.internal.toJSON(struct('String',altText,'Tex',texStrings,'LaTex',latexStrings));       
            catch e %#ok<NASGU>
                % If there are unexpected errors, generate default text to
                % avoid failing example builds
                altTextJSON = mls.internal.toJSON(struct('String',"MATLAB figure",'Tex',[],'LaTex',[]));
            end            
        end
           
        function state = isSupportedFigure(f)
            % returns true iff the figure supports aria screen reader text,
            % i.e. a web figure in MATLAB Online
            
            import matlab.graphics.internal.FigureScreenReaderManager;
            state =  isvalid(f) && f.BeingDeleted == "off" && f.Visible=="on" && ...
                (FigureScreenReaderManager.isTesting ||  isWebFigureType(f,'EmbeddedMorphableFigure'));
        end
    end
    
    methods (Static, Hidden)
        function isTestingValue = isTesting(state)
            persistent testingState;
            if nargin>=1
                testingState = state;
            end
            isTestingValue = ~isempty(testingState) && testingState;     
        end
    end
    
    methods (Static, Access = private)
        
        function figureDescription = figureDescription(f)
            % Describes the figure and its children for the MATLAB Online
            % aria-label
            
            % 1. figure [number, name] with no handle-visible children.
            % 2. figure [number, name] with children of types: (list of types). 
            % 3. figure [number, name] with (num) axes.
            % 4. figure [number, name].
            
            import matlab.graphics.internal.FigureScreenReaderManager;
            currentAxes = f.CurrentAxes;
            figureChildren = FigureScreenReaderManager.getAnnouncedNonAxesFigureDescendants(f);
            
            if strlength(deblank(f.Name)) == 0
                if f.IntegerHandle % Numbered no-name figure
                    if isempty(currentAxes)
                        % If there is no current axes just list the
                        % figure children (if any)
                        if isempty(figureChildren)
                            % 1. figure [number] with no handle-visible children
                            figureDescription =  getString(message('MATLAB:graphics:figurescreenreader:FigureDescriptionNumNoChildren', f.Number));
                        else
                            % 2. figure [number] with children of types: (list of types). 
                            figureDescription =  getString(message('MATLAB:graphics:figurescreenreader:FigureDescriptionNumWithChildren', ...
                                f.Number,join(unique(string(get(figureChildren,{'Type'})),'stable'),', ')));
                        end
                    else
                        % If there is a current axes, rely on
                        % gcaDescription() to
                        % describe it. If there are more than one axes, describe
                        % the axes count so that users can recognize
                        % subplots
                        allAxes = findobj(f.Children,'-isa','matlab.graphics.axis.AbstractAxes');
                        if length(allAxes)>=2 
                            %3. figure [number] with (num) axes
                            figureDescription = getString(message('MATLAB:graphics:figurescreenreader:FigureDescriptionNumWithMultipleAxes',f.Number,length(allAxes)));
                        else
                            %4. figure [number]
                            figureDescription = getString(message('MATLAB:graphics:figurescreenreader:FigureDescriptionNumWithSingleAxes',f.Number));
                        end
                    end
                else % Non-numbered no-name figure
                    if isempty(currentAxes)
                        % If there is no current axes just list the
                        % figure children (if any)
                        if isempty(figureChildren)
                            % 1. figure with no handle-visible children
                            figureDescription =  getString(message('MATLAB:graphics:figurescreenreader:FigureDescriptionNoNumNoChildren'));
                        else
                            % 2. figure with children of types: (list of types). 
                            figureDescription =  getString(message('MATLAB:graphics:figurescreenreader:FigureDescriptionNoNumWithChildren', ...
                                join(unique(string(get(figureChildren,{'Type'})),'stable'),', ')));
                        end
                    else
                        % If there is a current axes, rely on gcaDescription 
                        % describe it. If there are more than one, describe
                        % the axes count so that users can recognize
                        % subplots
                        allAxes = findobj(f.Children,'-isa','matlab.graphics.axis.AbstractAxes');
                        if length(allAxes)>=2
                            %3. figure with (num) axes
                            figureDescription = getString(message('MATLAB:graphics:figurescreenreader:FigureDescriptionNoNumWithMultipleAxes',length(allAxes)));
                        else
                            %4. figure.
                            figureDescription = getString(message('MATLAB:graphics:figurescreenreader:FigureDescriptionNoNumWithSingleAxes'));
                        end
                    end                    
                end
                
            else % Named figure
                if isempty(currentAxes)
                    % If there is no current axes just list the
                    % figure children (if any)
                    if isempty(figureChildren)
                        % 1. figure [name] with no handle-visible children.                      
                        figureDescription =  getString(message('MATLAB:graphics:figurescreenreader:FigureDescriptionNamedNoChildren',f.Name));
                    else     
                        % 2. figure [name] with children of types: (list of types). 
                        figureDescription =  getString(message('MATLAB:graphics:figurescreenreader:FigureDescriptionNamedWithChildren', ...
                            f.Name,join(unique(string(get(figureChildren,{'Type'})),'stable'),', ')));
                    end
                else
                    % If there is a current axes, rely on gcaDescription
                    % describe it. If there are more than one, describe
                    % the axes count so that users can recognize
                    % subplots
                    allAxes = findobj(f.Children,'-isa','matlab.graphics.axis.AbstractAxes');
                    if length(allAxes)>=2
                        %3. figure [name] with (num) axes
                        figureDescription = getString(message('MATLAB:graphics:figurescreenreader:FigureDescriptionNamedWithMultipleAxes',f.Name,length(allAxes)));
                    else
                        %4. figure [name]
                        figureDescription = getString(message('MATLAB:graphics:figurescreenreader:FigureDescriptionNamedWithSingleAxes',f.Name));
                    end
                end
            end
            figureDescription = string(figureDescription);
           
        end    
        
        function figureDescription = figureDescriptionForDoc(f, allAxes)
            % Describes the figure and its descendants in English for mlx example since 
            % doc xml is separately translated. See "Announced Figure Descendants" 
            % in the spec

            import matlab.graphics.internal.FigureScreenReaderManager;
            lc_en =  matlab.internal.i18n.locale('en');
            
            nonAxesChildren = FigureScreenReaderManager.getAnnouncedNonAxesFigureDescendants(f, "matlab.graphics.axis.AbstractAxes");
            if strlength(deblank(f.Name)) == 0                
                if isempty(nonAxesChildren) % e.g., No non-axes children
                    if numel(allAxes)==1 % e.g., Figure containing an axes.
                        figureDescription = getString(message('MATLAB:graphics:figurescreenreader:UnnamedFigureSingleAxes'),lc_en);
                    elseif numel(allAxes)==0 % e.g.,  Empty figure.             
                        figureDescription = "";       
                    else % e.g., Figure containing n>=2 axes.     
                        figureDescription =  getString(message('MATLAB:graphics:figurescreenreader:UnnamedFigureMultipleAxes',numel(allAxes)),lc_en);
                    end
                elseif numel(nonAxesChildren)==1
                    if numel(allAxes)==1 % e.g., Figure containing an axes and another object of type: foobar.
                        figureDescription = getString(message('MATLAB:graphics:figurescreenreader:UnnamedFigureSingleNonAxesChildrenSingleAxes', ...
                            nonAxesChildren.Type),lc_en);
                    elseif numel(allAxes)==0 % e.g., Figure containing an object of type foobar
                        figureDescription = getString(message('MATLAB:graphics:figurescreenreader:UnnamedFigureNoAxesWithOneOtherChild', ...
                            nonAxesChildren.Type),lc_en);
                    else % e.g., Figure containing 3 axes and another object of type foobar
                        figureDescription = getString(message('MATLAB:graphics:figurescreenreader:UnnamedFigureSingleNonAxesChildMultipleAxes', ...
                            numel(allAxes),nonAxesChildren.Type),lc_en);
                    end                    
                else                  
                    if numel(allAxes)==1 % e.g., Figure containing an axes and objects of type foobar
                        figureDescription = getString(message('MATLAB:graphics:figurescreenreader:UnnamedFigureNonAxesChildrenSingleAxes', ...
                            join(unique(string(get(nonAxesChildren,{'Type'})),'stable'),', ')),lc_en);
                    elseif numel(allAxes)==0 % e.g., Figure containing objects of type: foobar
                        figureDescription = getString(message('MATLAB:graphics:figurescreenreader:UnnamedFigureNoAxesWithOtherChildren', ...
                            join(unique(string(get(nonAxesChildren,{'Type'})),'stable'),', ')),lc_en);
                    else % e.g., Figure containing {num} axes and objects of type1, type2, ...
                        figureDescription = getString(message('MATLAB:graphics:figurescreenreader:UnnamedFigureNonAxesChildrenMultipleAxes', ...
                            numel(allAxes),join(unique(string(get(nonAxesChildren,{'Type'})),'stable'),', ')),lc_en);
                    end
                end           
            else % Named figure
                 if isempty(nonAxesChildren)
                    if numel(allAxes)==1 % e.g., Figure figName contains an axes.
                        figureDescription =  getString(message('MATLAB:graphics:figurescreenreader:NamedFigureSingleAxes',f.Name),lc_en);
                    elseif numel(allAxes)==0
                        figureDescription =  ""; 
                    else % e.g., Figure figName contains 2 axes.
                        figureDescription =  getString(message('MATLAB:graphics:figurescreenreader:NamedFigureMultipleAxes',f.Name,numel(allAxes)),lc_en);
                    end
                elseif numel(nonAxesChildren)==1
                    if numel(allAxes)==1 % e.g., Figure figName contains an axes and an object of type uipanel
                        figureDescription =  getString(message('MATLAB:graphics:figurescreenreader:NamedFigureSingleNonAxesChildSingleAxes', ...
                            f.Name, nonAxesChildren.Type),lc_en);
                    elseif numel(allAxes)==0 % e.g., Figure figName contains an object of type uipanel
                        figureDescription =  getString(message('MATLAB:graphics:figurescreenreader:NamedFigureNoAxesWithOneOtherChild', ...
                            f.Name,nonAxesChildren.Type),lc_en);
                    else % e.g., Figure figName contains 2 axes and an object of type uipanel
                        figureDescription =  getString(message('MATLAB:graphics:figurescreenreader:NamedFigureSingleNonAxesChildMultipleAxes', ...
                            f.Name, numel(allAxes), nonAxesChildren.Type),lc_en);
                    end                    
                else                  
                    if numel(allAxes)==1 % e.g., Figure containing an axes and objects of type uipanel
                        figureDescription =  getString(message('MATLAB:graphics:figurescreenreader:NamedFigureNonAxesChildrenSingleAxes', ...
                            f.Name, join(unique(string(get(nonAxesChildren,{'Type'})),'stable'),', ')),lc_en);
                    elseif numel(allAxes)==0 % e.g., Figure containing objects of type uipanel
                        figureDescription =  getString(message('MATLAB:graphics:figurescreenreader:NamedFigureNoAxesWithOtherChildren', ...
                            f.Name,join(unique(string(get(nonAxesChildren,{'Type'})),'stable'),', ')),lc_en);
                    else % e.g., Figure containing 2 axes and objects of type uipanel
                        figureDescription =  getString(message('MATLAB:graphics:figurescreenreader:NamedFigureNonAxesChildrenMultipleAxes', ...
                            f.Name, numel(allAxes),join(unique(string(get(nonAxesChildren,{'Type'})),'stable'),', ')),lc_en);
                    end
                end
            end
            figureDescription = string(figureDescription);           
        end
        
        function [axesDescription, texStrings, latexStrings] = axesDescriptionForDoc(allAxes)
             % Describes the content of visible axes for mlx example in English since
             % doc xml is separately translated. See "Description of the Visible Axes"
             % in the spec
            
             texStrings = string.empty;
             latexStrings = string.empty;
             import matlab.graphics.internal.FigureScreenReaderManager;
             lc_en =  matlab.internal.i18n.locale('en');
             if numel(allAxes)==0 % No axes
                 axesDescription = "";
             elseif numel(allAxes)==1 % Single axes
                 % Find all objects which represent visible data (exlcudes 
                 % grouping objects like hgroup, hgtransform inheriting
                 % from SceneNodeGroup)
                 visibleObjects = findobj(allAxes,'-isa','matlab.graphics.primitive.Data',...
                     '-and','-not','-isa','matlab.graphics.mixin.SceneNodeGroup',...
                     '-and','-property','Type','-and','Visible','on');
                 visibleObjects = visibleObjects(end:-1:1);
                 titleString = getTitleString(allAxes);
                 if strlength(titleString) > 0 % Titled axes 
                     if isempty(visibleObjects) % e.g., The axes with title axesTitle is empty.
                         axesDescription = " " + getString(message('MATLAB:graphics:figurescreenreader:TitledSingleAxesDescriptionEmpty',titleString),lc_en);
                     elseif numel(visibleObjects)==1 % e.g., The axes with title axesTitle contains an object of type bar
                         axesDescription = " " + getString(message('MATLAB:graphics:figurescreenreader:TitledSingleAxesDescriptionSingleObject',titleString,visibleObjects.Type),lc_en);
                     else % e.g., The axes with title axesTitle contains object of type line, bar
                         visibleObjectTypes = join(unique(string(get(visibleObjects,{'Type'})),'stable'),', ');
                         axesDescription = " " + getString(message('MATLAB:graphics:figurescreenreader:TitledSingleAxesDescriptionNonEmpty',titleString,numel(visibleObjects),visibleObjectTypes),lc_en);
                     end     
                     if ~isempty(titleString) 
                         if strcmp(allAxes.Title.Interpreter,"tex") 
                             texStrings = [titleString];
                         elseif strcmp(allAxes.Title.Interpreter,"latex")
                             latexStrings = [titleString];
                         end
                     end
                 else
                     if isempty(visibleObjects) % e.g., The axes is empty.
                         axesDescription = " " + getString(message('MATLAB:graphics:figurescreenreader:UntitledSingleAxesDescriptionEmpty'),lc_en);
                     elseif numel(visibleObjects)==1 % e.g., The axes contains an object of type line              
                         axesDescription = " " + getString(message('MATLAB:graphics:figurescreenreader:UntitledSingleAxesDescriptionSingleObject',visibleObjects.Type),lc_en);
                     else % e.g., The axes contains objects of type bar, line
                         visibleObjectTypes = join(unique(string(get(visibleObjects,{'Type'})),'stable'),', ');
                         axesDescription = " " + getString(message('MATLAB:graphics:figurescreenreader:UntitledSingleAxesDescriptionNonEmpty',numel(visibleObjects),visibleObjectTypes),lc_en);
                     end
                 end
                 namedAxesContent = FigureScreenReaderManager.getNamedAxesContent(allAxes);
                 if strlength(namedAxesContent)>=1
                     axesDescription = axesDescription+" "+namedAxesContent;
                 end
                 
             elseif numel(allAxes)>1 % Multiple axes         
                 axesDescription = "";
                 
                 % Describe each axes one-by-one
                 for k=1:numel(allAxes)
                     % Find all objects which represent visible data (exlcudes
                     % grouping objects like hgroup, hgtransform inheriting
                     % from SceneNodeGroup)
                     visibleObjects = findobj(allAxes(k).Children,'-isa','matlab.graphics.primitive.Data',...
                     '-and','-not','-isa','matlab.graphics.mixin.SceneNodeGroup',...
                     '-and','-property','Type','-and','Visible','on');
                     visibleObjects = visibleObjects(end:-1:1);
                     titleString = getTitleString(allAxes(k));
                     if strlength(titleString) > 0 % Titled axes
                         if isempty(visibleObjects) % e.g., Axes 1 with title axesTitle is empty.
                             axesDescription = axesDescription + " " + getString(message('MATLAB:graphics:figurescreenreader:TitledMultipleAxesDescriptionEmpty',...
                                 k,titleString),lc_en);
                         elseif numel(visibleObjects)==1 % e.g., Axes 1 with title axesTitle contains an object of type line.
                             axesDescription = axesDescription + " " + getString(message('MATLAB:graphics:figurescreenreader:TitledMultipleAxesDescriptionSingleObject',...
                                 k,titleString,visibleObjects.Type),lc_en);
                         else % e.g., Axes 1 with title axesTitle contains objects of type line, bar.
                             visibleObjectTypes = join(unique(string(get(visibleObjects,{'Type'})),'stable'),', ');
                             axesDescription = axesDescription + " " + getString(message('MATLAB:graphics:figurescreenreader:TitledMultipleAxesDescriptionNonEmpty',...
                                 k,titleString,numel(visibleObjects),visibleObjectTypes),lc_en);
                         end
                         if ~isempty(titleString) 
                             if strcmp(allAxes(k).Title.Interpreter,"tex")
                                 texStrings(end+1) = titleString; %#ok<AGROW> 
                             elseif strcmp(allAxes(k).Title.Interpreter,"latex")
                                 latexStrings(end+1) = titleString; %#ok<AGROW> 
                             end
                         end
                     else % Untitled axes
                         if isempty(visibleObjects) % e.g., Axes 1 is empty.
                             axesDescription = axesDescription + " " + getString(message('MATLAB:graphics:figurescreenreader:UntitledMultipleAxesDescriptionEmpty',...
                                 k),lc_en);
                         elseif numel(visibleObjects)==1 % e.g., Axes 1 contains an object of type line
                             axesDescription = axesDescription + " " + getString(message('MATLAB:graphics:figurescreenreader:UntitledMultipleAxesDescriptionSingleObject',...
                                 k,visibleObjects.Type),lc_en);
                         else % e.g., Axes 1 contains objects of type line, bar.
                             visibleObjectTypes = join(unique(string(get(visibleObjects,{'Type'})),'stable'),', ');
                             axesDescription = axesDescription + " " + getString(message('MATLAB:graphics:figurescreenreader:UntitledMultipleAxesDescriptionNonEmpty',...
                                 k,numel(visibleObjects),visibleObjectTypes),lc_en);
                         end
                     end
                     namedAxesContent = FigureScreenReaderManager.getNamedAxesContent(allAxes(k));
                     if strlength(namedAxesContent)>=1
                         axesDescription = axesDescription+" "+namedAxesContent;
                     end
                 end
             end
        end
        
        function chartDescription = chartDescriptionForDoc(f)
            % Lists the type and titles of all named charts for mlx examples.
            % See "Description of the Titled Charts " in the spec
            lc_en =  matlab.internal.i18n.locale('en');
            allCharts = findobj(f.Children,'-isa','matlab.graphics.chart.Chart','Visible','on','-function',@(h) isprop(h,'Title') && ~isempty(h.Title));
            allCharts = allCharts(end:-1:1);
            if numel(allCharts)==0
                chartDescription = "";
            else
                % e.g., The chart of type geobubble has title population.
                chartDescription = " " + getString(message('MATLAB:graphics:figurescreenreader:TitledSingleChart', allCharts(1).Type, allCharts(1).Title),lc_en);
                for k=2:numel(allCharts)
                    chartDescription = chartDescription + " " + getString(message('MATLAB:graphics:figurescreenreader:TitledSingleChart', allCharts(k).Type, allCharts(k).Title),lc_en);
                end
            end
        end
        
        function gcaDescription = gcaDescription(ax)
            % Describes the current axes for MOL aria labels
            
            if ~isa(ax,'matlab.graphics.axis.Axes')
                % Deal with charts and non-cartesian axes
                if isprop(ax,'Type')
                    gcaDescription = getString(message('MATLAB:graphics:figurescreenreader:ChartWithType',ax.Type));
                else
                    gcaDescription = "";
                end
                return
            end
            visibleObjects = findobj(ax.Children,'flat','Visible','on','-property','Type');
            if ~isempty(visibleObjects)
                visibleObjectTypes = join(unique(string(get(visibleObjects,{'Type'})),'stable'),', ');
            end
            axesTitle = getTitleString(ax);
            if strlength(axesTitle) == 0 
                if isempty(visibleObjects)
                    gcaDescription = getString(message('MATLAB:graphics:figurescreenreader:UntitledCurrentAxesDescriptionEmpty')); 
                else
                gcaDescription = getString(message('MATLAB:graphics:figurescreenreader:UntitledCurrentAxesDescriptionNonEmpty',...
                    numel(visibleObjects), visibleObjectTypes));
                end
            else
                if isempty(visibleObjects)
                    gcaDescription = getString(message('MATLAB:graphics:figurescreenreader:TitledCurrentAxesDescriptionEmpty',axesTitle)); 
                else
                gcaDescription = getString(message('MATLAB:graphics:figurescreenreader:TitledCurrentAxesDescriptionNonEmpty',...
                    axesTitle,numel(visibleObjects), visibleObjectTypes));
                end
            end     
        end
        
        function updateAriaLabel(hFig, ariaLabel)
            % Publish aria-labels back to the MOL client
            
            channel = "/embeddedfigure/ServerToClient"+ matlab.ui.internal.FigureServices.getUniqueChannelId(hFig);

            ariaUpdateFcn = @() message.publish(channel , struct('eventType', 'AriaLabel', 'value', ariaLabel));
            % Use dispatchWhenViewIsReady to make sure that the 
            % embedded figure subscriptions are ready before publishing the
            % message
            matlab.ui.internal.dialog.DialogHelper.dispatchWhenViewIsReady(hFig, ariaUpdateFcn);
        end
        
        function updateAriaLiveTextContent(hFig, textContent)
            % Publish the text contents for the aria-live region div
            
            channel = "/embeddedfigure/ServerToClient"+ matlab.ui.internal.FigureServices.getUniqueChannelId(hFig);

            ariaUpdateFcn = @() message.publish(channel , struct('eventType', 'AriaLiveTextContent', 'value', textContent));
            % Use dispatchWhenViewIsReady to make sure that the 
            % embedded figure subscriptions are ready before publishing the
            % message
            matlab.ui.internal.dialog.DialogHelper.dispatchWhenViewIsReady(hFig, ariaUpdateFcn);
        end
        
        function hDescendants = getAnnouncedNonAxesFigureDescendants(f,additionalExcludedClasses)
            % Return the visible (non axes) figure descendants for mlx
            % examples. This should exclude layout classes and peers
            % of axes such as legend and colorbar. It includes visible
            % chart descendants and any other direct children of the figure
            excludedTypes = ["legend","colorbar"];
            excludedClasses = "matlab.graphics.layout.Layout";
            
            % Add any additional excluded classes (e.g. to exclude axes
            % when finding charts)
            if nargin>=2 && ~isempty(additionalExcludedClasses)
                excludedClasses = [excludedClasses,additionalExcludedClasses(:)'];
            end
            
            hChildren = findobj(f.Children,'flat','-property','Visible','Visible','on','-function',...
                @(h) isprop(h,'Type') && ~ismember(h.Type,excludedTypes) && ~any(arrayfun(@(mixin) isa(h,mixin),excludedClasses)));
            hCharts = findobj(f.Children,'-isa','matlab.graphics.chart.Chart','-property','Visible','Visible','on','-function',...
                @(h) isprop(h,'Type'));
            hDescendants = [hChildren(:)',hCharts(:)'];
            hDescendants = unique(hDescendants(end:-1:1),'stable');
        end
        
        function namedAxesContent = getNamedAxesContent(ax)
            % Describe the conents of an axes for mlx examples for cases
            % where axes contain objects with DisplayNames. See section
            % "Description of the Visible Axes" of the spec for details.
            namedAxesContent = "";
            if ~isempty(ax.Children)
                namedChildren = findobj(ax,'-depth',1,'-property','DisplayName',...
                    '-function',@(h) ~isempty(h.DisplayName) && ...
                    isprop(h,'DisplayNameMode') && h.DisplayNameMode=="manual");
                namedChildren = namedChildren(end:-1:1);
                if numel(namedChildren)>1
                    % These objects respresent: foo, bar.
                    namedAxesContent = getString(message('MATLAB:graphics:figurescreenreader:AxesNamedChildren', ...
                        join(unique(string(get(namedChildren,{'DisplayName'})),'stable'),', ')));
                elseif numel(namedChildren)==1
                    % This object respresent1: foo
                    namedAxesContent = getString(message('MATLAB:graphics:figurescreenreader:AxesNamedSingleChild', ...
                        namedChildren.DisplayName));
                end
            end
        end
        
        function validState = validateFigureContent(f,allAxes) %#ok<INUSL>
            % Single place to put logic to remove all charts that have
            % non-standard behavior that make the standard alt-text logic
            % misleading
            
            % Exclude plotmatrix since it places viisble axes on top of
            % eachother making the axes count misleading
            validState = isempty(intersect(get(allAxes,'Tag'),{'PlotMatrixHistAx','PlotMatrixScatterAx','PlotMatrixBigAx'}));
        end
    end
end


function titleString = getTitleString(ax)
% Find the title of an axes as a single string after removing any newline
% characters (if any)
try
    if isa (ax,'matlab.graphics.axis.Axes') && ~isempty(ax.Title_IS)
        titleString = string(deblank(ax.Title_IS.String));
        if numel(titleString)>1
            % Join multi-line titles to create a single string
            titleString = strjoin(titleString);
        end
    else
        titleString = "";
    end
catch
    titleString = "";
end
end

function visibleAndImageAxes = getVisibleAndImageAxes(f)

    % Find all the visible axes and invisible axes which contain images.
    % Do not include other invisible axes because they may have been
    % intentionally hidden.
    allAxes = findobj(f.Children,'-isa','matlab.graphics.axis.AbstractAxes');
    visibleAxes = findobj(allAxes,'flat','-property','Visible','Visible','on');
    visibleAxes = visibleAxes(end:-1:1);
    imageAxes = findobj(allAxes,'flat','-function',@(ax) ~isempty(findobj(ax.Children,'flat','-isa','matlab.graphics.primitive.Image')));
    imageAxes = imageAxes(end:-1:1);
    visibleAndImageAxes = unique([visibleAxes(:);imageAxes(:)]','stable');
end


function outsstr = removeInvalidXMLChars(instr)

% https://docs.microsoft.com/en-us/previous-versions/troubleshoot/msxml/error-when-xml-contains-low-order-ascii
str = char(instr);
uCode = double(str);
str = str(uCode>=20 | abs(uCode-9)<0.01 | abs(uCode-10)<0.01 | abs(uCode-13)<0.01);
outsstr = string(str);
end