classdef DCTableForm < dialogmgr.DialogContent
    %DCTableForm Table-style dialog layout.
    %  DCTableForm is a subclass of DialogContent providing automatic
    %  table-style layout of controls.  It is used to create
    %  user-interaction dialogs.
    %
    %  A typical use of DCTableForm is the design of an editable table of
    %  labeled edit boxes in a 2-column layout, or a 3-column layout
    %  where units are displayed in the third column.
    %
    %  A dialog using DCTableForm will be written as a new MATLAB class
    %  that subclasses dialogmgr.DCTableForm.  The new class will typically
    %  create user-interaction controls in initTable(), a protected method
    %  of the new class.  The new class can set properties that control the
    %  table layout.  The subclass can also update or validate dialog
    %  content and properties in update(), an optional protected method of
    %  the new class.
    %
    %  Typically, a MATLAB application will interact with dialog content
    %  via public properties of the new dialog, whether to get user-
    %  specified values or to update the dialog with new values.  Use
    %  connectivity commands to establish automatic two-way updates between
    %  controls and public properties of the new class.
    %
    %  To learn more:
    %   <a href="matlab:help dialogmgr.DCTableForm_GettingStarted">Getting Started</a>
    %   <a href="matlab:help dialogmgr.DCTableForm_Controls">Types Of Controls</a>
    %   <a href="matlab:help dialogmgr.DCTableForm_LayoutCommands">Layout Commands</a>
    %   <a href="matlab:help dialogmgr.DCTableForm_LayoutProperties">Layout Properties</a>
    %   <a href="matlab:help dialogmgr.DCTableForm_Connectivity">Connectivity</a>
    %
    % See also dialogmgr.DCTableReadout.

    %   Copyright 2014 The MathWorks, Inc.
    
    % TYPICAL USE
    %
    % Subclass must override:
    %     createTable(dlg) - protected
    % Subclass can optionally override:
    %     update(dlg) - protected
    %
    % It is unusual to implement update(dlg), since class properties are
    % the typical way to interact with the dialog.  Clients generally
    % implement onPropertyChange:
    %
    %        onPropertyChange(myForm, ...
    %            @(h,ev),myUpdateFcn(ant,ev))
    %
    %        onPropertyPreSet(myForm, ...
    %            @(h,ev),myUpdateFcn(ant,ev))
    %        onPropertyPostSet(myForm, ...
    %            @(h,ev),myUpdateFcn(ant,ev))

    properties (Access=protected)
        % Specify width of table columns.
        %
        % Specify one of the following to set the width for all columns in
        % table, or use a cell-vector to specify each column separately.
        %  [minP maxP]
        %    Specifies min and max pixel width for the table column.
        %    Common values for minP and maxP are:
        %      minP =    0: Lower limit of column width
        %      minP =   -1: Column width computed from InitialText
        %      maxP = minP: Disables resizing of column width
        %      maxP =  inf: Enables unlimited maximum column width
        %  'min'
        %    Equivalent to [-1 -1].  Sets column width based on
        %    InitialText and disables column resizing.
        %  'max'
        %    Equivalent to [-1 inf].  Sets minimum column width based on
        %    InitialText and enables column resizing.
        ColumnWidths = 'max'
        
        % True to align decimal-points down text in column.
        %
        % Use a vector to specify decimal alignment independently for each
        % column.  Enabling decimal alignment will reduce performance of
        % updates to text in those columns.
        DecimalAlignment = false
        
        FontName  = get(0,'defaultuicontrolfontname')
        FontSize  = get(0,'defaultuicontrolfontsize')
        FontUnits = get(0,'defaultuicontrolfontunits')
        
        FontSizeMin = 4
        FontSizeMax = 40
        
        % Style for header rows
        HeaderRowsBold = true
        
        % Horizontal alignment of controls in each column.
        %
        % Specify one of the following to set the alignment for all columns
        % of the table, or use a cell-vector to specify columns separately.
        %   'inherit' - alignment is specified on individual controls
        %      'left' - left-justify controls in column
        %    'center' - center controls in column
        %     'right' - right-justify controls in column
        HorizontalAlignment = 'inherit'
        
        % Define number of header rows in table. Header rows are column
        % titles, may be given bold text, and do not participate in decimal
        % alignment.
        %     0: No headers (default)
        %   N>0: First N rows are headers
        NumHeaderRows = 0
        
        InterColumnSpacing = 2  % Between adjacent columns
        InterRowSpacing    = 2  % Between adjacent rows
        InnerBorderSpacing = 2  % Around dialog perimeter within border
    end
    
    properties (Hidden,SetObservable)
        % Enable animation of visibility changes to table controls.
        Animation = true
    end
    
    properties (Hidden)
        % True visualizes flow containers to diagnose if column widths are
        % set appropriately.
        Debug = false
        
        % Cell matrix of uicontrol handles to display in table.
        %
        % The uicontrols referenced in each row and column of Controls are
        % displayed in rows and columns of the table.  Call updateElement
        % if a uicontrol is deleted or replaced after initial display.
        Controls = {}
    end
    
    properties (Access=private)
        Listeners % for resize events
        
        % Cell Matrix of controls, with same size as Controls, containing
        % uicontrols for fractional part of decimal-aligned values.
        % Controls holds the uicontrols for integer parts.
        %
        % empty objects appear in all rows for non-decimal aligned columns.
        %
        % Header rows do not participate in decimal alignment and also
        % contain GraphicsPlaceholder objects.
        ControlsFraction
        
        % Extent of each uicontrol on .Controls, including "fixups" for
        % vertical, horizontal, and/or offsets that align controls with
        % each other.
        %     .horiz: horizontal extent, in pixels
        %      .vert: vertical extent, in pixels
        %   .hoffset: pixel offset that aligns control down table cols
        %   .voffset: pixel offset that aligns control across table rows
        ControlExtents
        ControlFractionExtents
        
        % Additional uicontrol created just for assessing extents of text
        % when needed
        TempTextControl
        
        % Scalar-expanded and normalized versions of property values
        CachedHorizontalAlignment
        CachedDecimalAlignCols

        % Cached widths/heights do not include gutters/borders
        CachedRowHeights
        CachedRowOffsets
        
        CachedColumnWidths         % matrix of Nx2 entries, one per col, [minW maxW], overall
        CachedColumnOffsets        % horiz pixel origin for start of each column
        CachedFractionColumnOffsets
        
        % True if font sizes were cached on each control
        % We do "lazy caching" at time of first use
        CachedOrigFontSizes = false
        
        % Logical matrix with same size as Controls
        % True when control is not a matlab.Graphics.GraphicsPlaceholder
        NotPlaceholderControl

        % Cell-string matrix of Tags of all non-placeholder controls
        ControlTags
        
        % Cached layout info, from latest call to updateControlPositions
        LastLayoutOrigin
        LastLayoutSpans
        
        % 2xN matrix of pixel width of the integer and fractional parts of
        % the Controls strings, [int;frac], for each decimal-aligned
        % column.  Non-decimal aligned columns store an empty value.
        MinDecimalWidths
        
        % Matrix holding initial row indices and visibility states. These
        % are set in client code.  Could be vis or invis settings.  These
        % need to be retained and deferred until table has been created,
        % then applied once after initialization.
        %  [rowIdx visState;
        %   ... ]
        RuntimeRowVisibility
        RuntimeRowVisibility_Prev
        RuntimeElementVisibility
        
        % Deferred flow-centric commands
        DeferredRowVisArgs = {}
        DeferredRowEnableArgs = {}
        DeferredElementEnableArgs = {}
        DeferredElementVisArgs = {}
        
        % Cell-vector, one entry per row.
        %
        % Each cell entry has a cell containing vectors of contiguous
        % column indices to merge.
        %
        % Empty cells and missing rows indicate no merges in that row.
        %
        % If a row has any merges, all column indices are present in the
        % row entry.
        %
        % Ex: 5 rows, 6 cols
        %     { {}, {1:6}, {1,2:4,5,6}, {1:2,3:4,5:6} }
        % indicates:
        %     row 1: no merges
        %     row 2: merge all columns into a single element
        %     row 3: merge cols 2,3,4
        %     row 4: merge cols 1+2, then 3+4, then 5+6
        %     row 5: no merges
        %
        % Gets "normalized" in mergecols_Action to have a complete span of
        % all Control-matrix columns, OR a row has an empty cell implying
        % no merge-sets.  It truncates early such that missing end-rows
        % also have no merge-sets.
        DeferredMergeCells = {}
        
        % Summary of merged cells
        % Ex:
        %       [0 1 1 1 2 2 0 3 3 3 3;     { {2:4,5:6,8:11}
        %        0 0 0 0 0 0 0 0 0 0 0;  ->   {}
        %        0 0 1 1 1 0 0 0 2 2 0]       {3:5,9:10} }
        SummaryMergeCells = []
        
        % List of controls for flow-syntax
        FlowControls
        
    end
    
    % Parent/Child Layout Control
    properties (Access=private)
        % Handle to parent DCTableForm responsible for layout.
        % If empty, we do our own layout.
        % If non-empty, we do NOT do layout.
        %
        % Only valid when:
        %  - parent dialog is a DCTableForm
        %  - parent has same number of columns as this dialog
        LayoutParent
        
        % Zero or more child DCTableForm handles that participate in our
        % layout.
        %
        % Only valid when:
        %  - child dialogs are DCTableForm
        %  - children have same number of columns as this dialog
        LayoutChildren
    end
    
    properties(Access=private)
        ManualApply = false
        SubDCTableForms
        ControlPropertyPairs = {}
        ApplyButton
        RevertButton
        ValidationFunctions = {}
        BackingStore
        PendingProperties
        ManualApplyOverrideProps = {}
        RowVisLis = {}
    end
    
    properties(GetAccess=public,SetAccess=private, Hidden)
        StringValues
    end
    
    properties(Dependent)
        ChangesPending
    end
    
    events
        PendingUpdate
    end
    
    methods
        
        function val = get.ChangesPending(dlg)
            val = ~isempty(dlg.PendingProperties);
        end
        
        function set.ColumnWidths(dlg,v)
            % Check values:
            %     cell: may contain a combination of the following
            %   string: 'min', 'max'
            %  numeric: scalar or 2-element vector of pixel widths,
            %           [minW; maxW], positive integers
            %           -1 means automatic sizing of width
            %           inf means unlimited max width
            %
            % The only changes we make to user values is normalizing
            % shortened strings to their full length.
            %
            % - Leaves value as either numeric OR a cell
            % - Does NOT expand number of entries, replace -1's, etc
            % - Does NOT replace 'min' or 'max' with corresponding vectors
            
            is_cell = iscell(v);
            if ~is_cell
                v = {v};
            end
            
            allStrs = {'min','max'};
            for i = 1:numel(v)
                cw_i = v{i};
                if ischar(cw_i) || (isstring(cw_i) && isscalar(cw_i))
                    % Check strings
                    sel = strncmpi(cw_i,allStrs,numel(cw_i));
                    if sum(sel)~=1
                        dialogmgr.untranslatedError([ ...
                            'Dialog "' dlg.Name '":\n' ...
                            '  Unrecognized ColumnWidths string: "' cw_i{i} '".']);
                    end
                    v(i) = allStrs(sel); % normalize string
                else
                    % Check numeric values
                    %  - scalar or 2-element vectors only
                    %  - real, full, integer values
                    if isempty(cw_i) || ~isnumeric(cw_i) || ~isreal(cw_i) || issparse(cw_i) || (numel(cw_i)>2)
                        dialogmgr.untranslatedError([ ...
                            'Dialog ' dlg.Name ':\n' ...
                            '  ColumnWidths must be a scalar W or 2-element vector [Wmin Wmax],\n' ...
                            '  the string ''min'' or ''max'', or a cell-vector containing\n' ...
                            '  one of the above for each column of table.']);
                    end
                    if any(cw_i<-1) || any(cw_i==0) || any(cw_i~=fix(cw_i))
                        dialogmgr.untranslatedError([ ...
                          'Dialog "' dlg.Name '":\n' ...
                          '  Min and max column widths must be positive integers or -1 (auto-width).']);
                    end
                    if isinf(cw_i(1))
                        dialogmgr.untranslatedError([ ...
                          'Dialog "' dlg.Name '":\n' ...
                          '  Inf may not be specified for minimum column width.']);
                    end
                    if numel(cw_i)>1 && (cw_i(2)~=-1) && (cw_i(1)>cw_i(2))
                        dialogmgr.untranslatedError([ ...
                          'Dialog "' dlg.Name '":\n' ...
                            '  Minimum column width must not exceed maximum column width.']);
                    end
                end
            end
            
            if is_cell
                dlg.ColumnWidths = v;
            else
                dlg.ColumnWidths = v{1};
            end
        end
        
        function set.DecimalAlignment(dlg,v)
            if ~islogical(v)
                dialogmgr.untranslatedError([ ...
                    'Dialog "' dlg.Name '":\n' ...
                    '  DecimalAlignment must be a logical scalar or vector.'])
            end
            dlg.DecimalAlignment = v;
        end
        
        function set.HorizontalAlignment(dlg,v)
            % Check values, and normalize user strings to complete versions
            % of the corresponding string.  But do NOT check or expand
            % number of entries.
            %
            % Number of entries depends on size of InitialText, and
            % checking here would force these properties to be
            % order-dependent in terms of setting or changing their values.
            % That check/expand operation is done in cachePropertyValues.
            
            is_cellstr = iscellstr(v) || isstring(v);
            if ~is_cellstr
                if ~ischar(v) && ~(isstring(v) && isscalar(v))
                    dialogmgr.untranslatedError([ ...
                        'Dialog "' dlg.Name '":\n' ...
                        '  HorizontalAlignment must be a string or a cell-string.']);
                end
                % Record full version of string for each entry.
                % User may have used an abbreviated form.
                v = {v};
            end
            
            allStrs = {'inherit','left','center','right'};
            for i = 1:numel(v)
                sel = strncmpi(v{i},allStrs,numel(v{i}));
                if sum(sel)~=1
                    dialogmgr.untranslatedError([ ...
                        'Dialog "' dlg.Name '":\n' ...
                        '  Unrecognized HorizontalAlignment value: "' v{i} '"']);
                end
                v(i) = allStrs(sel);
            end
            
            % Record "normalized" string, but don't record it as a cell if
            % it came to us as a string.  This will prevent the automatic
            % expansion of a single string to a cell-string later.
            if is_cellstr
                dlg.HorizontalAlignment = v;    % record normalized cell
            else
                dlg.HorizontalAlignment = v{1}; % record normalized string
            end
        end
        
        function set.NumHeaderRows(dlg,v)
            % Do not check value versus number of rows of Controls; that
            % becomes an order-dependent test and is done later.
            if ~isnumeric(v) || ~isreal(v) || ~isscalar(v) || v~=fix(v) || v<0 || isinf(v)
                dialogmgr.untranslatedError([ ...
                    'Dialog "' dlg.Name '":\n' ...
                    '  NumHeaderRows must be a non-negative integer.']);
            end
            dlg.NumHeaderRows = v;
        end
        
        function set.InterColumnSpacing(dlg,v)
            if ~isnumeric(v) || ~isreal(v) || ~isscalar(v) || v~=fix(v) || v<0 || isinf(v)
                dialogmgr.untranslatedError([ ...
                    'Dialog "' dlg.Name '":\n' ...
                    '  InterColumnSpacing must be a non-negative integer.']);
            end
            dlg.InterColumnSpacing = v;
        end

        function set.InterRowSpacing(dlg,v)
            if ~isnumeric(v) || ~isreal(v) || ~isscalar(v) || v~=fix(v) || v<0 || isinf(v)
                dialogmgr.untranslatedError([ ...
                    'Dialog "' dlg.Name '":\n' ...
                    '  InterRowSpacing must be a non-negative integer.']);
            end
            dlg.InterRowSpacing = v;
        end
        
        function set.InnerBorderSpacing(dlg,v)
            if ~isnumeric(v) || ~isreal(v) || ~isscalar(v) || v~=fix(v) || v<0 || isinf(v)
                dialogmgr.untranslatedError([ ...
                    'Dialog "' dlg.Name '":\n' ...
                    '  InnerBorderSpacing must be a non-negative integer.']);
            end
            dlg.InnerBorderSpacing = v;
        end
    end
    
    % Overrides
    %
    methods
        function ext = getExtent(dlg,ymax)
            % Extent of dialog content.
            %
            % Extent is returned as [0 0 width height]
            %  - Height is from dialogContent
            %  - Width is from dialogBorder, includes all gutters/spacing,
            %    but excludes the uipanel's decorative BorderWidth.
            if nargin<2
                ymax = getHeight(dlg);
            end
            ext = [0 0 getInnerWidth(dlg.DialogBorder) ymax];
        end
        
        function H = getHeight(dlg)
            % Height extent of dialog content.
            
            % First row is highest y-coord.
            % Only retain rows that are visible.
            rh = dlg.CachedRowHeights(dlg.RuntimeRowVisibility);
            
            % one top and one bottom (inner) border
            % N-1 inter-row spaces
            H = 2*dlg.InnerBorderSpacing + (numel(rh)-1)*dlg.InterRowSpacing + sum(rh);
        end
        
        function setWidth(dialogContent,W)
            % Change width of dialog content.
            %
            % Overrides DialogContent superclass implementation.
            
            % Invoke base class method
            setWidth@dialogmgr.DialogContent(dialogContent,W);
            resizeLocal(dialogContent,false);
        end
        
        function W = getMinWidth(dlg)
            % Minimum render size of DialogContent in pixels.
            %
            % Minimum size is the smallest width and height that the
            % uidialog content can be rendered without clipping.  It is
            % generally a smaller size than the current extent of the
            % uidialog.
            
            % CachedColumnWidths contains [minW;maxW] for each row.
            % Just get minimums:
            cw_min = dlg.CachedColumnWidths(1,:);
            W = 2*dlg.InnerBorderSpacing + sum(cw_min) + ...
                (numel(cw_min)-1)*dlg.InterColumnSpacing;
        end
        
        function initializeFinal(dlg)
            % Initialization tasks to complete after all dialogs have been
            % registered with DialogPresenter.
            
            % Control-based actions for visibility and enable
            connectRowVisToControl_Install(dlg);
            connectElementVisToControl_Install(dlg);
            connectRowEnableToControl_Install(dlg);
            connectElementEnableToControl_Install(dlg);
            
            %Trigger row visibility callbacks manually
            for i=1:length(dlg.RowVisLis)
                li = dlg.RowVisLis{i};
                li.Callback();
            end

        end
        
        function changeDialogContentHeight(dc)
            % Force dialog layout to respond to a change in height of the
            % form content, such as a change in row visibility.
            
            [origin,spans,ymax] = updateTableLayout_height( ...
                dc, dc.LastLayoutOrigin, dc.LastLayoutSpans);
            
            updateControlPositions(dc,origin,spans);
            
            changeDialogContentHeight(dc.DialogBorder,ymax);
        end
    end
    
    % Flow methods
    %
    methods (Access=protected)
        function newrow(dc)
            % Begin a new row in the table layout.
            
            if dc.IsLazyLoadPhase
                return
            end
            dc.FlowControls{end+1} = {};
        end
        
        function startrow(dc)
            % Create a new row if not at the start of an empty row.
            
            if dc.IsLazyLoadPhase
                return
            end
            
            % FlowControls could be empty, i.e., the first control added to
            % parent is a child dialog.  In that case, just add the new
            % dialog as the first entry.
            if ~isempty(dc.FlowControls) && ~isempty(dc.FlowControls{end})
                nextrow(dc);
            end
        end
        
        function skipcol(dc)
            % Leave the next element empty in the table.
            
            if dc.IsLazyLoadPhase
                return
            end
            if isempty(dc.FlowControls)
                dc.FlowControls = {{[]}};
            else
                dc.FlowControls{end}{end+1} = [];
            end
        end
        
        function mergecols(dc,varargin)
            % mergecols(dlg,C) specifies columns to merge in column
            % index vector C.  No repeated indices can appear in C.  No
            % columns can be skipped when merging columns.  Only one
            % uicontrol can be specified in the set of merged columns.
            %
            % mergecols(dlg) merges all columns in this row into a single
            % wide cell.  Only one uicontrol can appear in this row of the
            % table.
            %
            % mergecols(dlg,C1,C2,C3,...) specifies multiple sets of
            % columns to merge.  One uicontrol per merged set of columns
            % may be specified.

            if dc.IsLazyLoadPhase
                return
            end
            rowNum = numel(dc.FlowControls);
            
            dmc = dc.DeferredMergeCells;
            if (numel(dmc) >= rowNum) && (rowNum~=0) && ~isempty(dmc{rowNum})
                dialogmgr.untranslatedError([ ...
                    'Dialog "%s":\n' ...
                    '  You cannot call ''mergcols'' more than once in a row.\n' ...
                    '  To specify multiple sets of columns to merge, pass multiple\n' ...
                    '  comma-separated vectors with those sets of column indices.'], ...
                    dc.Name);
            end
            
            if nargin<2
                fullColList = {'all'};
            else
                % Analyze column indices
                N = numel(varargin); % # column groups to merge
                usedColList = false; % grows to a logical vector
                startIdxList = zeros(N,1);  % first index of each set of cols to merge
                colList = cell(N,1);
                for i = 1:N
                    cols_i = sort(varargin{i});
                    if any(diff(cols_i))~=1
                        dialogmgr.untranslatedError([ ...
                            'Dialog "%s":\n' ...
                            '  Only adjacent columns may be merged.'], ...
                            dc.Name);
                    end
                    
                    % Extend used-column list as needed, init new cols to false
                    max_cols_i = max(cols_i);
                    if max_cols_i > numel(usedColList)
                        usedColList(max_cols_i) = false;
                    end
                    
                    % Now check if newly specified col indices are in used-column list
                    if any(usedColList(cols_i))
                        dialogmgr.untranslatedError([ ...
                            'Dialog "%s":\n' ...
                            '  Cannot repeat indices of columns to merge.'], ...
                            dc.Name);
                    end
                    usedColList(cols_i) = true;
                    
                    % Record starting index of this merge-set
                    startIdxList(i) = cols_i(1);
                    
                    % Retain new merge-set, containing column indices in
                    % this row
                    colList{i} = cols_i;
                end
                
                % Sort the merge-sets into ascending order with respect to
                % the start index of each set:
                [startIdxList,idx] = sort(startIdxList);
                colList = colList(idx);
                
                % At this point, colList has all sets of cols to merge in
                % this row.  The cols in each merge group are sorted and
                % not missing indices.  The set of merge groups are sorted
                % from lowest to highest start index.
                %
                % There CAN BE HOLES however, such as: {2:4, 7:8} where
                % columns 1, 5, 6 are not specified (therefore are not
                % being merged).
                %
                % Fill in those holes now.
                %
                % NOTE: We do not know the max column at this time.  We
                % won't know until all rows have been specified, and we're
                % being called on a single row -- so we don't have that
                % visibility here.  Just fill in holes.
                fullColList = {};
                nextIdx = 1;
                for i = 1:N
                    col1 = startIdxList(i); % 1st element in colList{i}
                    while col1 > nextIdx
                        % need to insert a single column index into set
                        fullColList{end+1} = nextIdx; %#ok<AGROW>
                        nextIdx = nextIdx + 1;
                    end
                    fullColList{end+1} = colList{i}; %#ok<AGROW>
                end
            end
            
            dc.DeferredMergeCells{rowNum} = fullColList;
        end
    end
    
    % Flow private methods
    %
    methods (Access=protected)
        function addFlowControl(dlg,h)
            % Records uicontrol handle to flow-layout list.
            % Add control to cell array, which is a ragged-matrix.
            
            % Support "empty h" for lazy-load execution.
            %
            % We do NOT alter Flowcontrols in that case - just leave it
            % as-is (which should be "empty" during lazy-load).
            if isempty(h)
                return
            end
            
            if isempty(dlg.FlowControls)
                % Create new cell vector, with a cell in it.
                dlg.FlowControls = {{h}};
            else
                % Append handle to end of last vector in cell
                dlg.FlowControls{end}{end+1} = h;
            end
        end
    end
    
    methods (Access=private)
        function mergecols_Action(dlg)
            % Finalize merging of all columns within a row.
            %
            % - Check that each merge-set in each row has exactly zero or
            % one control associated with it.
            %
            % - Move control in each merge-set to be in the first column
            % specified for each merge-set.
            %
            % - Confirms merge cols don't participate in decimal alignment
            %
            % - Extends merge-set spec to account for all columns in row
            %    - merge-set indices for a given row may not cover all cols
            %    - non-merged columns are added to spec for uniformity
            %
            % No graphical repositioning of controls occurs here, just
            % organizing of sets of controls.
            
            % Check that each merged column-set has exactly zero or one
            % control associated with it.  Move control to first column of
            % a given merge-set.
            %
            %decimalAlign = dlg.CachedDecimalAlignCols;
            c = dlg.Controls;
            [NR,NC] = size(c);

            % Compute summary of which elements are merged
            s = zeros(NR,NC);
            dlg.SummaryMergeCells = s;

            % Example: Controls is 12x6 matrix
            %  { {1, 2:4, 5, 6:7};    -> row 1: merge cols 2-4 and 6-7
            %    {};                  -> row 2: no merges
            %    {1,2,3,4:6};         -> row 3: merge cols 4-6
            %    ... }
            mergeAll = dlg.DeferredMergeCells;
            numMergeSets = numel(mergeAll); % rows that have merge-index sets specified
            if numMergeSets==0
                return
            end
            
            for rowIdx = 1:numMergeSets
                % Loop over rows of control matrix, only from 1:Nm.  That
                % is, only for rows having a column-index merge-set.
                
                % Get sets of columns to merge.  May be a no sets, just
                % one, or multiple sets.
                mergeSets_thisRow = mergeAll{rowIdx};
                Nms = numel(mergeSets_thisRow);
                
                % Loop over each merge-set for this row
                for j = 1:Nms
                    % We want to find 0 or 1 control in this range of cols.
                    % If two or more, throw error.
                    % If one, move it to first column in merge-set.
                    nextMergeSetCols = mergeSets_thisRow{j};  % e.g., [2 3 4], or 'all'
                    if ischar(nextMergeSetCols) || (isstring(nextMergeSetCols) && isscalar(nextMergeSetCols))
                        if strcmpi(nextMergeSetCols,'all')
                            % Merge all cols in row
                            nextMergeSetCols = 1:NC;
                            
                            % Update .DeferredMergeCells:
                            mergeSets_thisRow{j} = nextMergeSetCols;
                            mergeAll{rowIdx} = mergeSets_thisRow;
                        else
                            dialogmgr.untranslatedError([ ...
                                'Dialog "%s":\n' ...
                                '  Unsupported option ''%s'' passed to mergecols for row %d.'], ...
                                dlg.Name,nextMergeSetCols,rowIdx);
                        end
                    end
                    controls_in_merge_set = c(rowIdx,nextMergeSetCols);  % e.g., 3 controls
                    
                    % Update summary
                    %  common 1-based index written for each merge-set
                    %  ex: DeferredMergeSetCols = {2:4,6:7}
                    %      s(j,:) = [0 1 1 1 0 2 2]
                    % Only record if non-scalar; scalars don't represent
                    % merge-sets and we want 0 in those elements:
                    if numel(nextMergeSetCols)>1
                        s(rowIdx,nextMergeSetCols) = j;
                    end
                    
                    % Confirm that only one non-placeholder control appears
                    % in each merge-set.
                    %
                    % To do this, identify Placeholder controls:
                    %
                    % xxx this code doesn't work as expected!
                    %  (gives a single outcome, but expected/need N)
                    %realControlsSel = ~isa(controls_in_merge_set,'dialogmgr.uiobject');
                    Nrcs = numel(controls_in_merge_set);
                    realControlsSel = false(Nrcs,1);
                    for p = 1:Nrcs
                        realControlsSel(p) = ~isempty(controls_in_merge_set{p});
                    end
                    % Identify column indices of non-placeholder controls
                    colIdx = nextMergeSetCols(realControlsSel); % logical indexing
                    if isscalar(colIdx)
                        % decimal align is unavailable at the time this method is called
                        %if decimalAlign(colIdx)
                        %    dialogmgr.untranslatedError('Cannot merge columns that use decimal alignment.');
                        %end
                        
                        % Move the one-and-only control to first column of
                        % merge-set. Remove it from current position.
                        if colIdx > 1
                            % Instead of instantiating GraphicsPlaceholder,
                            % just swap existing one into new position.
                            t = c(rowIdx,nextMergeSetCols(1));
                            c(rowIdx,nextMergeSetCols(1)) = c(rowIdx,colIdx);
                            c(rowIdx,colIdx) = t; % {};
                        end
                    elseif ~isempty(colIdx)
                        dialogmgr.untranslatedError([ ...
                            'Dialog "%s":\n' ...
                            '  Cannot merge columns %s in row %d:\n' ...
                            '  multiple controls appear in those columns.'], ...
                            dlg.Name, mat2str(nextMergeSetCols), rowIdx);
                    end
                end
            end
            % Store updated Controls matrix
            dlg.Controls = c;
            dlg.SummaryMergeCells = s;
            
            % Extend merge-set for each row to cover all columns.
            % - merge-set indices for a given row may not cover all cols at
            %   present, so we extend the set for each row
            % - extensions to merge-set are all non-merged columns
            % - rows with NO merges can be left empty, but need to be
            %   present in cell array
            %
            for i = 1:NR
                % Extend merge-set for this row to cover all columns:
                % - merge-set indices may not cover all columns
                % - extensions to merge-set are non-merged columns
                if i <= numMergeSets
                    mergeSets_thisRow = mergeAll{i}; % could be empty
                else
                    % to fill in every row with full set of indices:
                    mergeSets_thisRow = {};
                end
                if ~isempty(mergeSets_thisRow)
                    maxIdx = mergeSets_thisRow{end}(end);
                    if maxIdx < NC
                        mergeSets_thisRow = [mergeSets_thisRow num2cell(maxIdx+1:NC)]; %#ok<AGROW>
                    end
                end
                mergeAll{i} = mergeSets_thisRow;
            end
            dlg.DeferredMergeCells = mergeAll;
        end
    end
    
    % Serialize all public properties
    %
    methods
        function p = getAllPublicProps(dlg) %#ok<STOUT,MANU>
            % All properties includes child uidialogs and
            % dialogBorder-specific properties such as RollerShade.
            %
            % Returns a structure with property names as field names.
            % Hierarchy in the structure reflects hierarchy from child
            % uidialogs.
            
            keyboard
        end
        
        function setAllPublicProps(dlg,p) %#ok<INUSD>
            % All properties includes child uidialogs and
            % dialogBorder-specific properties such as RollerShade.
            %
            % Takes a structure with property names as field names.
            
            keyboard
        end
    end
    
    % Basic flow-based uicontrol creation overrides.
    %
    methods
        function h = uicheckbox(dc,varargin)
            % Create a uicheckbox control.
            %   h = uicheckbox(dlg,str,param,value, ...)
            %   h = uicheckbox(dlg,param,value, ...)
            %
            % str is an optional string that appears to the right of the
            % uicheckbox control.
            %
            % Supports (...,'label',label) param/value pair, which adds
            % a uitext control before the uicheckbox control.
            %
            % The following are equivalent:
            %    uitext(dlg,'Style Control:');
            %    c = uicheckbox(dlg,'Italics');
            % and
            %    c = uicheckbox(dlg,'Italics','label','Style Control:');
            % and
            %    c = uicheckbox(dlg,'label','Style Control:','string','Italics');
            %
            % Note that all additional param/value pairs will be passed to
            % BOTH the uitext and the uiedit controls.
            
            if dc.IsLazyLoadPhase
                h = [];
                return
            end
            
            if rem(numel(varargin),2)==1
             
                args = addLabelIfPresent(dc,varargin(2:end));
                str = varargin{1};
            else
                args = addLabelIfPresent(dc,varargin);
                str = '';
            end
            h = uicheckbox@dialogmgr.DialogContent(dc,str,args{:});
            addFlowControl(dc,h);
        end
        
        function h = uiedit(dc,varargin)
            % Create a uiedit control.
            %   h = uiedit(dlg,str,param,value, ...)
            %   h = uiedit(dlg,param,value, ...)
            %
            % str is an optional initial string for the uiedit control.
            %
            % Supports (...,'label',label) param/value pair, which adds
            % a uitext control before the uiedit control.
            %
            % The following are equivalent:
            %    uitext(dlg,'Phi:');
            %    c = uiedit(dlg,'+00.0');
            % and
            %    c = uiedit(dlg,'+00.0','label','Phi:');
            % and
            %    c = uiedit(dlg,'label','Phi:','string','+00.0');
            %
            % Note that all additional param/value pairs will be passed to
            % BOTH the uitext and the uiedit controls.
            
            if dc.IsLazyLoadPhase
                h = [];
                return
            end

            if rem(numel(varargin),2)==1
                args = addLabelIfPresent(dc,varargin(2:end));
                str = varargin{1};
            else
                args = addLabelIfPresent(dc,varargin);
                str = '';
            end
            h = uiedit@dialogmgr.DialogContent(dc,str,args{:});
            addFlowControl(dc,h);
        end

        function h = uieditd(dc,varargin)
            % Create a uieditd control.
            %   h = uieditd(dlg,str,param,value, ...)
            %   h = uieditd(dlg,param,value, ...)
            %
            % str is an optional initial string for the uieditd control.
            %
            % Supports (...,'label',label) param/value pair, which adds
            % a uitext control before the uiedit control.
            %
            % The following are equivalent:
            %    uitext(dlg,'Phi:');
            %    c = uieditd(dlg,'+00.0');
            % and
            %    c = uieditd(dlg,'+00.0','label','Phi:');
            % and
            %    c = uieditd(dlg,'label','Phi:','string','+00.0');
            %
            % Note that all additional param/value pairs will be passed to
            % BOTH the uitext and the uieditd controls.
            
            if dc.IsLazyLoadPhase
                h = [];
                return
            end
            
            if rem(numel(varargin),2)==1
                args = addLabelIfPresent(dc,varargin(2:end));
                str = varargin{1};
            else
                args = addLabelIfPresent(dc,varargin);
                str = '';
            end
            h = uieditd@dialogmgr.DialogContent(dc,str,args{:});
            addFlowControl(dc,h);
        end
        
        function h = uieditv(dc,varargin)
            % Create a real-valued numeric edit control.
            %   h = uieditv(dlg,str,param,value, ...)
            %   h = uieditv(dlg,param,value, ...)
            %
            % str is an optional initial string for the uieditd control.
            %
            % Supports (...,'label',label) param/value pair, which adds
            % a uitext control before the uiedit control.
            %
            % The following are equivalent:
            %    uitext(dlg,'Phi:');
            %    c = uieditv(dlg,'+00.0');
            % and
            %    c = uieditv(dlg,'+00.0','label','Phi:');
            % and
            %    c = uieditv(dlg,'label','Phi:','string','+00.0');
            %
            % Note that all additional param/value pairs will be passed to
            % BOTH the uitext and the uieditv controls.
            
            if dc.IsLazyLoadPhase
                h = [];
                return
            end
            
            if rem(numel(varargin),2)==1
                args = addLabelIfPresent(dc,varargin(2:end));
                str = varargin{1};
            else
                args = addLabelIfPresent(dc,varargin);
                str = '';
            end
            h = uieditv@dialogmgr.DialogContent(dc,str,args{:});
            addFlowControl(dc,h);
        end
        
        function h = uieditvc(dc,varargin)
            % Create a complex-valued numeric edit control.
            %   h = uieditvc(dlg,str,param,value, ...)
            %   h = uieditvc(dlg,param,value, ...)
            %
            % str is an optional initial string for the uieditd control.
            %
            % Supports (...,'label',label) param/value pair, which adds
            % a uitext control before the uiedit control.
            %
            % The following are equivalent:
            %    uitext(dlg,'Phi:');
            %    c = uieditvc(dlg,'+00.0');
            % and
            %    c = uieditvc(dlg,'+00.0','label','Phi:');
            % and
            %    c = uieditvc(dlg,'label','Phi:','string','+00.0');
            %
            % Note that all additional param/value pairs will be passed to
            % BOTH the uitext and the uieditvc controls.
            
            if dc.IsLazyLoadPhase
                h = [];
                return
            end
            
            if rem(numel(varargin),2)==1
                args = addLabelIfPresent(dc,varargin(2:end));
                str = varargin{1};
            else
                args = addLabelIfPresent(dc,varargin);
                str = '';
            end
            h = uieditvc@dialogmgr.DialogContent(dc,str,args{:});
            addFlowControl(dc,h);
        end
        
        function h = uiframe(dc,varargin)
            
            if dc.IsLazyLoadPhase
                h = [];
                return
            end
            
            h = uiframe@dialogmgr.DialogContent(dc,varargin{:});
            addFlowControl(dc,h);
        end
        
        function h = uihline(dc,varargin)
            if dc.IsLazyLoadPhase
                h = [];
                return
            end
            
            h = uihline@dialogmgr.DialogContent(dc,varargin{:});
            addFlowControl(dc,h);
        end
        
        function h = uilistbox(dc,varargin)
            if dc.IsLazyLoadPhase
                h = [];
                return
            end
            
            h = uilistbox@dialogmgr.DialogContent(dc,varargin{:});
            addFlowControl(dc,h);
        end
        
        function h = uipopup(dc,strs,varargin)
            % Create a uipopup control.
            %   h = uipopup(dlg,choices,param,value, ...)
            %   h = uipopup(dlg,choices,InitialChoice,param,value, ...)
            %
            % choices is required and must be a cell-string.
            % InitialChoice is optional and may be a string or an index.
            %
            % Supports (...,'label',label) param/value pair, which adds
            % a uitext control before the uipopup control.
            %
            % The following are equivalent:
            %    uitext(dlg,'Color:');
            %    c = uipopup(dlg,{'Red','Green','Blue'},'Green');
            % and
            %    c = uipopup(dlg,{'Red','Green','Blue'},'Green','label','Color:');
            %
            % Note that all additional param/value pairs will be passed to
            % BOTH the uitext and the uipopup controls.
            
            if dc.IsLazyLoadPhase
                h = [];
                return
            end
            
            if rem(numel(varargin),2)==1
                args = addLabelIfPresent(dc,varargin(2:end));
                val = varargin{1}; % initializer specified
            else
                args = addLabelIfPresent(dc,varargin);
                val = 1;
            end
            h = uipopup@dialogmgr.DialogContent(dc,strs,val,args{:});
            addFlowControl(dc,h);
        end
        
        function h = uipushbutton(dc,varargin)
            
            if dc.IsLazyLoadPhase
                h = [];
                return
            end
            
            h = uipushbutton@dialogmgr.DialogContent(dc,varargin{:});
            addFlowControl(dc,h);
        end
        
        function h = uiradiobutton(dc,varargin)
            % Create a uiradiobutton control.
            %   h = uiradiobutton(dlg,str,param,value, ...)
            %   h = uiradiobutton(dlg,param,value, ...)
            %
            % str is an optional string that appears to the right of the
            % uiradiobutton control.
            %
            % Supports (...,'label',label) param/value pair, which adds
            % a uitext control before the uicheckbox control.
            %
            % The following are equivalent:
            %    uitext(dlg,'Option:');
            %    c = uiradiobutton(dlg,'A');
            % and
            %    c = uiradiobutton(dlg,'A','label','Option:');
            % and
            %    c = uiradiobutton(dlg,'label','Option:','string','A');
            %
            % Note that all additional param/value pairs will be passed to
            % BOTH the uitext and the uiradiobutton controls.

            if dc.IsLazyLoadPhase
                h = [];
                return
            end
            
            if rem(numel(varargin),2)==1
                args = addLabelIfPresent(dc,varargin(2:end));
                str = varargin{1};
            else
                args = addLabelIfPresent(dc,varargin);
                str = '';
            end
            h = uiradiobutton@dialogmgr.DialogContent(dc,str,args{:});
            addFlowControl(dc,h);
        end
        
        function h = uislider(dc,range,varargin)
            % Create a uislider control.
            %   h = uislider(dlg,Range,param,value, ...)
            %   h = uislider(dlg,Range,InitialValue,param,value, ...)
            %
            % range is required and must be a 2-element vector, [min max],
            % specifying the range of values for the slider.
            %
            % InitialValue is optional and must be a scalar within the
            % specified range.
            %
            % Supports (...,'label',label) param/value pair, which adds
            % a uitext control before the uipopup control.
            %
            % The following are equivalent:
            %    uitext(dlg,'Range:');
            %    c = uislider(dlg,[1 5],3);
            % and
            %    c = uislider(dlg,[1 5],3,'label','Range:');
            %
            % Note that all additional param/value pairs will be passed to
            % BOTH the uitext and the uislider controls.
            
            if dc.IsLazyLoadPhase
                h = [];
                return
            end
            
            if rem(numel(varargin),2)==1
                args = addLabelIfPresent(dc,varargin(2:end));
                val = varargin{1}; % initializer specified
            else
                args = addLabelIfPresent(dc,varargin);
                val = range(1);
            end
            h = uislider@dialogmgr.DialogContent(dc,range,val,args{:});
            addFlowControl(dc,h);
        end
        
        function h = uispintext(dc,strs,varargin)
            % Create a uispintext control.
            %   h = uispintext(dlg,choices,param,value, ...)
            %   h = uispintext(dlg,choices,InitialChoice,param,value, ...)
            %
            % choices is required and must be a cell-string.
            % InitialChoice is optional and may be a string or an index.
            %
            % Supports (...,'label',label) param/value pair, which adds
            % a uitext control before the uispintext control.
            %
            % The following are equivalent:
            %    uitext(dlg,'Color:');
            %    c = uispintext(dlg,{'Red','Green','Blue'},'Green');
            % and
            %    c = uispintext(dlg,{'Red','Green','Blue'},'Green','label','Color:');
            %
            % Note that all additional param/value pairs will be passed to
            % BOTH the uitext and the uispintext controls.
            
            if dc.IsLazyLoadPhase
                h = [];
                return
            end
            
            if rem(numel(varargin),2)==1
                args = addLabelIfPresent(dc,varargin(2:end));
                val = varargin{1}; % initializer specified
            else
                args = addLabelIfPresent(dc,varargin);
                val = 1;
            end
            h = uispintext@dialogmgr.DialogContent(dc,strs,val,args{:});
            addFlowControl(dc,h);
        end

        function h = uitext(dc,varargin)
            % Create a uieditd control.
            %   h = uitext(dlg,str,param,value, ...)
            %   h = uitext(dlg,param,value, ...)
            %
            % str is an optional initial string for the uitext control.
            %
            % Supports (...,'label',label) param/value pair, which adds
            % an initial uitext control before the uitext control.
            %
            % The following are equivalent:
            %    uitext(dlg,'Max:');
            %    c = uitext(dlg,'100');
            % and
            %    c = uitext(dlg,'100','label','Max:');
            %
            % Note that all additional param/value pairs will be passed to
            % BOTH the initial uitext and later uitext controls.
            
            if dc.IsLazyLoadPhase
                h = [];
                return
            end
            
            if rem(numel(varargin),2)==1
                args = addLabelIfPresent(dc,varargin(2:end));
                str = varargin{1};
            else
                args = addLabelIfPresent(dc,varargin);
                str = '';
            end
            h = uitext@dialogmgr.DialogContent(dc,str,args{:});
            addFlowControl(dc,h);
        end
        
        function h = uitogglebutton(dc,varargin)
            % Add a uitogglebutton control to the current row.
            
            if dc.IsLazyLoadPhase
                h = [];
                return
            end
            
            h = uitogglebutton@dialogmgr.DialogContent(dc,varargin{:});
            addFlowControl(dc,h);
        end
        
        function h = uidialog(dc,propName,varargin)
            % Add a dialog within a parent dialog.
            %
            % uidialog(dlg,propName,dlgChild) adds a dialog within the
            % current dialog.  dlgChild is a DialogContent object that will
            % render within the parent dialog.
            
            c = dc.(propName);
            if isempty(c)
                % Don't replace if we already have a child DialogContent
                % (from lazy-loading).  A new instance is passed in but we
                % ignore it after the lazy-load pass.
                %
                % We don't have it yet so we get it one now:
                [~,~,c] = dialogmgr.uidialog.parseArgs(varargin);
                dc.(propName) = c;
            end
            
            % Always create a child uidialog, doesn't take much time and
            % caller needs it.
            h = dialogmgr.uidialog(dc,varargin{:});
          	dc.SubDCTableForms{end+1} = h.Dialog.DialogContent;

            if dc.IsLazyLoadPhase
                return
            end

            % Add this to all controls that can dynamically change their
            % height.  So far, that's just the uidialog control.
            addlistener(h,'HeightChanged',@(~,~)uicontrolChangedHeight(dc,h));
            
            startrow(dc);
            addFlowControl(dc,h);
            
            % merge all cols in this row for dialog
            % Do this AFTER adding the flow control
            mergecols(dc);
            newrow(dc);
            
            % dlg is a DCTableForm, so we know the dialog has inputs that
            % should trigger PropertyUpdate events.
            % Report this up to parent, in case onPropertyChange is used.
            addlistener(c,'PropertyUpdate', ...
                @(src,ev)childDialogEventRepeater(dc,ev));
        end
        
        function h = uitable(dc,varargin)
            % Add a uitable control to the current row.
            
            if dc.IsLazyLoadPhase
                h = [];
                return
            end
            
            h = uitable@dialogmgr.DialogContent(dc,varargin{:});
            addFlowControl(dc,h);
        end
    end
    
    methods (Access=private)
        function args = addLabelIfPresent(dlg,args)
            % Identifies optional ('label',labelVal) param/value pair from cell-array
            % args, which is a list of parameter/value pairs.
            %
            % If 'label' parameter is present, adds a uitext control with specified
            % label string, and applies all remaining args as param/value
            % pairs to the uitext control.
            %
            % Returns all remaining arguments in args.
            
            if ~isempty(args)
                % Check for 'label' in parameter names
                sel = strcmpi('label',args(1:2:end));
                if any(sel)
                    % Could be multiple 'label' p/v pairs in degenerate case.
                    % Only retain the last, but remove them all.
                    idx = find(sel);          % find possibly multiple 'label' names
                    idx = (idx-1)*2+1;        % location of 'index' param name in args list
                    label = args{idx(end)+1}; % get last of possibly multiple label strings
                    args([idx idx+1]) = [];   % remove all 'label' p/v pairs
                    
                    uitext(dlg,label,args{:});
                end
            end
        end
    end
    
    % Parent/Child Layout Control
    %
    methods
        function setLayoutParent(dlg,parentDialog)
            % Identify parent that will be responsible for layout rules of
            % this dialog.
            dlg.LayoutParent = parentDialog;
        end
        
        function val = getLayoutParent(dlg)
            val = dlg.LayoutParent;
        end
        
        function addLayoutChild(dlg,ch)
            dlg.LayoutChildren = [dlg.LayoutChildren ch];
        end
        
        function removeLayoutChild(dlg,ch)
            dlg.LayoutChildren(dlg.LayoutChildren==ch) = [];
        end
        
        function val = getLayoutChildren(dlg)
            val = dlg.LayoutChildren;
        end
    end
    
    % Notify-helpers for uidialog
    methods (Access=private)
        function childDialogEventRepeater(dlg,~)
          % Signal that a child TableForm property changed value.
          notify(dlg,'PropertyUpdate');
        end
    end
    
    % Concrete implementations intended to be optionally overridden by
    % a subclass.
    methods (Access=protected)
        function initTable(dc) %#ok<MANU>
            % Initialize properties of the DCTableForm object,
            % especially uicontrol handles in the Controls property.
            %
            % This is a concrete empty implementation that is intended to
            % be overridden by subclasses.
            
            fprintf([ ...
                'Subclasses of DCTableForm must implement initTable(obj).\n' ...
                'Example:\n' ...
                '\n' ...
                'classdef MyDialog < dialogmgr.DCTableForm\n' ...
                '    methods (Access=protected)\n' ...
                '        function initTable(obj)\n' ...
                '           %% Create table content here (required method)\n' ...
                '        end\n' ...
                '\n' ...
                '        function update(obj)\n' ...
                '           %% Update table content here (optional method)\n' ...
                '        end\n' ...
                '    end\n' ...
                'end\n' ...
                '\n'],'');
            dialogmgr.untranslatedError('See <a html="matlab:">Getting Started examples</a> for more help.');
        end
    end
    
    methods % Override of base class
        function lazyLoaderDataInit(dc)
            % Establish hierarchical data in parent
            % - we must execute the DC initTable method
            % - it may contain child uidialogs, which form a hierarchy with
            %   possibly more data properties at lower levels
            
            dc.IsLazyLoadPhase = true;
            initTable(dc);
            dc.IsLazyLoadPhase = false;
        end
    end
    
    methods (Access=protected) % Abstract
        function createContent(dlg)
            % Create uicontrols specific to the layout.
            
            % Invoke subclass implementation.
            %
            % We expect the subclass to set values for protected
            % properties, if they haven't already been set in the subclass
            % constructor.
            
            initTable(dlg);
            convertFlowControlsToMatrixControls(dlg);
            mergecols_Action(dlg);
            cachePropertyValues(dlg);
            createFractionControls(dlg);  % splits initial string for decimal-aligned cols
            initCommonControlProps(dlg);
            
            computeControlExtents(dlg);
            computeRowHeights(dlg);
            computeColWidths(dlg);
            
            initControlVisibility(dlg);
            
            [origin,spans] = computeTableLayout(dlg);
            
            % Here we would normally call:
            %    updateControlPositions(dlg,origin,spans);
            % However, our call to:
            %    connectRowVisToControl_Install (etc)
            % will do this for us, guaranteed - even with no rowvis to
            % change.  So we just do the one thing that
            % updateControlPositions would normally set:
            dlg.LastLayoutOrigin = origin;
            dlg.LastLayoutSpans = spans;
            
            % xxx for now, this is done in DialogContent::initialize.
            % Consider "distributing it out" to each DC subclass to do on
            % its own.
            %changeDialogContentHeight(dlg.DialogBorder,ymax,dlg);
            
            % Installing dialog row/element visibility and enable services
            % is done in initializeFinal(), which is called on each Dialog.
        end
    end
    
    % Initialize all property cache
    %
    methods (Access=private)
        function cachePropertyValues(dlg)
            % Expand values where scalar-expansion supported.
            % Check vector-lengths here, since num cols now known.
            % Replace tokens in local cache copies.
            %
            % Do NOT check element values - that's done in "set." methods.
            
            initDecimalAlignCache(dlg);
            initHorizAlignCache(dlg);
            initPlaceholderCache(dlg);
            initControlTagsCache(dlg);
        end
        
        function initPlaceholderCache(dlg)
            % Cache location of non-placeholder Controls
            c = dlg.Controls;
            notPlaceholder = true(size(c));
            N = numel(c);
            for i = 1:N
                notPlaceholder(i) = ~isempty(c{i});
            end
            dlg.NotPlaceholderControl = notPlaceholder;
        end
        
        function initControlTagsCache(dlg)
            % Collect all tags from Controls matrix
            
            % NOTE: Custom controls don't work with this syntax:
            %     allTags = {c.Tag};
            % Instead, we get each .Tag individually:
            notPlaceholder = dlg.NotPlaceholderControl;
            c = dlg.Controls;
            N = numel(c);
            allTags = cell(N,1);
            for i = 1:N
                if notPlaceholder(i)
                    allTags{i} = c{i}.Tag;
                end
            end
            dlg.ControlTags = allTags;
        end

        function initDecimalAlignCache(dlg)
            % DecimalAlignment
            
            Nc = size(dlg.Controls,2);
            v = dlg.DecimalAlignment;
            if isscalar(v)
                % Scalar-expand:
                v = v(ones(1,Nc));
            elseif numel(v) ~= Nc
                dialogmgr.untranslatedError([ ...
                    'Dialog "%s":\n' ...
                    '  Number of entries in DecimalAlignment (%d)\n' ...
                    '  must equal number of columns in Controls (%d).'], ...
                    dlg.Name, numel(v), Nc);
            end
            dlg.CachedDecimalAlignCols = v;
        end
        
        function initHorizAlignCache(dlg)
            % HorizontalAlignment
            
            Nc = size(dlg.Controls,2);
            v = dlg.HorizontalAlignment;
            if ischar(v) || (isstring(v) && isscalar(v))
                % Put string into cell, if cell not already specified.
                % Replicate string to make one entry per column.
                v = repmat({v},1,Nc);
            elseif numel(v)~=Nc
                dialogmgr.untranslatedError([ ...
                    'Dialog "%s":\n'...
                    '  Number of entries in HorizontalAlignment cell-vector (%d)\n' ...
                    '  must equal number of columns in Controls (%d).'], ...
                    dlg.Name, numel(v), Nc);
            end
            dlg.CachedHorizontalAlignment = v;
        end
    end
    
    methods (Access=private)
        function convertFlowControlsToMatrixControls(dlg)
            % Convert flow controls (a ragged array of cells) to matrix of
            % controls.  "Deblank" trailing empty cells.
            
            % Get # cols filled in each row
            fc = dlg.FlowControls;
            if isempty(fc)
                % One dummy control
                c = uitext(dlg,'');
                
            else
                N = cellfun(@numel,fc);
                
                % Remove trailing rows with no controls (deblank operation)
                %
                % emptyTrailingRows will be one higher than count of trailing
                % empty rows, or empty if none:
                emptyTrailingRows = find(fliplr([0 N]>0),1,'first');
                if ~isempty(emptyTrailingRows)
                    fc = fc(1 : end+1-emptyTrailingRows);
                end
                
                % Find # rows/cols in table
                Nc = max(N);    % max # cols in any row
                Nr = numel(fc); % # rows
                
                % Convert flow-of-controls to a uniform matrix
                c{Nr,Nc} = [];
                for i = 1:Nr
                    fc_i = fc{i};
                    for j = 1:numel(fc_i)
                        fc_ij = fc_i{j};
                        if ~isempty(fc_ij)
                            c{i,j} = fc_ij;
                        end
                    end
                end
            end
            dlg.Controls = c;
        end

        function createFractionControls(dlg)
            % Creates fractional-part text uicontrol for each integer-part
            % text uicontrol that the user specified in a decimal-aligned
            % column.
            %
            % ControlsFraction retains the uicontrols for the fractional
            % part, while Controls retains the integer part.
            %
            % No layout, just creation of control and splitting of initial
            % string into integer and fraction parts.
            
            alignCols = dlg.CachedDecimalAlignCols;
            
            % Do not create/allocate if no columns are decimal-aligned.
            if ~any(alignCols)
                dlg.ControlsFraction = [];
                return
            end
            
            ci = dlg.Controls;
            [Nr,Nc] = size(ci);
            Nh = dlg.NumHeaderRows;
            
            % Allocate matrix for fractional part
            cf{Nr,Nc} = [];
            
            for j = 1:Nc
                if alignCols(j)
                    % This column is to be decimal-aligned.
                    
                    % Identify all text-style controls, which are the only
                    % controls that participate in decimal alignment.
                    %
                    % Skip detection in header rows.
                    %
                    % Warn about edit boxes, as it could be unexpected that
                    % they do NOT participate.
                    %
                    % Ignore all other controls.
                    ci_tmp = ci(Nh+1:end,j);
                    to_align = true(size(ci_tmp)); % assume all controls participate
                    for i = 1:numel(ci_tmp)
                        cii = ci_tmp{i};
                        if isempty(cii)
                            % These don't have a '.Style' property
                            to_align(i) = false;
                            
                        elseif ~isa(cii,'matlab.ui.control.UIControl') ...
                                  && ~isa(cii,'dialogmgr.uiobject')
                            dialogmgr.untranslatedError([ ...
                                'Dialog "%s":\n' ...
                                '  Decimal-aligned column %d contains a non-text control in row %d.\n' ...
                                '  Either row %d is a header (and should be in the NumHeaderRows count)\n' ...
                                '  or change the control type.'], ...
                                dlg.Name,j,Nh+i,Nh+i);
                        else
                            style = cii.Style;
                            if ~strcmpi(style,'text')
                                % Don't align anything but text
                                % Ex: edit, hline, placeholder, etc
                                to_align(i) = false;
                                
                                % Edit boxes are particularly troublesome
                                % as caller probably expected them to
                                % align.  Warn about this, with the
                                % expectation that user will remove the
                                % edit box.
                                if strcmpi(style,'edit')
                                    [Msg, Warn] = dialogmgr.untranslatedWarning(['Dialog "%s":\n' ...
                                        '  An ''edit'' control is in a decimal-aligned column (row %d, column %d).\n' ...
                                        '  Only ''text'' controls participate in decimal alignment.'], ...
                                        dlg.Name,i,j);
                                    warning(Msg,Warn);
                                end
                            end
                        end
                    end
                    ci_tmp = ci_tmp(to_align); % only keep these text controls
                    
                    % Create a matching fraction control by making a deep
                    % copy of integer control.
                    %
                    % NOTE: copy() leaves parent empty!  We fill that in.
                      cf_tmp = cell(size(ci_tmp));
                      for ii=1:numel(ci_tmp)
                        cf_tmp{ii} = copy(ci_tmp{ii});
                        set(cf_tmp{ii},'Parent',ci_tmp{1}.Parent);
                      end
                      cf_idx = Nh+find(to_align);  % Nh+1 is first possible frac control
                      cf(cf_idx,j) = cf_tmp;
                      %cf(Nh+1:end,j) = cf_tmp;
                    
                    % Create struct for appdata held by these controls
                    da.ColumnControlPairs = [ci_tmp cf_tmp];
                    da.ControlPair = [];
                    
                    % Mark all integer-part controls so we know they are
                    % part of an integer/fraction pair.  This helps us
                    % during element value updates.
                    %
                    % - setappdata: DCTableForm_DecimalAlign
                    %   data is a struct:
                    %      .ColumnControlPairs = Nx2 matrix, [uictrls_int uictrls_frac]
                    %      .ControlPair        = 1x2 vector, [uictrl_int uictrl_frac]
                    %
                    for i = 1:numel(ci_tmp)
                        da.ControlPair = [ci_tmp(i) cf_tmp(i)];
                        setappdata(ci_tmp{i},'DCTableForm_DecimalAlign',da);
                        setappdata(cf_tmp{i},'DCTableForm_DecimalAlign',da);
                        
                        % Split initial string into int/frac parts.
                        %
                        % It may be empty, or it may have "+00.00" or some
                        % other initializer to guide the initial (later)
                        % assessment of minimum column width.
                        str = strtrim(ci_tmp{i}.String);
                        if ~isempty(str)
                            setDecimalAlignedElement_intfrac(ci_tmp{i},cf_tmp{i},str,false);
                        end
                    end
                end
            end
            dlg.ControlsFraction = cf;
        end
        
        function updateControlPositions(dlg,origin,spans)
            % Set full position of controls (locations and dimension).
            % Cache latest origin and spans structs.
            
            % Only the visible subset of rows are computed in origin
            %  origin.y
            %  spans.dy
            %
            % All entries in the following are present
            %  origin.x
            %  spans.dx
            %  ce.hoffset
            %  ce.voffset
            
            % Cache for faster layout updates
            dlg.LastLayoutOrigin = origin;
            dlg.LastLayoutSpans = spans;
            
            % Just for use in child dialogs
            W = getInnerWidth(dlg.DialogBorder) - 2*dlg.InnerBorderSpacing;
            
            Nh = dlg.NumHeaderRows;
            decimalAlign = dlg.CachedDecimalAlignCols;
            visRowIdx = find(dlg.RuntimeRowVisibility); % visible-row indices
            
            % columns to merge in each row
            % - has one row per Controls row
            % - may be empty, indicating no merges in that row
            % - otherwise, has a complete set of indices, divided into
            %   sub-vectors according to merge-groups of cols within row
            mergeSets = dlg.DeferredMergeCells;
            numMergeSets = numel(mergeSets);

            % Maximum x-offset of any child uidialog whose content
            % participates in the parent layout.
            parentLayoutShift = 0;
            
            notPlaceholder = dlg.NotPlaceholderControl;
            c = dlg.Controls;
            cf = dlg.ControlsFraction;
            ce = dlg.ControlExtents;
            cfe = dlg.ControlFractionExtents;
            x0 = dlg.CachedColumnOffsets;
            xf0 = dlg.CachedFractionColumnOffsets;
            y0 = dlg.CachedRowOffsets; % complete set of rows
            eachCol = num2cell(1:size(c,2));
            
            for i_vis = 1:numel(visRowIdx)
                i = visRowIdx(i_vis); % next visible-row index into "c"
                
                % Merge-set for each row has a complete set of columns
                % listed within it:
                if i <= numMergeSets
                    mergeSets_thisRow = mergeSets{i};
                else
                    mergeSets_thisRow = {};
                end
                if isempty(mergeSets_thisRow)
                    mergeSets_thisRow = eachCol;
                end
                
                for k = 1 : numel(mergeSets_thisRow)
                    thisMergeSet = mergeSets_thisRow{k};
                    j = thisMergeSet(1); % 1st col idx in next merge-set
                    
                    if notPlaceholder(i,j)
                        % - x, y, dy are common to all parts
                        % - dx needs special cases
                        % - fractional part needs full attention
                        %
                        % NOTE: dy does not use spans.dy(i), that part was
                        % just to compute RowOffsets.
                        %
                        ce_ij = ce(i,j);
                        y  = ce_ij.voffset - y0(i) + origin.y(i_vis);
                        dy = ce_ij.vert;
                        x  = ce_ij.hoffset - x0(j) + origin.x(j);
                        
                        % Child uidialogs compute their own layout. If they
                        % participate in parent layout, they need to
                        % utilize the full parent width ... despite
                        % possibly clipping early and "pushing off to the
                        % sides."  This way content aligns.
                        %
                        c_ij = c{i,j};
                        is_uidialog = isa(c_ij,'dialogmgr.uidialog');
                        % if any(c_ij.Dialog.DialogContent == dlg.LayoutChildren) && c_ij.ParticipateInParentLayout
                        if is_uidialog && c_ij.ParticipateInParentLayout
                            % Left-shift children.
                            %
                            % The child DialogBorder is likely to clip on
                            % the left, and we compensate for this after
                            % all content has been positioned, at end of
                            % this function.
                            %
                            % The offset to the "interior" of the child
                            % dialog is the parent inner-border spacing,
                            % PLUS the dialog border interior offset.
                            db = dlg.DialogBorder;
                            innerBorderOffset = floor((getOuterWidth(db) - getInnerWidth(db))/2);
                            thisShift = dlg.InnerBorderSpacing + innerBorderOffset;
                            x = x - thisShift;
                            
                            parentLayoutShift = max(parentLayoutShift,thisShift);
                        end
                        
                        % x-coord stuff varies by situation:
                        %
                        % We already checked that decimal-aligned cols
                        % don't participate in column merge-sets.
                        if decimalAlign(j) && i>Nh
                            % Fractional part
                            % Need to compute everything:
                            %  - x,y,dx,dy and set .Position
                            xf = cfe(i,j).hoffset - xf0(j) + origin.x_frac(j);
                            yf = cfe(i,j).voffset - y0(i) + origin.y(i_vis);
                            dxf = spans.dx_frac(j);
                            dyf = cfe(i,j).vert; % not spans.dy(i)
                            cf{i,j}.Position = [xf yf dxf dyf];
                            
                            % Integer part
                            dx = spans.dx_int(j);
                        else
                            % Full (non-decimal aligned column or header)
                            
                            % Give "unrestricted" width to child uidialog
                            if is_uidialog % && ~c_ij.ParticipateInParentLayout
                                % really, any control that spans all cols
                                % could be routed here...
                                %
                                % W is NOT the same value as dx computed
                                % below.  It is (generally) smaller.
                                dx = W;
                            else
                                % First col (j) of merge-set should be set to
                                % "span" the full width of all cols in
                                % merge-set. (Not all cols in row, just all
                                % cols in merge-set.)
                                %
                                % All other cols (2:numMerge) in merge-set
                                % should be invisible. We can safely disregard
                                % their pos.  These should be
                                % GraphicsPlaceholders, so no need for this
                                % explicitly.
                                % set(c(i,thisMergeSet(2:end)),'Visible','off');
                                numToMerge = numel(thisMergeSet);
                                if numToMerge == 1
                                    % Non-merged cell - the "normal" case
                                    dx = spans.dx(j);
                                else
                                    % Merge-set
                                    dx = sum(spans.dx(thisMergeSet)) + ...
                                        (numToMerge-1)*dlg.InterColumnSpacing;
                                end
                                
                                % If this is a checkbox, shorten its horiz extent
                                % to the minimum to prevent "false triggering" on
                                % stray clicks to the right of its text.
                                if strcmpi(c_ij.Style,'checkbox')
                                    dx = ce_ij.horiz;
                                elseif strcmp(c_ij.Style,'popupmenu') && ismac
                                    % mac has five leading pixels
                                    x = x-5;
                                end
                            end
                        end % decimal aligned
                        
                        % Update full/integer position
                        c_ij.Position = [x y dx dy];
                        
                    end   % not a placeholder
                end   % mergeset/k-loop
            end   % column i-loop
            
            % ParticipateInParentLayout compensation:
            %
            % If any child participates in parent layout, all content
            % must shift to right.  The child dialog shifted to left.
            if parentLayoutShift > 0
                ch = dlg.ContentPanel.Children;
                for i = 1:numel(ch)
                    ch(i).Position(1) = ch(i).Position(1) + parentLayoutShift;
                end
            end
        end
        
        function uicontrolChangedHeight(dlg,h)
            % One uicontrol changed its height.
            % Update the dialog to reflect this.
            
            % Update extent info for this control
            updateControlExtentHeight(dlg,h);
            
            % Carry out remaining steps for entire dialog
            computeRowHeights(dlg);
            
            changeDialogContentHeight(dlg);
        end
        
        function resizeLocal(dlg,changeInVis)
            % Force form to respond to a change in width of the parent.
            
            % Get immediate graphical parent of DialogContent
            % (may be same as hB)
            W = getInnerWidth(dlg.DialogBorder);
            %dlg.ContentPanel.Parent.Position(3) = W; % content parent dx = border dx
            dlg.ContentPanel.Position(3) = W; % content dx = border dx
            
            [origin,spans,ymax] = computeTableLayout(dlg);
            updateControlPositions(dlg,origin,spans);
            
            % Set DialogContent panel height
            if nargin>1 && changeInVis
                changeDialogContentHeight(dlg.DialogBorder,ymax,dlg);
            end
        end
        
        function computeControlExtents(dlg)
            % Cache matrix of structures with placement information for
            % each uicontrol in the .Controls and .ControlsFraction
            % matrices.
            %
            % Retains extent structs in .ControlExtents and
            % .ControlFractionExtents properties
            % 
            % Structures contain:
            %   .horiz: horizontal extent,  in pixels
            %    .vert: vertical extent, in pixels
            % .hoffset: horizontal offset, in pixels
            % .voffset: vertical offset, in pixels
            
            tmp_txt = dlg.TempTextControl;
            if isempty(tmp_txt)
                % Create a text uicontrol for assessing extent of non-text
                % uicontrols.  Keep this around for extent updates.
                %
                % Font-properties get set later.
                tmp_txt = uicontrol( ...
                    'visible','off', ...
                    'parent',dlg.ContentPanel, ...
                    'style','text',...
                    'string','');
                
                dlg.TempTextControl = tmp_txt;
            end
            
            % Allocate info structure
            s_ij = struct( ...
                'horiz',0, ...
                'vert',0, ...
                'hoffset',0, ...
                'voffset',0);
            
            % Full or integer-part controls
            ctrl = dlg.Controls;
            [R,C] = size(ctrl);
            s(R,C) = s_ij; % preallocate matrix of structs
            for j = 1:C
                for i = 1:R
                    s(i,j) = getMinControlExtent(ctrl{i,j},tmp_txt);
                end
            end
            dlg.ControlExtents = s;
            
            % Fractional-part controls
            % - just cache extent for fractional-part controls, no others
            alignCols = dlg.CachedDecimalAlignCols;
            if ~any(alignCols)
                t = [];
            else
                t(R,C) = s_ij;
                Nh = dlg.NumHeaderRows;
                ctrl = dlg.ControlsFraction;
                for j = 1:C
                    if alignCols(j)
                        for i = Nh+1:R
                            t(i,j) = getMinControlExtent(ctrl{i,j},tmp_txt);
                        end
                    end
                end
            end
            dlg.ControlFractionExtents = t;
        end
        
        function updateControlExtentHeight(dlg,hControl)
            % Update cached extent info for one control.
            
            % Index for full or integer-part control
            [i,j] = find(cellfun(@(x) isequal(x,hControl),dlg.Controls));
            assert(~isempty(i));
            
            % Full or Integer-part
            % xxx we get "minimum extent" here
            %     we really want "actual vertical extent"
            %     turns out min often makes a difference only for width
            %     reconsider - perhaps make a getActualControlExtent
            %
            s_ij = getMinControlExtent(hControl,dlg.TempTextControl);
            dlg.ControlExtents(i,j).vert = s_ij.vert;
            
            % Fractional-part
            if dlg.CachedDecimalAlignCols(j) && i>dlg.NumHeaderRows
                s_ij = getMinControlExtent(dlg.ControlsFraction(i,j),dlg.TempTextControl);
                dlg.ControlFractionExtents(i,j).vert = s_ij.vert;
            end
        end
        
        function computeColWidths(dlg)
            % Computes final pixel min/max widths for each table column.
            % 
            % Results are in:
            %   .CachedColumnWidths
            %   .CachedColumnOffsets
            %   .CachedFractionColumnOffsets
            %
            % Assumes:
            %  - initColumnWidthCache() has run,
            %       converting 'min','max' to 2-element vectors
            %       leaving "-1" and "inf"
            %  - createFractionControls() has run,
            %       splitting decimal-aligned strings into integer and
            %       fractional parts so we get extents on each properly.
            %
            % What this does:
            %  - "-1" is replaced with minimum width (.CachedColumnWidths)
            %  - offsets calculated (.CachedColumnOffsets)
            %
            % Leaves:
            %  - "inf" which is resolved in computeTableLayout
            %        (in its call to interpResizeableCols)
            
            [cw,isAutoWidth] = computeColumnWidths_Init(dlg);
            computeColWidths_IntegerPart(dlg,cw,isAutoWidth);
            computeColWidths_FractionalPart(dlg,isAutoWidth);
        end
        
        function [colWidths,isAutoWidth] = computeColumnWidths_Init(dlg)
            % Check ColumnWidths (a numeric scalar, 2-element vector, or
            % cell-vector) for correct number of columns.
            %
            % Returns 2xNc matrix of col-width constraints:
            %    [ [minW_c1;maxW_c1], [minW_c2;maxW_c2], ...]
            % 
            % NOTE: Many basic property checks were done in the set-method
            % for .ColumnWidths.
            %
            % STRING TOKENS
            %   Replace string tokens ('min', 'max') with 2-element numeric
            %   width descriptors.
            %     'min' -> [-1 -1]  (use min width, disable col stretch)
            %     'max' -> [-1 inf] (use min width, enable col stretch)
            %
            % Leaves -1 (auto-width) and inf (adjustable width) values in
            % matrix for now; it's up to _IntegerPart to replace those.
            
            Nc = size(dlg.Controls,2);
            colWidths = zeros(2,Nc);  % 2xNc matrix
            v = dlg.ColumnWidths;
            
            if iscell(v)
                % Cell-array is an explicit descriptor for each column
                % - copy each entry from .ColumnWidths
                % - force 2-element vectors to be columns
                % - replicate scalars to be same value for both min/max
                % - replace 'min' with [-1;-1]
                % - replace 'max' with [-1;inf]
                if numel(v) ~= Nc
                    dialogmgr.untranslatedError([ ...
                        'Dialog "%s":\n' ...
                        '  Number of cell entries in ColumnWidths (%d)\n' ...
                        '  must equal number of table columns (%d).'], ...
                        dlg.Name, numel(v), Nc);
                end
                for i = 1:Nc
                    v_i = v{i};
                    if strcmpi(v_i,'min')
                        colWidths(:,i) = [-1;-1];
                    elseif strcmpi(v_i,'max')
                        colWidths(:,i) = [-1;inf];
                    elseif isscalar(v_i)
                        % Scalar expand
                        colWidths(:,i) = [v_i;v_i];
                    else
                        % Assume numeric 2-element vector
                        colWidths(:,i) = v_i(:); % force into column
                    end
                end
                
            elseif ischar(v) || (isstring(v) && isscalar(v))
                % Single 'min' or 'max' descriptor specified by user.
                % Produce 2xNc matrix with [-1;-1] or [-1;inf] in each col.
                colWidths = repmat(-1,[2,Nc]);
                if strcmpi(v,'max')
                    colWidths(2,:) = inf;
                end
                
            else
                % Single width descriptor specified, which should be
                % replicated for each table column. Could be a scalar or a
                % 2-element vector.
                %
                % Produce 2xNc matrix
                if isscalar(v)
                    colWidths = repmat(v,[2,Nc]);
                else
                    % 2-element vector specified, could be row or col.
                    % Force into a column and replicate Nc times.
                    colWidths = repmat(v(:),[1,Nc]);
                end
            end
            
            % Return 2xN logical matrix. Pre-compute since these -1's will
            % be overwritten in later steps.
            isAutoWidth = colWidths == -1;
        end
        
        function computeColWidths_IntegerPart(dlg,cw,isAutoWidth)
            % Computes pixel min/max widths for each table column, writing
            % results in the 2xNc matrix .CachedColumnWidths.
            % Nc is the number of columns.
            %
            % Column widths are guided by the .ColumnWidths property,
            % specified in a matrix:
            %    [ [minW_c1;maxW_c1], [minW_c2;maxW_c2], ...]
            %
            % OVERWRITES:
            %   .CachedColumnWidths
            %   .CachedColumnOffsets
            %
            % SCALAR-EXPANSION
            %   If .ColumnWidths is a scalar, it is replaced by a 2-element
            %   copy of the scalar, meaning the column min and max are the
            %   same (either a fixed pixel width or "-1").  A copy of the
            %   2-element vector is also made for each of the Nc columns.
            %
            % AUTO-WIDTH
            %   -1 tokens, implying an "automatic minimum" width, are
            %   overwritten with computed minimum widths, taking into
            %   account control extents and horizontal offsets.
            %
            % INF VALUES NOT TOUCHED
            %   Inf values are handled when computeTableLayout is called,
            %   and not touched here.
            
            ce = dlg.ControlExtents;
            [Nr,Nc] = size(ce);
            co = zeros(1,Nc); % min column pixel offsets, nominally 0
            decimal = dlg.CachedDecimalAlignCols;
            Nh = dlg.NumHeaderRows;
            
            % We need merge-set info to compute column widths
            % dmc = dlg.DeferredMergeCells;
            smc = dlg.SummaryMergeCells;
            
            % Loop over columns
            for i = 1:Nc
                % Determine horizontal offset of column.
                % - each control specifies a possibly non-zero hoffset
                %   describing its horiz positioning relative to a uitext
                %   control
                % - hoffset may represent the full column, or just the
                %   integer part if decimal alignment is being used for
                %   this column. Code here assumes strings in
                %   decimal-aligned columns were already split into integer
                %   and fractional strings/controls.
                % - include header rows in hoffset computation, as the
                %   hoffset of all controls must be considered.  This is
                %   NOT the overall width, just the hoffset.
                %
                % Horiz offset is the leftmost horizontal offset (toward
                % -inf) we find for all controls in this column:
                
                ce_i = ce(:,i);  % extent struct for controls in column i
                co(i) = min([ce_i.hoffset]);
                
                % Fill in auto-width values (-1's)
                %
                % Ignore header rows for decimal-aligned "integer" col
                % width computation.  Reason is that the header is in the
                % "integer" controls column, but should display across
                % both int/frac cols as if they are merged.  So its impact
                % should be considered vs the sum of int+frac, not just
                % int.
                % 
                autoW = isAutoWidth(:,i);
                if any(autoW)
                    % Determine minimum pixel width of column, which is the
                    % maximum control width down entire column.
                    %
                    % Horizontal offset (-ve, 0 or +ve) changes actual
                    % rendered position for the column.  Example
                    % representation of each control, including varying
                    % width and horiz offset:
                    %     |----|            (.hoffset=-2)
                    %         |--|          (.hoffset=+2)
                    %       |-----------|   (.hoffset=0)
                    %     ^             ^
                    %    min           max
                    %     |=============|
                    %         W I D T H     ( = max-min+1 )
                    
                    % Consider non-header rows if this is a
                    % decimal-aligned column.  Header can't participate
                    % in integer-only column width computation.
                    sel = true(Nr,1);
                    if decimal(i)
                        sel(1:Nh) = false;
                    end
                    
                    % We can't compute width of merged cols individually.
                    % Remove those elements from col-width consideration
                    % by logical-AND with "unmerged cell" locations:
                    sel = sel & (smc(:,i)==0);
                    
                    % In the future, we MUST deal with this as it can
                    % occur.  For now, error-out:
                    if ~any(sel)
                        dialogmgr.untranslatedError([ ...
                            'Dialog "%s":\n' ...
                            '  All elements in column %d are either merged, or are header cells\n' ...
                            '  in a decimal-aligned col.\n' ...
                            '  Remove column from the design, change mergecols specs, or\n' ...
                            '  add data rows in decimal-aligned columns.'], ...
                            dlg.Name,i);
                    end
                    
                    % Get largest width needed for column content.
                    % This is the minimum width of this "int" col:
                    %
                    % NOTE: co(i) includes all rows -- that's correct for
                    % the int/full col offset computation.
                    %
                    % Overwrite -1's in vector with minimum width
                    hpadding = [ce_i(sel).hoffset] - co(i);
                    minW_int = max([ce_i(sel).horiz] + hpadding);
                    cw(autoW,i) = minW_int;
                end
            end
            dlg.CachedColumnWidths = cw;
            dlg.CachedColumnOffsets = co;
        end
        
        function computeColWidths_FractionalPart(dlg,isAutoWidth)
            % Similar to computeColWidths_IntegerPart.
            % Updates fractional-part column widths.
            %
            % OVERWRITES
            %   .CachedFractionColumnOffsets
            %   .MinDecimalWidths
            % UPDATES
            %   .CachedColumnWidths

            decimalAlign = dlg.CachedDecimalAlignCols;
            if ~any(decimalAlign)
                dlg.CachedFractionColumnOffsets = [];
                dlg.MinDecimalWidths = [];
                return % EARLY EXIT
            end
            
            % cw_orig: 2xNc matrix, [ [minW1;maxW1] [minW2;maxW2] ... ]
            %  - Orig has the "-1's" still in it, whereas...
            %  - .CachedColumnWidths replaced those -1's with integer-part
            %    extents at this point.
            %
            ce = dlg.ControlExtents; % for header assessment
            co_int = dlg.CachedColumnOffsets;
            
            cfe = dlg.ControlFractionExtents;
            [Nr,Nc] = size(cfe);
            co = zeros(1,Nc);  % min column pixel offsets, nominally 0
            
            Nh = dlg.NumHeaderRows;
            cw_overall = dlg.CachedColumnWidths; % has widths for integer-part now
            
            % Record [int;frac] min widths for each column
            minDecimalWidths = zeros(2,Nc);
            
            for i = 1:Nc
                if decimalAlign(i)
                    % Assess fractional-part uicontrol offsets/widths
                    
                    % Remove header rows from consideration in "fractional
                    % part" column:
                    sel = true(Nr,1);
                    sel(1:Nh) = false;
                    if ~any(sel)
                        continue
                    end
                    
                    % Horiz offset for i
                    cfe_i = cfe(sel,i);
                    co(i) = min([cfe_i.hoffset]);
                    
                    autoW = isAutoWidth(:,i);
                    if any(autoW)
                        % Determine minimum pixel width of fraction column.
                        % Need horiz extent & offset of each control.
                        %
                        % See _IntegerPart function comments for details.
                        
                        row_padding = [cfe_i.hoffset] - co(i);
                        minW_frac = max([cfe_i.horiz] + row_padding);
                        
                        % Get integer-part min width, before we overwrite
                        % it with total min width.
                        %
                        % autoW is a logical vector of 2 elements, and 1 or
                        % 2 elements could be true.  Therefore we could
                        % be get a scalar or vector, so ensure a scalar.
                        minW_int = cw_overall(autoW,i);
                        minW_int = minW_int(1);
                        
                        % Record int and frac minW for later use during
                        % computeTableLayout:
                        minDecimalWidths(:,i) = [minW_int;minW_frac];
                        
                        % Update overall minimum column width by combining
                        % independently-considered int and frac widths.
                        % That's exactly what we need for decimal
                        % alignment.
                        %
                        totalW = minW_int + minW_frac;
                        cw_overall(autoW,i) = totalW;
                        
                        % If header rows (from int column) have extent
                        % wider than the int+frac computation here, which
                        % was done only over the data rows, adjust upwards.
                        %
                        % Only needed if auto-width being requested.
                        if Nh > 0
                            % Get max width over all header rows.
                            %
                            % Record header width as overall min col width
                            % if header width exceeds total width of
                            % decimal-aligned.
                            %
                            % Need integer-col info for header-row offset /
                            % width:
                            ce_i = ce(1:Nh,i);  % extent struct for col i
                            row_padding_hdr = [ce_i.hoffset] - co_int(i);
                            minW_hdr = max([ce_i.horiz] + row_padding_hdr);
                            
                            if minW_hdr > totalW
                                cw_overall(autoW,i) = minW_hdr;
                            end
                        end
                    end
                end
            end
            dlg.MinDecimalWidths = minDecimalWidths;
            dlg.CachedFractionColumnOffsets = co;
            dlg.CachedColumnWidths = cw_overall;
        end

        function computeRowHeights(dlg)
            % Computes final pixel heights for each table row.
            % No public property or client specification is provided for
            % this.  It is all automatic and must be computed.
            %
            % Takes into account actual control extents, including offsets.
            % Results are in:
            %   .CachedRowHeights
            %
            % Row height specs are in a vector, [minH_r1 minH_r2 ...].
            % There is no min/max height to deal with here.
            %
            % No interaction for parent/child dialogs, as these results are
            % computed across each rows individually.
            
            Nh = dlg.NumHeaderRows;
            ce = dlg.ControlExtents;
            cfe = dlg.ControlFractionExtents;
            Nr = size(ce,1);
            rh = zeros(Nr,1);
            ro = zeros(Nr,1); % minimum row pixel offsets, nominally 0
            
            alignDecimal = dlg.CachedDecimalAlignCols;
            hasDecimalAlign = any(alignDecimal);
            
            for i = 1:Nr
                % Determine actual minimum pixel height of row.
                % Need vertical extent & offset of each control.
                %
                % If a control has a vertical offset (either -ve or
                % +ve), it effectively increases the minimum height
                % needed to display the row.
                ce_i = ce(i,:);
                if hasDecimalAlign && i>Nh
                    cfe_i       = cfe(i,alignDecimal);
                    all_voffset = [ce_i.voffset cfe_i.voffset];
                    ro(i)       = min(all_voffset);
                    vpadding    = all_voffset - ro(i);
                    rh(i)       = max([ce_i.vert cfe_i.vert] + vpadding);
                else
                    all_voffset = [ce_i.voffset];
                    ro(i)       = min(all_voffset);
                    vpadding    = all_voffset - ro(i);
                    rh(i)       = max([ce_i.vert] + vpadding);
                end
            end
            dlg.CachedRowHeights = rh;
            dlg.CachedRowOffsets = ro;
        end
        
        function cw = getCachedColumnWidths_withChildDlgs(dlg)
            % Return .CachedColumnWidths entry, which still includes inf's,
            % but preprocessed so it's a 2xN matrix and has no -1's.
            %
            % If there are child dialogs that participate in this (parent)
            % dialog layout, the CachedColumnWidths (cw) from each child is
            % obtained and aggregated into an overall cw matrix.
            %
            % Aggregation is simple, since values of cw are min and max:
            %
            %   2xNc: [ [minW_c1;maxW_c1] [minW_c2;maxW_c2] ... ]
            
            % Parent layout info:
            cw = dlg.CachedColumnWidths;
            
            % Get the list of child dialogs whose content must participate
            % in the layout for this (parent) dialog.
            %
            % Some child dialogs use independent layout, others do not.
            ch = dlg.LayoutChildren;
            Nch = numel(ch);
            for i = 1:Nch
                % Integrate column min's and max's from child layout.
                %
                % CAREFUL!  We do the "max" on the row of minW's,
                %           and the "min" on the row of maxW's!
                % It's non-intuitive at first, but that's how these
                % constraints must be aggregated.
                %
                % Also note that min and max compute element-wise results
                % for vector inputs, so they work well for us here.
                cw_i = ch(i).CachedColumnWidths;
                cw(1,:) = max(cw(1,:),cw_i(1,:));
                cw(2,:) = min(cw(2,:),cw_i(2,:));
                %fprintf('   Layout of "%s", integrating child "%s"\n', ...
                %    dlg.Name,ch(i).Name);
            end
        end
        
        function [origin,spans,ymax] = computeTableLayout(dlg)
            % Return structure with table layout information, given width
            % of available display region W, in pixels:
            %
            % Always returns:
            %  origin.x = [x1 x2 ...]
            %  origin.y = [y1 y2 ...]
            %  span.dx  = [dx1 dx2 ...]
            %  span.dy  = [dy1 dy2 ...]
            %
            % Optionally returns:
            %  origin.x_frac
            %  span.dx_int
            %  span.dx_frac
            %
            % Returns lower-left corner coordinate of each control in
            % table, using .x(rowIdx) and .y(colIdx).
            % Assumes pixel coords and (1,1) is the origin.
            %
            % maxX is the maximum x (column) pixel occupied, and will be
            % <= W. The table may not use the W pixel width if no column
            % width has an unbounded maximum (i.e., Inf).  It takes into
            % account control widths, including any offsets.
            %
            % maxY is the maximum y (row) pixel occupied and is
            % unconstrained (no bound provided).  It takes into account
            % control heights, including any offsets.
            %
            % It is assumed origin is needed quickly during resize events,
            % while ymax and span are needed only at initialization and
            % infrequently otherwise.
            
            % Compute total height of dialog (ymax)
            %
            ics = dlg.InterColumnSpacing;
            irs = dlg.InterRowSpacing;
            ibs = dlg.InnerBorderSpacing;
            %
            % First row is highest y-coord
            % Only retain rows that are visible
            %
            %  ymax ---------------- ibs   (3 pix)   ymax=70
            %       - row 3        - rh(3) (20 pix)
            %       -              - irs   (2 pix)
            %       - row 2        - rh(2) (20 pix)
            %       -              - irs   (2 pix)
            %       - row 1        - rh(1) (20 pix)
            %     1 ---------------- ibs   (3 pix)
            %
            rh = dlg.CachedRowHeights(dlg.RuntimeRowVisibility).';
            ymax = 2*ibs + (numel(rh)-1)*irs + sum(rh);
            
            % Check if this dialog is a child that has its layout
            % controlled by a parent.
            useParentLayout = ~isempty(dlg.LayoutParent);
            if useParentLayout
                % Dialog layout is controlled by a parent dialog layout.
                %
                % Use the "Last" cache entries from the layout parent,
                % as those will have the parent information.
                %
                % Get that layout to use for:
                %   origin.x
                %   spans.dx
                %
                % If the Last cache holds them, also use:
                %   - origin.x_frac
                %   - spans.dx_int
                %   - spans.dx_frac
                % Otherwise these quantities will be computed locally.
                
                origin = dlg.LayoutParent.LastLayoutOrigin;
                spans = dlg.LayoutParent.LastLayoutSpans;
                cw = spans.dx;
            else
                % Locally compute layout elements:
                %   - origin.x
                %   - spans.dx (= cw)
                %
                % Setup rh and ymax for later computation of:
                %   - origin.y
                %   - spans.dy
                
                % The last step of layout computation is interp/stretching
                % of resizable columns, which uses the overall column
                % widths (cw) matrix:
                %   2xNc: [ [minW_c1;maxW_c1] [minW_c2;maxW_c2] ... ]
                %
                % With child dialogs, we aggregate the min's and max's
                % together before handing off to the resize-engine.
                
                % If dialog has any child that sets
                % ParticipateInParentLayout, we narrow the dialog layout
                % width to compensate for the left shift by child dialogs,
                % and the subsequent right shift compensation by all
                % controls in the parent.  Overall this means the dialog
                % needs narrowing by offset number of pixels, if there's
                % any "inf" spacing available to accommodate this offset.
                %
                % Makes sense, right?
                parentLayoutShift = maxShiftOfChildrenParticipatingInParentLayout(dlg);
                
                W_inner = getInnerWidth(dlg.DialogBorder);
                cw = getCachedColumnWidths_withChildDlgs(dlg);
                cw = interpResizeableCols(cw,W_inner-parentLayoutShift,ibs,ics);
                spans.dx = cw;
                origin.x = cumsum([ibs+1 ics+cw(1:end-1)]);
            end
            
            % Always (re-)compute y-related info locally:
            %
            %  ymax ---------------- ibs   (3 pix)    ymax=70
            %       - row 3        - rh(3) (20 pix)
            %       -              - irs   (2 pix)
            %       - row 2        - rh(2) (20 pix)
            %       -              - irs   (2 pix)
            %       - row 1        - rh(1) (20 pix)
            %     1 ---------------- ibs   (3 pix)
            %
            %  y = 70+1-3+2-cumsum([20 20 20]+2) = [48 26 4]
            spans.dy = rh;
            origin.y = ymax+1-ibs+irs-cumsum(rh+irs);
            
            % Compute layout info for decimal-aligned columns.
            %
            % This info may not be present in Last cache, if the parent
            % didn't have decimal-aligned cols but the child layout does.
            % Compute locally if cache didn't hold this.
            %
            if ~useParentLayout || ~isfield(origin,'x_frac')
                % Compute layout for decimal-aligned columns.
                % Quantities we need:
                %   - origin.x_frac
                %   - spans.dx_int
                %   - spans.dx_frac
                
                colIdx = find(dlg.CachedDecimalAlignCols);
                if ~isempty(colIdx)
                    minWidths = dlg.MinDecimalWidths;
                    horizAlign = dlg.CachedHorizontalAlignment;
                    Nc = numel(colIdx);
                    for i = 1:Nc
                        % We want to be "static" in our resizing of the
                        % int/frac columns.  Get minimum pixel widths of
                        % integer and fractional part, cached from controls
                        % for this column:
                        c = colIdx(i);
                        minW = minWidths(:,c); % [int;frac], in pixels
                        
                        % HorizontalAlignment should no longer be set to
                        % 'inherit' at this point.
                        switch horizAlign{c}
                            case 'left'
                                dxi = minW(1);     % integer: fixed min width
                                dxf = cw(c) - dxi; % fractional: rest of col
                            case 'right'
                                dxf = minW(2);     % fraction: fixed min width
                                dxi = cw(c) - dxf; % integer: rest of col
                            case 'center'
                                % We might not fit the decimal-aligned
                                % number in the center.  Try it, then back
                                % off to fit.
                                dxi = floor(cw(c)/2);
                                if dxi < minW(1)
                                    dxi = minW(1);
                                end
                                dxf = cw(c) - dxi;
                                if dxf < minW(2)
                                    dxf = minW(2);
                                    dxi = cw(c) - dxf;
                                end
                            case 'inherit'
                                % This should have been re-mapped to one of
                                % the above
                                assert(0);
                            otherwise
                                dialogmgr.untranslatedError([ ...
                                    'Dialog "%s":\n' ...
                                    '  Unsupported HorizontalAlignment option ''%s'' for decimal-aligned columns.'], ...
                                    dlg.Name, horizAlign{c});
                        end
                        origin.x_frac(c) = origin.x(c) + dxi;
                        spans.dx_int(c)  = dxi;
                        spans.dx_frac(c) = dxf;
                    end
                end
            end % locally computed decimal-aligned layout
        end
        
        function [origin,spans,ymax] = updateTableLayout_height(dlg,origin,spans,rrv)
            % Return structure with table layout information, given that
            % width has not changed, and only the height (visibility) of
            % rows is changing.
            %
            % Updates origin.y, spans.dy and ymax.
            % Leaves origin.x, spans.dx, and all other fields as-is
            %
            % Does NOT change any state in dialog properties.
            % Use computeTableLayout() for comprehensive layout.
            
            if nargin<4
                rrv = dlg.RuntimeRowVisibility;
            end
            irs = dlg.InterRowSpacing;
            ibs = dlg.InnerBorderSpacing;

            % Select visible entries, in a row vec suitable for cumsum
            % First row is highest y-coord.
            % Only retain rows that are visible.
            rh = dlg.CachedRowHeights(rrv).';
            Nr = numel(rh);
            ymax = 2*ibs + (Nr-1)*irs + sum(rh);

            % Compute starting coordinates for each row and column,
            % including borders and margins:
            origin.y = ymax+1-ibs+irs-cumsum(rh+irs);
            spans.dy = rh;
        end
        
        function initCommonControlProps(dlg)
            % Set Units, Enable, HorizontalAlignment parameters on
            % uicontrols.
            %
            % Apply text style to header row controls.
            %
            % Remap HorizontalAlignment 'inherit' values from column
            % specifier.
            
            notPlaceholder = dlg.NotPlaceholderControl;
            horizAlign = dlg.CachedHorizontalAlignment;
            decimalAlign = dlg.CachedDecimalAlignCols;
            mergeCols = dlg.DeferredMergeCells;
            Nmc = numel(mergeCols);
            c = dlg.Controls;
            cf = dlg.ControlsFraction;
            Nh = dlg.NumHeaderRows;
            [R,C] = size(c);
            
            for i = 1:R
                % For this row, determine if each column participates in a
                % merged set of columns.  Prepare info to answer that:
                has_merges = (i<=Nmc) && ~isempty(mergeCols{i});
                if has_merges
                    Nmc_i = cellfun(@numel,mergeCols{i}); % # indices in each merge-set
                    Nmc_cs_i = cumsum(Nmc_i);
                end
                    
                for j = 1:C
                    if notPlaceholder(i,j)
                        % Is the (i,j) control in a set of merged cols?
                        %
                        % Determine cell entry in which this col idx
                        % appears.  If cell entry has a vector of col
                        % indices, this col is in a set of merged cols.
                        is_merged_cell = has_merges && ...
                            Nmc_i(find(j<=Nmc_cs_i,1,'first'))>1;
                        
                        % Enable text controls to be dragged in Dialog Manager
                        % by setting them to inactive.
                        if strcmpi(c{i,j}.Style,'text')
                            c{i,j}.Enable = 'inactive';
                        end
                        % Always set pixel units on main control
                        c{i,j}.Units = 'pixels';
                        
                        % Identify 'inherit' values for HorizAlign, and
                        % remap according to setting on uicontrol.
                        horiz_j = horizAlign{j};
                        if strcmpi(horiz_j,'inherit')
                            % Decimal-aligned columns should throw an error
                            % if 'inherit' is chosen.  We can re-map this
                            % value, of course, but the decimals will not
                            % align if each control in the column chooses a
                            % different alignment (left/center/right)!  So
                            % we protect the user from this.
                            if decimalAlign(j)
                                dialogmgr.untranslatedError([ ...
                                    'Dialog "%s":\n' ...
                                    '   HorizontalAlignment for column %d cannot be set to\n' ...
                                    '  ''inherited'' when DecimalAlign is enabled for the column.\n' ...
                                    '\n' ...
                                    '  Set HorizontalAlignment to ''left'', ''center'' or ''right'' for ' ...
                                    '  decimal-aligned columns.\n'], ...
                                    dlg.Name, j);
                            end
                            
                            % Otherwise, remap 'inherit' to whatever value
                            % is specified in the user control:
                            horiz_j = c{i,j}.HorizontalAlignment;
                            horizAlign{j} = horiz_j;
                        end
                        
                        if decimalAlign(j) && i>Nh
                            % Decimal-aligned column, after header rows
                            
                            if ~strcmpi(cf{i,j}.Style,'text')
                                dialogmgr.untranslatedError([ ...
                                    'Dialog "%s":' ...
                                    '  Decimal-aligned column (col %d) must contain a text uicontrol.\n'], ...
                                    dlg.Name,j);
                            end
                            
                            % Set pixel units on fractional-part text
                            cf{i,j}.Units = 'pixels';
                            
                            % Set fractional-part text to inactive
                            % (A bit overkill on confirming it's text;
                            % decimal alignment only works with text!)
                            cf{i,j}.Enable = 'inactive';
                            
                            % Override horizontal alignment to achieve
                            % decimal alignment:
                            c{i,j}.HorizontalAlignment = 'right';
                            cf{i,j}.HorizontalAlignment = 'left';
                        else
                            % Non-decimal aligned, or header row.
                            
                            % Apply style to header row text controls
                            %
                            if i<=Nh && strcmpi(c{i,j}.Style,'text')
                                c{i,j}.FontWeight = 'bold';
                            end
                            
                            % Horizontal alignment for column is applied to
                            % controls only in non-merged columns:
                            if ~is_merged_cell
                                c{i,j}.HorizontalAlignment = horiz_j;
                            end
                        end
                    end % ~ GraphicsPlaceholder
                end
            end
            
            % Store any updates to horizontal alignment:
            dlg.CachedHorizontalAlignment = horizAlign;
        end
        
        function maxShift = maxShiftOfChildrenParticipatingInParentLayout(dlg)
            % True if any child uidialog sets ParticipateInParentLayout
            
            % NOTE: Assumes uidialog controls only appear in column 1.
            
            maxShift = 0;
            c = dlg.Controls;
            Nr = size(c,1);
            for i = 1:Nr
                c_i = c{i,1};
                if isa(c_i,'dialogmgr.uidialog') && c_i.ParticipateInParentLayout
                    % child is a uidialog that participates in parent
                    % layout.
                    %
                    % The "shift" is the number of horiz pixels we must
                    % narrow the parent in order to compensate for various
                    % left/right shifts of content.
                    %
                    % offset is width of any interior graphical border,
                    % typical in a uipanel, and we determine it by
                    % subtraction:
                    db_i = dlg.DialogBorder; % parent dlg
                    innerBorderOffset_i = floor((getOuterWidth(db_i) - getInnerWidth(db_i))/2);
                    shift_i = dlg.InnerBorderSpacing + innerBorderOffset_i;
                    maxShift = max(maxShift,shift_i);
                end
            end
        end
    end
    
    methods (Access=protected)
        function connectRowVisToControl(dc,varargin)
            % connectRowVisToControl(dlg,ctrlTags,uiHandle,uiValue,visValue)
            %
            % Hide or show table row implied by the row in which tag
            % ctrlTags is found.  ctrlTags may be a string or a cell-string
            % in order to control the visibility of multiple rows.
            %
            % Visibility is changed to visValue, based on uiHandle having a
            % Value or a String equal to the specified uiValue. uiValue may
            % be a scalar or vector of values, or may be a string or cell
            % vector of strings.
            %
            % When test is true, visibility is set to visValue, which
            % defaults to false.
            
            if dc.IsLazyLoadPhase
                return
            end
            
            % Cache args, implement after controls are defined:
            dc.DeferredRowVisArgs{end+1} = [varargin 'RowVis'];
        end
        
        function connectRowEnableToControl(dc,varargin)
            % connectRowEnableToControl(dlg,ctrlTags,uiHandle,uiValue,enaValue)
            %
            % Enable or disable table row implied by the row in which tag
            % ctrlTags is found.  ctrlTags may be a string or a cell-string
            % in order to control the visibility of multiple rows.
            %
            % Enable is changed to enaValue, based on uiHandle having a
            % Value or a String equal to the specified uiValue. uiValue may
            % be a scalar or vector of values, or may be a string or cell
            % vector of strings.
            %
            % When the test is true, enable is set to enaValue, which
            % defaults to true.
            
            if dc.IsLazyLoadPhase
                return
            end
            
            % Cache the args to implement after controls are defined.
            dc.DeferredRowEnableArgs{end+1} = [varargin 'RowEna'];

        end
        
        function connectElementEnableToControl(dc,varargin)
            % connectElementEnableToControl(dlg,ctrlTags,uiHandle,uiValue,enaValue)
            %
            % Enable or disable individual controls referenced by tag
            % strings in ctrlTags.  ctrlTags may be a string or a
            % cell-string in order to control the visibility of multiple
            % elements.
            %
            % Enable is changed to enaValue, based on uiHandle having a
            % Value or a String equal to the specified uiValue. uiValue may
            % be a scalar or vector of values, or may be a string or cell
            % vector of strings.
            %
            % When the test is true, enable is set to enaValue, which
            % defaults to true.
            
            if dc.IsLazyLoadPhase
                return
            end
            
            % Cache the args to implement after controls are defined.
            dc.DeferredElementEnableArgs{end+1} = [varargin 'ElementEna']; % hold cell
        end
        
        function connectElementVisToControl(dc,varargin)
            % connectElementVisToControl(dlg,ctrlTags,uiHandle,uiValue,enaValue)
            %
            % Show or hide individual controls referenced by tag
            % strings in ctrlTags.  ctrlTags may be a string or a
            % cell-string in order to control the visibility of multiple
            % elements.
            %
            % Visibility is changed to visValue, based on uiHandle having
            % a Value or a String equal to the specified uiValue. uiValue
            % may be a scalar or vector of values, or may be a string or
            % cell vector of strings.
            %
            % When the test is true, vis is set to visValue, which
            % defaults to true.
            
            if dc.IsLazyLoadPhase
                return
            end
            
            % Cache the args to implement after controls are defined.
            dc.DeferredElementVisArgs{end+1} = [varargin 'ElementVis'];
        end
    end
    
    % General vis/enable action support
    methods (Access=private)
        function ret = combineSameSourceActions(dc,args)
            % Identifies and groups dialog visibility/enable actions that
            % are based on same source control.
            %
            % args: cell-vector, one per "test for action"
            %      = { {test1} {test2} .... }
            %    testN: another cell-vector of args,
            %        {ctrlTags,uiHandle,uiValue,state,actionType}
            %
            %   actionType is a string:
            %         'RowVis','ElementVis','RowEna','ElementEna'
            %
            %    - uiHandle is the "source" of the mouse-interaction,
            %      a handle to a uicontrol that just changed.  We group
            %      actions that have the same uiHandle.
            %
            % Returns a vector of structs
            %   ret(i).uiHandle
            %   ret(i).actions.uiValue
            %   ret(i).actions.DlgDst
            %   ret(i).actions.RowIdx
            %   ret(i).actions.ColIdx
            %   ret(i).actions.State
            %     .State: on input, state of action when uiValue is met
            %             on output, state of action to install
            %             action is typically used for Enable or Visible
            %
            % Handles both "local" and "hierarchical" references.
            %         Local: tag name
            %  Hierarchical: tag name with '/' in its name
            
            ret = [];
            
            % Assign indices from 1 to N, where N is the # of unique
            % uiHandles.
            idx = groupSameSourceHandles(args);
            
            if ~isempty(idx)
                max_i = max(idx); % Ex: idx=[1 1 1 2 2 3]
                for i = 1:max_i
                    % Get subset of tests with common uiHandle
                    the_rows = args(idx==i);
                    
                    % get uiHandle from any row since they're identical by
                    % definition due to the grouping by same-source:
                    uiHandle = the_rows{1}{2};
                    
                    % Expand uiHandle if there's a control-group with
                    % identical callbacks
                    grp = getappdata(uiHandle,'RadiobuttonGroup');
                    if ~isempty(grp)
                        uiHandle = grp;
                    end
                    
                    % Group all actions for this uiHandle (or group of
                    % handles with same set of actions, in the case of
                    % linked radiobuttons)
                    %
                    % Put them into a vector of structs
                    %   ret(i).uiHandle
                    %   ret(i).actions.uiValue
                    %   ret(i).actions.DlgDst
                    %   ret(i).actions.RowIdx
                    %   ret(i).actions.ColIdx
                    %   ret(i).actions.State
                    %
                    actions = [];
                    for j = 1:numel(the_rows)
                        % one_row: {ctrlTags,uiHandle,uiValue,state}
                        row_j = the_rows{j};
                        u.uiValue = row_j{3};
                        [u.RowIdx,u.ColIdx,u.DlgDst] = ...
                            findIndicesFromControlTags(dc,row_j{1});
                        u.State = repmat(row_j{4},size(u.RowIdx));
                        u.ActionType = row_j{5}; % 'RowVis', etc
                        if isempty(actions)
                            actions = u;
                        else
                            actions(j) = u; %#ok<AGROW>
                        end
                    end
                    
                    t.uiHandle = uiHandle;
                    t.actions  = actions;
                    if isempty(ret)
                        ret = t;
                    else
                        ret(i) = t; %#ok<AGROW>
                    end
                end
            end
        end
        
        function [rowIdx,colIdx,dlgDst] = findIndicesFromControlTags(dc,ctrlTags)
            % Find element indices of uicontrols with matching tag strings.
            % One string or a cell-vector of strings may be specified.
            %
            % Parses tag to see if it has a hierarchical reference.
            %        Regular tag: 'My_Tag'
            %   Hierarchical tag: '..\Other Dialog\My_OtherTag'
            %                     (any '/' or '\' char)
            %
            % All regular tags are converted to matrix row/col indices.
            % All hierarchical tags are returned in hTags.
            
            % Make sure ctrlTags is a cellstr.
            if ischar(ctrlTags) || (isstring(ctrlTags) && isscalar(ctrlTags))
                ctrlTags = {ctrlTags};
            end
            if ~iscellstr(ctrlTags) && ~isstring(ctrlTags)
                dialogmgr.untranslatedError([ ...
                    'Dialog "%s":\n' ...
                    '  Tag references must be specified as a string or a cellstring.'], ...
                    dc.Name);
            end
            
            % Identify tags as: dialog path, row index, col index
            % Allocate maximum of N for each
            N = numel(ctrlTags);
            rowIdx = zeros(N,1);
            colIdx = zeros(N,1);
            dlgDst = [];
            rTagCnt = 0;
            dp = dc.DialogPresenter;
            isWarn = false;
            
            for i = 1:N
                % Parse tag for hierarchical reference:
                tag_i = ctrlTags{i};
                
                if ~isempty(fileparts(tag_i))
                    % Hierarchical reference (has path in name)
                    [dlg_i,tag_i] = mapHierarchicalRefToHandle(dp,tag_i);
                    if isempty(dlg_i)
                        dc_i = [];
                        isWarn = true;
                    else
                        dc_i = dlg_i.DialogContent;
                    end
                else
                    % Local reference
                    dc_i = dc;
                    %dlg_i = dc.Dialog;
                end
                
                if ~isWarn
                    % Find linear index then convert to 2D matrix index:
                    idx = find(strcmpi(tag_i,dc_i.ControlTags));
                    isWarn = isempty(idx);
                end
                
                if isWarn
                    [Msg, Warn] = dialogmgr.untranslatedWarning([ ...
                        'Dialog ''%s'':\n' ...
                        '  Specified tag ''%s'' not found.'], ...
                        dc.Name, tag_i);
                    warning(Msg,Warn);
                else
                    rTagCnt = rTagCnt+1;
                    [rowIdx(rTagCnt),colIdx(rTagCnt)] = ind2sub(size(dc_i.Controls),idx);
                    if isempty(dlgDst)
                        dlgDst = dc_i;
                    else
                        dlgDst(end+1,1) = dc_i; %#ok<AGROW>
                    end
                end
            end
            % Trim return vectors:
            rowIdx = rowIdx(1:rTagCnt);
            colIdx = colIdx(1:rTagCnt);
        end
    end
    
    % RowVisibility
    methods (Access=private)
        function initControlVisibility(dlg)
            % Track runtime-visibility of each row using a vector, with
            % one entry per row of controls.  All are visible at first:
            dlg.RuntimeRowVisibility = true(size(dlg.Controls,1),1);
            
            % Also track runtime-vis of each element using a logical
            % matrix, so we can compute interaction between row-vis and
            % element-vis.
            % - When row vis is off, it takes priority over element vis
            % - When row vis is on, element vis is taken into account
            dlg.RuntimeElementVisibility = true(size(dlg.Controls));
        end
        
        %{
        function connectAllVisToControl_Aggregate(dlg)
            % Aggregate the ElementVis and RowVis actions.
            
            % Aggregate across all row-vis actions
            %  - identify all entries that are based on same source control
            %  - uicontrols responsible for notifying us of a mouse
            %    interaction are in rowVisArgs{i}{2}
            argList = [ ...
                dlg.DeferredRowVisArgs ...
                dlg.DeferredElementVisArgs ...
                dlg.DeferredRowEnableArgs ...
                dlg.DeferredElementEnableArgs ];
            argStruct  = combineSameSourceActions(dlg,argList);

            keyboard % xxx
        end
        %}
    
        function connectRowVisToControl_Install(dlg)
            % Initialize the deferred row vis
            %
            % Called individually on each top-level dialog of the
            % presenter.
            
            % Get args for row-vis control tests
            %  rowVisArgs: cell-vector, one per "test+reaction"
            %  rowVisArgs{i}: another cell-vector of args,
            %          {ctrlTags,uiHandle,uiValue,visValue}
            %          uiHandle is the "source" of the mouse-interaction
            %
            rowVisArgs = dlg.DeferredRowVisArgs;
            if isempty(rowVisArgs)
                return % EARLY EXIT
            end
            dlg.DeferredRowVisArgs = {}; % clear the entries
            
            % Aggregate across all row-vis actions
            %  - identify all entries that are based on same source control
            %  - uicontrols responsible for notifying us of a mouse
            %    interaction are in rowVisArgs{i}{2}
            rowVisStruct = combineSameSourceActions(dlg,rowVisArgs);
            
            % Install listeners, and perform one-time actions, for control
            % action on row visibilities.
            %
            % rowVisStruct: Vector of structs, each having fields:
            %    .uiHandle: typically a scalar handle, but could be a
            %         vector if all handles will have the exact same
            %         test and same actions (ex: radiobutton group)
            %
            %    .actions: a vector of structs, each having fields:
            %        .uiValue: value for which the condition will be true;
            %                 could be a numeric scalar, a cellstring, etc
            %        .DlgDst: handle to Dialog
            %        .RowIdx: vector of row indices whose
            %                 visibility will be updated
            %         .State: same length vector of logicals,
            %                 indicating vis state if uiValue test passes
            %
            % One struct per handle-interaction test.
            % There may be multiple handles in each entry.
            
            for i = 1:numel(rowVisStruct)
                % Listen to changes to controls, and update vis properties
                % due to the change.
                rvs_i = rowVisStruct(i);
                
                % Get all actions related to this uiHandle
                % same action for each handle (j-iteration)
                a_j = rvs_i.actions;
                
                Nc = numel(rvs_i.uiHandle);
                for j = 1:Nc
                    % Get next uiHandle - could be a set for radiobuttons
                    h_j = rvs_i.uiHandle(j);
                    addlistener(h_j,'Action', ...
                        @(~,~)updateRowVisibility_Action(h_j,a_j,false));
                end
                
                % Execute manually to force vis to be updated.
                %
                % Pass "true" for initialization (no height change)
                %
                % Just execute for the last handle, if multiple specified
                % for a group of uiHandles (like a radiobuttongroup).
                updateRowVisibility_Action(h_j,a_j,true);
            end
            
            % Calls to changeDialogContentHeight are suppressed above, as
            % this is an initialization sequence.  A single call is made on
            % each dialog in installDeferredDialogActions().
        end
        
        function connectElementVisToControl_Install(dlg)
            % Initialize the deferred element visibility
            %
            % Called individually on each top-level dialog of the
            % presenter.
            
            % Get args for ele-vis control tests
            %  eleVisArgs: cell-vector, one per "test+reaction"
            %  eleVisArgs{i}: another cell-vector of args,
            %          {ctrlTags,uiHandle,uiValue,visValue}
            %          uiHandle is the "source" of the mouse-interaction
            %
            eleVisArgs = dlg.DeferredElementVisArgs;
            if isempty(eleVisArgs)
                return % EARLY EXIT
            end
            dlg.DeferredElementVisArgs = {}; % clear the entries
            
            % Aggregate across all row-vis actions
            %  - identify all entries that are based on same source control
            %  - uicontrols responsible for notifying us of a mouse
            %    interaction are in rowVisArgs{i}{2}
            eleVisStruct = combineSameSourceActions(dlg,eleVisArgs);
            
            % Install listeners, and perform one-time actions, for control
            % action on row visibilities.
            %
            % eleVisStruct: Vector of structs, each having fields:
            %    .uiHandle: typically a scalar handle, but could be a
            %         vector if all handles will have the exact same
            %         test and same actions (ex: radiobutton group)
            %
            %    .actions: a vector of structs, each having fields:
            %        .uiValue: value for which the condition will be true;
            %                 could be a numeric scalar, a cellstring, etc
            %        .DlgDst: handle to Dialog
            %        .RowIdx: vector of row indices whose
            %                 visibility will be updated
            %       .ColIdx: similar
            %        .State: same length vector of logicals,
            %                indicating vis state if uiValue test passes
            %
            % One struct per handle-interaction test.
            % There may be multiple handles in each entry.
            
            for i = 1:numel(eleVisStruct)
                % Listen to changes to controls, and update vis properties
                % due to the change.
                rvs_i = eleVisStruct(i);
                
                % Get all actions related to this uiHandle
                % same action for each handle (j-iteration)
                a_j = rvs_i.actions;
                
                Nc = numel(rvs_i.uiHandle);
                for j = 1:Nc
                    % Get next uiHandle - could be a set for radiobuttons
                    h_j = rvs_i.uiHandle(j);
                    addlistener(h_j,'Action', ...
                        @(~,~)updateElementVis_Action(h_j,a_j));
                end
                
                % Execute manually to force vis to be updated.
                %
                % Pass "true" for initialization (no height change)
                %
                % Just execute for the last handle, if multiple specified
                % for a group of uiHandles (like a radiobuttongroup).
                updateElementVis_Action(h_j,a_j);
            end
            
            % Calls to changeDialogContentHeight are suppressed above, as
            % this is an initialization sequence.  A single call is made on
            % each dialog in installDeferredDialogActions().
        end
        
        function info = createAnimationPlan_rowHeights(dlg,rrv,rowSharing)
            % Compute info struct containing pre- and post-animation layout
            % data for control row heights in this dialog.
            %
            % rowSharing must contain row indices of all control rows that
            % are or will be visible.  Any control rows that are currently
            % invisible and remain invisible may be omitted.
            %
            % Info is a struct with these fields:
            %
            %  .Pre - pre-animation spacing
            %        .y: y-coordinate of each control; includes merge of
            %            rows that will overlay each other during animation
            %            as defined in plan.rowSharing
            %       .dy: height of each control
            %     .ymax: total height of content including inner-border and
            %            inter-row spacing
            %      .rrv: RuntimeRowVisibility, logical vector
            % .rrv_suppressSharedRows: same as .rrv, except for shared rows,
            %     only one of the shared rows is true - the rest are false.
            %     This is used to coax height-layout to work.
            %
            %  .Post - post-animation spacing
            %     same fields
            %     only includes final visible rows
            
            % ------------------------------
            % Pre-animation layout info
            %
            
            % Compute y and dy for each pre-animation visible row.
            % But hold these in vectors with number of elements equal to
            % the number of defined controls.
            Nc = size(rrv,1); % = # controls
            dy_rows = zeros(Nc,1);
            y_rows = zeros(Nc,1);
            
            % Get heights of each pre-anim visible row.
            %
            % Compute max-heights for merged row, using it for each
            % merged row.
            %
            % Compute from BOTTOM to top, so we can also compute y-offset
            % for each row.
            %
            % For merged rows, we set the y-offset so the TOPS of both
            % merged rows is the same.  Which means the y-offset will
            % differ if the two merged rows don't hvae the same dy. (Not
            % dy_pre_rows, the real dy!  dy_pre_rows has the SAME dy for
            % merged rows!)
            irs = dlg.InterRowSpacing;
            ibs = dlg.InnerBorderSpacing;
            cached_row_heights = dlg.CachedRowHeights;
            rrv_oneOfSharedRowsOnly = true(Nc,1);
            
            % y-coord of first control
            % - maintain inner-border space, y=1 if no space
            y = 1+ibs;
            
            Nrs = numel(rowSharing);
            for i = Nrs:-1:1
                % Compute y and "max dy"
                %  - shared rows occupy the same row during animation
                %  - find the max height across these shared rows
                %  - if only one row in set, max won't hurt
                shared_i = rowSharing{i};
                
                % Overwrite false where shared rows are located, other than
                % one row in the set of shared rows.  This is for
                % max-height computation by another method that needs to
                % see only one of the shared-set enabled for it to compute
                % the right max-height.
                %
                % Retain first (lowest) index of shared rows:
                %max_dy = max(cached_row_heights(shared_i));
                %if ~isscalar(shared_i)
                %    rrv_oneOfSharedRowsOnly(shared_i(2)) = false;
                %end
                %
                % Retain the first (lowest) index with the max height in
                % the shared group: there are only 1 or 2 in each group.
                [max_dy,max_idx] = max(cached_row_heights(shared_i));
                if ~isscalar(shared_i)
                    rrv_oneOfSharedRowsOnly( shared_i(3-max_idx) ) = false;
                end
                
                for j = numel(shared_i):-1:1
                    row_i = shared_i(j);
                    this_dy = cached_row_heights(row_i);
                    % y-coords for shared rows must align at top edge:
                    y_rows(row_i) = y + max_dy-this_dy;
                    dy_rows(row_i) = this_dy;
                end
                y = y + max_dy + irs; % next visible row
            end
            
            % Total height is y, but remove the last "irs" addition, and
            % add in the upper inner-border spacing:
            ymax = y-irs+ibs-1;
            
            % Only include info for rows visible during animation
            rrv_pre = rrv(:,1) | rrv(:,2);
            
            % Summary of pre-animation data
            % - has info for all control rows, not just visible rows
            info.pre.y    = y_rows(rrv_pre);
            info.pre.dy   = dy_rows(rrv_pre);
            info.pre.ymax = ymax;
            info.pre.rrv  = rrv_pre;
            info.pre.rrv_suppressSharedRows = rrv_pre & rrv_oneOfSharedRowsOnly;
            
            % ------------------------------
            % Post-animation layout info
            %
            
            % select visible entries, in a row vec suitable for cumsum
            % First row is highest y-coord.
            % Only retain rows that are visible.
            dy_rows = cached_row_heights(rrv(:,2));
            Nr = numel(dy_rows);
            ymax = 2*ibs + (Nr-1)*irs + sum(dy_rows);

            % Compute starting coordinates for each row and column,
            % including borders and margins:
            y_rows = ymax+1 -ibs+irs-cumsum(dy_rows+irs);
            
            % Summary of post-animation data
            % - has info only for visible rows
            info.post.y = y_rows;
            info.post.dy = dy_rows;
            info.post.ymax = ymax;
            info.post.rrv = rrv(:,2);
            info.post.rrv_suppressSharedRows = rrv(:,2);
        end
        
        function rowSharing = createAnimationPlan_rowGroups_fast(dlg,rrv) %#ok<INUSL>
            % Determine which rows should be placed at the same row-height
            % in the dialog during animation of visibility.  The "fast"
            % algorithm simply pairs-up adjacent rows that are changing
            % visibility.
            %
            % Returns rowSharing:
            %    A cell-vector with scalar or 2-element vectors of row
            %    indices.  Each entry indicates rows that should appear at
            %    the same height within the dialog.  All control rows that
            %    are or will become visible are present once in rowSharing.
            %
            % Input rrv is an Nx2 RuntimeRowVisibility vector, where:
            %    - col 1 is original row vis
            %    - col 2 is the new row vis
            %
            % Each row of rrv is represented by an index in the plan.
            %
            % All rows in rrv (and in the plan) animate simultaneously,
            % whether it's to become visible or invisible. It's simply a
            % matter of vertical positioning of rows: which rows will align
            % as they animate?
            %
            % Example 1
            %  rrv: [ 1 1   (visible and not changing)
            %         1 0   (going invisible)
            %         0 1   (going visible)
            %         1 1]  (visible and not changing)
            %  rowSharing = {1,[2 3],4}
            %  Rows 2 and 3 will animate at the same height in the dialog,
            %  with height that is the maximum of the heights of control
            %  row 2 and control row 3.
            %
            % Example 2
            %  rrv: [ 1 1   (visible and not changing)
            %         1 0   (going invisible)
            %         0 0   (invisible and not changing)
            %         0 1   (going visible)
            %         1 1]  (visible and not changing)
            %  rowSharing = {1,[2 4],5}
            %  Rows 2 and 4 will animate at the same height in the dialog,
            %  with height that is the maximum of the heights of control
            %  row 2 and control row 4.
            %
            % Example 3
            %  rrv: [ 1 1
            %         1 0
            %         0 0
            %         1 1]
            %  rowSharing = {1,2,4}
            %  No sharing will take place.
            
            % Compute visChangeGroups
            %  - sparse list of dialog row indices
            %  - only rows that have a vis change are captured
            %  - usually rows are listed individually, meaning they don't
            %    share the y-position with another row
            %  - some rows are listed in pairs, meaning they will share the
            %    y-position with each other (they will "cross paths" as
            %    they animate)
            N = size(rrv,1);
            visChangeGroups = cell(N,1); % preallocate to max elements
            lastVisChangeDir = 0;
            lastVisChangeRow = 0;
            p_i = 0;
            for i = 1:N
                visChangeDir = rrv(i,2) - rrv(i,1);
                if lastVisChangeDir ~= 0
                    if visChangeDir == 0
                        % A previous row had a vis change and was
                        % unrecorded.  No change in vis for this row.
                        
                        if rrv(i,2)==0
                            % No change in vis this row, and it's invis.
                            % Retain and keep watching.
                        else
                            % No change in vis for this row, and it's vis.
                            % Record last vis change we saw.
                            p_i = p_i+1;
                            visChangeGroups{p_i} = lastVisChangeRow;
                            lastVisChangeDir = 0;  % reset
                        end
                        
                    elseif visChangeDir ~= lastVisChangeDir
                        % Adjacent row changes with different "directions"
                        p_i = p_i+1;
                        visChangeGroups{p_i} = [lastVisChangeRow i];
                        lastVisChangeDir = 0;  % reset
                    else
                        % Last row had same vis change as this row
                        p_i = p_i+1;
                        visChangeGroups{p_i} = lastVisChangeRow;
                        p_i = p_i+1;
                        visChangeGroups{p_i} = i;
                        lastVisChangeDir = 0;  % reset
                    end
                else
                    if visChangeDir==0 && rrv(i,1)
                        % no change in vis, and row was/still is visible
                        p_i = p_i+1;
                        visChangeGroups{p_i} = i;
                    end
                    lastVisChangeDir = visChangeDir;
                    lastVisChangeRow = i;
                end
            end
            
            % Prune visChangeGroups to keep only the filled part
            rowSharing = visChangeGroups(1:p_i);
        end
    end
    
    % RowEnable and ElementEnable
    methods (Access=private)
        function connectRowEnableToControl_Install(dlg)
            % Initialize the deferred row enable
            %
            % Called individually on each top-level dialog of the
            % presenter.
            
            % Get args for row-ena control tests
            %  rowEnaArgs: cell-vector, one per "test+reaction"
            %  rowEnaArgs{i}: another cell-vector of args,
            %          {ctrlTags,uiHandle,uiValue,enaValue,actionType}
            %
            %   uiHandle is the "source" of the mouse-interaction
            %   actionType is a string: 'rowvis','elevis'
            %                           'rowena','eleena'
            %
            rowEnaArgs = dlg.DeferredRowEnableArgs;
            if isempty(rowEnaArgs)
                return % EARLY EXIT
            end
            dlg.DeferredRowEnableArgs = {}; % clear the entries

            % Aggregate across all row-enable actions
            %  - identify all entries that are based on same source control
            %  - uicontrols responsible for notifying us of a mouse
            %    interaction are in rowEnaArgs{i}{2}
            rowEnaStruct = combineSameSourceActions(dlg,rowEnaArgs);

            for i = 1:numel(rowEnaStruct)
                args_i = rowEnaStruct(i); % cell of args
                a_j = args_i.actions;
                
                Nc = numel(args_i.uiHandle);
                for j = 1:Nc
                    % Get next uiHandle - could be a set for radiobuttons
                    h_j = args_i.uiHandle(j);
                    addlistener(h_j,'Action', ...
                        @(~,~)updateRowEnable_Action(h_j,a_j));
                end
                
                % Execute manually to force vis to be updated.
                %
                % Just execute for the last handle, if multiple specified
                % for a group of uiHandles (like a radiobuttongroup).
                updateRowEnable_Action(h_j,a_j);
            end
        end
        
        function connectElementEnableToControl_Install(dlg)
            % Initialize the deferred element enable
            %
            % Called individually on each top-level dialog of the
            % presenter.
            
            % Get args for ele-ena control tests
            %  eleEnaArgs: cell-vector, one per "test+reaction"
            %  eleEnaArgs{i}: another cell-vector of args,
            %          {ctrlTags,uiHandle,uiValue,enaValue}
            %          uiHandle is the "source" of the mouse-interaction
            %
            eleEnaArgs = dlg.DeferredElementEnableArgs;
            if isempty(eleEnaArgs)
                return % EARLY EXIT
            end
            dlg.DeferredElementEnableArgs = {}; % clear the entries

            % Aggregate across all element-enable actions
            %  - identify all entries that are based on same source control
            %  - uicontrols responsible for notifying us of a mouse
            %    interaction are in eleEnaArgs{i}{2}
            eleEnaStruct = combineSameSourceActions(dlg,eleEnaArgs);

            for i = 1:numel(eleEnaStruct)
                args_i = eleEnaStruct(i); % cell of args
                a_j = args_i.actions;
                
                Nc = numel(args_i.uiHandle);
                for j = 1:Nc
                    % Get next uiHandle - could be a set for radiobuttons
                    h_j = args_i.uiHandle(j);
                    addlistener(h_j,'Action', ...
                        @(~,~)updateElementEnable_Action(h_j,a_j));
                end
                
                % Execute manually to force vis to be updated.
                %
                % Just execute for the last handle, if multiple specified
                % for a group of uiHandles (like a radiobuttongroup).
                updateElementEnable_Action(h_j,a_j);
            end
        end
    end
    
    methods (Access=protected)
        % Understanding "connect" methods
        % -------------------------------
        % - "connect[A]To[B]"
        %      * External changes made to B propagate to A
        %      * A points to/watches B
        %      * A -> B
        %
        % - "connect[A]And[B]"
        %      * External changes made to B propagate to A
        %      * External changes made to A propagate to B
        %      * A points to/watches B, and vice versa
        %      * A <-> B
        
        function connectSliderToEdit(dc,hValue,hEdit,doInit)
            % One-way connection between two uicontrols.
            %
            % External changes to Edit uicontrol are copied to Value
            % uicontrol.
            %
            % String is taken from the .String property of the uieditv
            % uicontrol (hEdit) whenever a change to its Value property
            % occurs. Its value is set into the Value property of the
            % hValue uicontrol.
            %
            % An initial update to Value from String is made when this
            % method is called.
            %
            % The client must be sure that the hValue and hString
            % uicontrols have Value and String properties, respectively.
            %
            % If doInit is false, initial update is skipped.
            
            if dc.IsLazyLoadPhase
                return
            end
            
            if nargin<4 || doInit
                % Update value explicitly the first time:
                updateValueControlFromEditControl(hValue,hEdit);
            end
            
            % hString control is being modified by user - we listen to
            % hString.
            %
            % We update the value based on the string in hString.
            %
            % The modification to hValue should not cause hString to change
            % again - that's pointless.  So we disable hString's listener
            % which is listening to changes in hValue.
            %
            thisListener = addlistener(hEdit,'Action', ...
                @(~,~)updateValueControlFromEditControl( ...
                hValue,hEdit,'DCTableForm_ValueListenerThatUpdatesString'));
            setappdata(hValue,'DCTableForm_StringListenerThatUpdatesValue',thisListener);
        end
        
        function connectEditToSlider(dc,hEdit,hSlider,fmt)
            % One-way connection between two uicontrols.
            %
            % External changes to Slider uicontrol are copied to uieditv
            % uicontrol.
            %
            % An initial update to uieditv from uislider is made when this
            % method is called.
            %
            % The uieditv control is also setup for validation, based on
            % the slider min/max values.
            %
            % The client must be sure that the hValue and hString
            % uicontrols have Value and String properties, respectively.
            
            if dc.IsLazyLoadPhase
                return
            end
            
            if nargin<4
                fmt = '%g'; % just for sprintf of value
            end
            hEdit.ValidAttributes = {'real','scalar', ...
                '>=',hSlider.Min, ...
                '<=',hSlider.Max};
            
            % Update string explicitly the first time:
            errmsg = updateStringControlFromValueControl(hEdit,hSlider,fmt);
            error(errmsg);
            
            % hValue control is being modified by user - we listen to
            % hValue.
            %
            % We update the string based on the value in hValue.
            %
            % Modifications to hString should not cause hValue to change
            % again - that's pointless.  So we disable hValue's listener
            % to hString, if it exists.
            thisListener = addlistener(hSlider,'Action', ...
                @(~,~)updateStringControlFromValueControl( ...
                hEdit,hSlider,fmt,'DCTableForm_StringListenerThatUpdatesValue'));
            setappdata(hEdit,'DCTableForm_ValueListenerThatUpdatesString',thisListener);
        end
        
        function connectValueToText(dc,hValue,hText,fmt,doInit)
            % One-way connection between two uicontrols.
            %
            % External changes to String uicontrol are copied to Value
            % uicontrol.
            %
            % String is taken from the .String property of the hString
            % uicontrol whenever a change to the String property occurs.
            % The String is converted to a value and set into the Value
            % property of the hValue uicontrol.
            %
            % An initial update to Value from String is made when this
            % method is called.
            %
            % The client must be sure that the hValue and hString
            % uicontrols have Value and String properties, respectively.
            %
            % If doInit is false, initial update is skipped.
            
            if dc.IsLazyLoadPhase
                return
            end
            
            if nargin<4
                fmt = '%g';
            end
            
            if nargin<5 || doInit
                % Update value explicitly the first time:
                errmsg = updateValueControlFromStringControl(hValue,hText,fmt);
                error(errmsg);
            end
            
            % hString control is being modified by user - we listen to
            % hString.
            %
            % We update the value based on the string in hString.
            %
            % The modification to hValue should not cause hString to change
            % again - that's pointless.  So we disable hString's listener
            % which is listening to changes in hValue.
            %
            thisListener = addlistener(hText,'Action', ...
                @(~,~)updateValueControlFromStringControl( ...
                hValue,hText,fmt,'DCTableForm_ValueListenerThatUpdatesString'));
            setappdata(hValue,'DCTableForm_StringListenerThatUpdatesValue',thisListener);
        end
        
        function connectTextToValue(dc,hText,hValue,fmt)
            % One-way connection between two uicontrols.
            %
            % External changes to Value uicontrol are copied to uitext
            % uicontrol.
            %
            % An initial update to text from value is made when this
            % method is called.
            %
            % The client must be sure that the hValue and hString
            % uicontrols have Value and String properties, respectively.
            
            if dc.IsLazyLoadPhase
                return
            end
            
            if nargin<4
                fmt = '%g'; % just for sprintf of value
            end
            
            % Update string explicitly the first time:
            errmsg = updateStringControlFromValueControl(hText,hValue,fmt);
            error(errmsg);
            
            % hValue control is being modified by user - we listen to
            % hValue.
            %
            % We update the string based on the value in hValue.
            %
            % Modifications to hString should not cause hValue to change
            % again - that's pointless.  So we disable hValue's listener
            % to hString, if it exists.
            thisListener = addlistener(hValue,'Action', ...
                @(~,~)updateStringControlFromValueControl( ...
                hText,hValue,fmt,'DCTableForm_StringListenerThatUpdatesValue'));
            setappdata(hText,'DCTableForm_ValueListenerThatUpdatesString',thisListener);
        end
        
        function connectPropertyAndControl(dc,propName,hControl,cOpt)
            % Two-way connection between a uicontrol and a property.
            %
            % Changes to uicontrol are reflected in property value, and
            % vice-versa. Initialization of the uicontrol is performed
            % based on the current property value.
            %
            % connectPropertyAndControl(dlg,uiHandle,propName)
            %
            % connectPropertyAndControl(dlg,uiHandle,propName,cOpt)
            %    where conversion option cOpt is specified based on
            %    uicontrol style.
            %
            % Depending on the uicontrol style, the value recorded in the
            % property varies and is documented in the table below.  An
            % optional argument cOpt may be passed to override the value
            % recorded in the property.
            %
            %  Control Style   Optional arg
            %  -------------   ---------------------------------
            %  'edit','text'   -'string' (default)
            %                  -'eval', which evaluates expression in base
            %                    MATLAB workspace
            %                  -'<fmt>', which must be a valid string
            %                   format for sprintf such as '%g'
            %                  -two function handles: [v=fcn(h),fcn(h,v)]
            %
            %  'popupmenu'     -'selection' (default), the selected string
            %                  -'value', the selection index
            %                  -two function handles: [v=fcn(h),fcn(h,v)]
            %
            %  'listbox'       -'selection' (default), the selected string
            %                  -'value', the selection index
            %                  -two function handles: [v=fcn(h),fcn(h,v)]
            %
            %  'radiobutton'   - 'string'
            %  'checkbox','frame','pushbutton','radiobutton',
            %  'slider','togglebutton'
            %                  -'value' (default)
            %                  -two function handles: [v=fcn(h),fcn(h,v)]
            %
            %  'uitable'       -'data' (default)
            %                  -two function handles: [v=fcn(h),fcn(h,v)]
            
            if dc.IsLazyLoadPhase
                return
            end
            
            if nargin<4
                cOpt = ''; % invokes default
            end
            
            %Add control and propName to list for manual apply
            dc.ControlPropertyPairs{end+1} = {'control' dc propName hControl cOpt};
            
            %We setup the backing store whether manual or auto apply
            %because we may be changed auto->manual if added as subdialog
            dc.BackingStore.(propName) = dc.(propName);
                          
            % Initialize control value to the current property value
            if ~isa(hControl,'dialogmgr.uieditv') || isempty(hControl.String)
                updateControlFromClassProperty(dc,hControl,propName,cOpt);
            end
            dc.StringValues.(propName) = hControl.String;
            
            % Listen to class property, update control
            li = addlistener(dc,propName,'PostSet', ...
                @(~,~)updateControlFromClassProperty(dc,hControl,propName,cOpt));
          	setappdata(hControl,'ControlListenToProperty',li);

            li = addlistener(dc,propName,'PostSet', ...
                @(~,~)notify(dc,'PropertyUpdate',dialogmgr.DCEvent(propName,'single',dc)));
            setappdata(hControl,'PropertyChangeTriggerUpdate',li);

            % Listen to control, update class property
            li = addlistener(hControl,'Action', ...
                @(~,~)updateClassPropertyFromControl(dc,propName,hControl,cOpt));
            setappdata(hControl,'ControlListenToControl',li);
        end
        
        function connectPropertyToControl(dc,propName,hControl,cOpt)
            % One-way connection between a class property and a uicontrol.
            %
            % Changes to uicontrol are copied to class property.
            %
            % A initial update to the uicontrol from the property value is
            % performed when this method is called.  We don't synchronize
            % the other way around: properties are always the initial
            % values for uicontrols.
            %
            % connectPropertyToControl(dlg,propName,hControl)
            %
            % connectPropertyToControl(dlg,propName,hControl,cOpt)
            %    where conversion option cOpt is specified based on
            %    uicontrol style.
            %
            % Depending on the uicontrol style, the value copied to the
            % property varies, and is documented in the table below.  An
            % optional argument may be passed to override the value copied
            % to the property.
            %
            %  Control Style   Optional arg
            %  -------------   ---------------------------------
            %  'edit','text'   -'string' (default)
            %                  -'fmt', which must be a valid string format
            %                    for sprintf
            %                  -two function handles: [v=fcn(h),fcn(h,v)]
            %
            %  'popupmenu'     -'selection' (default), the selected string
            %                  -'value', the selection index
            %                  -two function handles: [v=fcn(h),fcn(h,v)]
            %
            %  'listbox'       -'selection' (default), the selected string
            %                  -'value', the selection index
            %                  -two function handles: [v=fcn(h),fcn(h,v)]
            %
            %  'checkbox','frame','pushbutton','radiobutton',
            %  'slider','togglebutton'
            %                  -'value' (default)
            %                  -two function handles: [v=fcn(h),fcn(h,v)]
            %
            %  'uitable'       -'data' (default)
            %                  -two function handles: [v=fcn(h),fcn(h,v)]
            
            if dc.IsLazyLoadPhase
                return
            end

         	%Add control and propName to list for manual apply
            dc.ControlPropertyPairs{end+1} = {'control' dc propName hControl cOpt};
            
            %We setup the backing store whether manual or auto apply
            %because we may be changed auto->manual if added as subdialog
            dc.BackingStore.(propName) = dc.(propName);
            
            % Initialize control value to the current property value
            %
            % NOTE: We DON'T do it the other way around, despite what may
            % seem natural to do here.  Properties override controls.
            %
            if ~isempty(dc.(propName))
                updateControlFromClassProperty(dc,hControl,propName,cOpt);
            end
            
            % Listen to control, update property
            li = addlistener(dc,'PostSet', propName, ...
                @(~,~)updateClassPropertyFromControl(dc,hControl,propName,cOpt));
            setappdata(hControl,'ControlListenToProperty',li);
        end
        
         function connectControlToProperty(dc,hControl,propName,cOpt)
            % One-way connection between a uicontrol and a class property.
            %
            % Changes to class property are copied to uicontrol.
            %
            % A initial update to the uicontrol from the property value is
            % performed when this method is called.
            %
            % connectControlToProperty(dlg,hControl,propName)
            %
            % connectControlToProperty(dlg,hControl,propName,cOpt)
            %    where option may specified based on uicontrol type.
            %
            % Depending on the uicontrol style, the value copied to the
            % property varies, and is documented in the table below.  An
            % optional argument may be passed to override the value copied
            % to the property.
            %
            %  Control Style   Optional arg
            %  -------------   ---------------------------------
            %  'edit','text'   -'string' (default)
            %                  -'fmt', which must be a valid string format
            %                    for sprintf
            %                  -two function handles: [v=fcn(h),fcn(h,v)]
            %
            %  'popupmenu'     -'selection' (default), the selected string
            %                  -'value', the selection index
            %                  -two function handles: [v=fcn(h),fcn(h,v)]
            %
            %  'listbox'       -'selection' (default), the selected string
            %                  -'value', the selection index
            %                  -two function handles: [v=fcn(h),fcn(h,v)]
            %
            %  'checkbox','frame','pushbutton','radiobutton',
            %  'slider','togglebutton'
            %                  -'value' (default)
            %                  -two function handles: [v=fcn(h),fcn(h,v)]
            %
            %  'uitable'       -'data' (default)
            %                  -two function handles: [v=fcn(h),fcn(h,v)]
            
            if dc.IsLazyLoadPhase
                return
            end
            
            if nargin<4
                cOpt = ''; % invokes default
            end
            
            % Initialize uicontrol based on current class property value
            if ~isempty(dc.(propName))
                updateControlFromClassProperty(dc,hControl,propName,cOpt);
            end
            
            % Listen to subsequent changes to control, and update property
            % with the change.
            li = addlistener(hControl,'Action', ...
                @(~,~)updateControlFromClassProperty(dc,hControl,propName,cOpt));
            setappdata(hControl,'ControlListenToControl',li);

            li = addlistener(dc,propName,'PostSet', ...
                @(~,~)notify(dc,'PropertyUpdate',dialogmgr.DCEvent(propName,'single',dc)));
            setappdata(hControl,'PropertyChangeTriggerUpdate',li);
        end
        
        function connectPropertyAndControlWithEngUnits(dc,propName,hControl,cOpt)
            % Two-way connection between a uicontrol and a property.
            %
            % Changes to uicontrol are reflected in property value, and
            % vice-versa. Initialization of the uicontrol is performed
            % based on the current property value.
            %
            % Only for controls that utilize text strings, and
            % numeric-valued class properties.
            %
            % Converts numeric value to and from text that has an optional
            % metric unit after a numeric value, expressing scaling as a
            % power of 1000. Lower- and upper-case are interchangeable
            % where it is not ambiguous to do so.
            %
            %   a:1e-18 f:1e-15 p:1e-12 n:1e-9 u:1e-6 m:1e-3 '':1
            %   E:1e18  P:1e15  T:1e12  G:1e9  M:1e6  k:1e3
            %
            % connectPropertyAndControlWithEngUnits(dlg,propName,hControl)
            %
            % connectPropertyAndControlWithEngUnits(dlg,propName,hControl,cOpt)
            %    where conversion option cOpt is specified based on
            %    uicontrol style.
            %
            % Depending on the uicontrol style, the value copied to the
            % property varies, and is documented in the table below.  An
            % optional argument may be passed to override the value copied
            % to the property.
            %
            %  Control Style   Optional arg
            %  -------------   ---------------------------------
            %  'edit','text'   -'string' (default)
            %                  -'efmt'
            %
            % efmt is specified in the help for:
            %     updateControlStringWithEngUnitsFromPropertyValue
            %
            % Format string efmt must be an "engineering" format, in which
            % both a numeric conversion specifier (like %g or %.2f) and a
            % char specifier (%c) must both be specified, in that order.
            % The string must enable sscanf to construct a string with
            % numeric value and engineering unit char.
            %
            % Example efmt:
            % Default "%g %c", yields "3.1415926 m".
            %
            %  "%g %cV" yields "3.1415926 mV", where the unit
            % of Volts has been added
            %
            %  "%.2f %sHz" yields "3.14 MHz"
            %
            % Ex: property value is 0.00621, efmt="%.2 %cV"
            %     string set into uicontrol is '62.10 mV'
            
            if dc.IsLazyLoadPhase
                return
            end
            
            if nargin<4
                cOpt = ''; % invokes default
            end
            
           	%Add control and propName to list for manual apply
            dc.ControlPropertyPairs{end+1} = {'control_eng' dc propName hControl cOpt};
            
            % Initialize control value to the current property value
            updateControlStringWithEngUnitsFromClassProperty(dc,hControl,propName,cOpt);
            
            % Listen to changes to property, update uicontrol
            addlistener(dc,propName,'PostSet', ...
                @(~,~)updateControlStringWithEngUnitsFromClassProperty(dc,hControl,propName,cOpt));
            
            % Listen to changes to control, update property
            addlistener(hControl,'Action', ...
                @(~,~)updateClassPropertyFromControlStringWithEngUnits(dc,propName,hControl,cOpt));
        end
        
        function connectPropertyAndRadiobuttons(dc,propName,hButtons)
            % Coordinate one or more radiobutton uicontrols into a 1-of-N
            % behavior.  Give them two-way connection to a class property
            % using a string enumeration similar to a popup control.
            %
            % connectPropertyAndRadiobuttons(dlg,propName,h1,h2,...)
            % where propName is the string name of the class property, and
            % h1, h2, ... are uicontrol handles.
            %
            % A initial update to the radiobutton group from the property
            % value is performed when this method is called.
            
            if dc.IsLazyLoadPhase
                return
            end
                       
            % Check args
            if ~ischar(propName) && ~(isstring(propName) && isscalar(propName))
                dialogmgr.untranslatedError([ ...
                    'Dialog "%s":\n' ...
                    '  Property name must be the last input argument and must be a string.'], ...
                    dc.Name);
            end
            Nc = numel(hButtons);
            if Nc==0
                dialogmgr.untranslatedError([ ...
                    'Dialog "%s":\n' ...
                    '  Must specify at least one radiobutton control.'], ...
                    dc.Name);
            end
            if ~isa(hButtons,'matlab.ui.control.UIControl')
                dialogmgr.untranslatedError([ ...
                    'Dialog "%s":\n' ...
                    '  Must pass a vector of radiobutton uicontrols.'], ...
                    dc.Name);
            end
            
            % Check that all radiobutton strings/userdata are unique.
            % Userdata strings can be empty, string labels cannot.
            %
            % UserData strings are "programmer" strings, which are not
            % subject to translation.
            %
            % Remove empty userdata strings from consideration:
            userStrs = {hButtons.UserData};
            userStrs = userStrs( ~cellfun(@isempty,userStrs) );
            strStrs = {hButtons.String};
            if any(cellfun(@isempty,strStrs))
                dialogmgr.untranslatedError([ ...
                    'Dialog "%s":\n' ...
                    '  String cannot be empty across all radiobutton controls in group.'], ...
                    dc.Name);
            end
            allStrs = [userStrs strStrs];
            [~,ia] = unique(allStrs);
            if numel(ia) ~= numel(allStrs)
                dialogmgr.untranslatedError([ ...
                    'Dialog "%s":\n' ...
                    '  String and UserData strings must be unique across all radiobutton uicontrols in group.\n' ...
                    '  (UserData strings may be left empty.)'], ...
                    dc.Name);
            end
            
            % Cache vector of buttons in each button
            % Check that all buttons have min=0 and max=1
            lvec = [];
            for i = 1:Nc
                hThisButton = hButtons(i);
                
                % Confirm this is a radiobutton
                if ~strcmpi(hThisButton.Style,'radiobutton')
                    dialogmgr.untranslatedError([ ...
                    'Dialog "%s":\n' ...
                    '  Must pass a vector of radiobutton uicontrols.'], ...
                    dc.Name);
                end
                
                if hThisButton.Min~=0 || hThisButton.Max~=1
                    dialogmgr.untranslatedError([...
                        'Dialog "%s":\n' ...
                        '  All radiobuttons must have the following property values:\n' ...
                        '  .Min=0 (currently %f)\n' ...
                        '  .Max=1 (currently %f)'], ...
                        dc.Name, hThisButton.Min, hThisButton.Max);
                end
                setappdata(hThisButton,'RadiobuttonGroup',hButtons);
            
                % Setup listeners
                
                addlistener(hThisButton,'Action', ...
                    @(src,ev)updateClassPropertyFromRadiobuttonGroup(dc,propName,hThisButton));
                li = addlistener(dc,propName,'PostSet', ...
                    @(src,ev)updateRadiobuttonGroupFromClassProperty(dc,hThisButton,propName));
                lvec = [lvec li]; %#ok<AGROW>
            end
            for i = 1:numel(hButtons)
                setappdata(hButtons(i),'RadioButtonListenToClassProp',lvec);
            end
            
            % Copy property value to radiobuttons, and sync the radiobutton
            % group to each other.
            updateRadiobuttonGroupFromClassProperty(dc,hButtons(1),propName);
            
            %Add control and propName to list for manual apply
            dc.ControlPropertyPairs{end+1} = {'radio' dc propName hButtons []};
        end
        
        function connectPropertyAndSliderAndEdit(dc,propName,uiSlider,uiEditv)
            % Connect a slider uicontrol to an edit box and to a property
            % value.  Changes to slider or edit box propagate to one
            % another, and to the property.  Changes to the property
            % propagate to both the slider and the edit box.
            %
            % An initial synchronization is performed from the property to
            % the slider and edit box.
            
            if dc.IsLazyLoadPhase
                return
            end
            
            % Two-way connect between property and slider
            %   - slider will constrain change due to out-of-bounds
            % Two-way connect between slider and edit box
            %   - if slider fails, we push a new value back to edit box
            %
            % Order matters here:
            % - want string to take value from slider
            %   since edit/text is likely empty, whereas
            %   slider always has a valid setting
            % - we don't need an initial update on ValueToString
            %   (class property has already updated the value/slider)
            connectPropertyAndControl(dc,propName,uiSlider);
            connectEditToSlider(dc,uiEditv,uiSlider);
            connectSliderToEdit(dc,uiSlider,uiEditv,false);
        end
        
        function connectPropertyAndSliderAndText(dc,propName,uiSlider,uiText,fmt)
            % Connect a slider uicontrol to a text box and to a property
            % value.  Changes to slider propagate to the text and to the
            % property.  Changes to the property propagate to both the
            % slider and the text.
            %
            % An initial synchronization is performed from the property to
            % the slider and text.
            %
            % connectSliderToTextAndProperty(dlg, ...
            %       uiSlider,uiText,propName)
            %
            % connectSliderToTextAndProperty(dlg, ...
            %       uiSlider,uiText,propName,fmt)
            
            if dc.IsLazyLoadPhase
                return
            end
            
            if nargin<5
                fmt = '%g';
            end

            % Two-way connect between property and slider
            %   - slider will constrain change due to out-of-bounds
            % Two-way connect between slider and text box
            %   - if slider fails, we push a new value back to edit box
            %   - assume text *could* change by some external means
            %
            % Order matters here:
            % - want string to take value from slider
            %   since edit/text is likely empty, whereas
            %   slider always has a valid setting
            % - we don't need an initial update on ValueToString
            %   (class property has already updated the value/slider)
            connectPropertyAndControl(dc,propName,uiSlider);
            connectTextToValue(dc,uiText,uiSlider,fmt);
            connectValueToText(dc,uiSlider,uiText,fmt,false);
        end
    end
    
    % For client app listening to property changes on DCTableForm
    %
    methods
        function el = onPropertyChange(dc,cbFcn)
            % Execute callback function when public properties of
            % DCTableForm change due to user interaction with dialog.
            %
            % By default, callback cbFcn is invoked with two
            % arguments for a Property event: eventSource and eventData.
            % Pass an anonymous function handle to remove or replace these
            % two arguments.
            %
            % Ex:
            %    onPropertyChange(dlg, ...
            %          @(evSrc,evData)PropChangeHandler(myObj,evData));
            %
            % ** Overrides DialogContent superclass implementation. **
            
            if dc.IsLazyLoadPhase
                el = [];
                return
            end
            
            % add listener to top-level properties
            el = addlistener(dc,'PropertyUpdate',cbFcn);
        end
        
        function changeFontSize(dlg,step)
            % Increment or decrement font size.
            %  - change font size recursively for all subdialogs first
            %
            % Dir:
            %   +1 to increase font size
            %   -1 to decrease font size
            %    0 to restore font size
            
            if ~dlg.CachedOrigFontSizes
                % If we haven't cached the original font size, it means we
                % haven't changed font size either.  So skip caching and
                % just return.
                if step==0
                    return
                end
                
                % Cache the original in preparation for changing font size
                dlg.CachedOrigFontSizes = true;
                changeFontSize_CacheOrig(dlg);
            end
            
            changeFontSize_Size(dlg,step);
            changeFontSize_Layout(dlg);
        end
    end
    
    methods (Hidden) % must be public/friend so uidialog can call these
        function changeFontSize_Size(dlg,step)
            % Increment or decrement font size.
            %  - change font size recursively for all subdialogs first
            %  - check first control for appdata that stores original font
            %    size; if non-existent, store it for all
            %
            % Dir:
            %   +1 to increase font size
            %   -1 to decrease font size
            %    0 to restore font size
            
            minSize = dlg.FontSizeMin;
            maxSize = dlg.FontSizeMax;
            
            c = dlg.Controls;
            cf = dlg.ControlsFraction;
            decimal = dlg.CachedDecimalAlignCols;
            Nh = dlg.NumHeaderRows;
            
            [Nr,Nc] = size(c);
            for i = 1:Nr
                for j = 1:Nc
                    c_ij = c{i,j};
                    if ~isempty(c_ij)
                        if isa(c_ij,'dialogmgr.uidialog')
                            changeFontSize_Size(c_ij,step); % descend
                            
                            % if a uidialog appears in a row, no other
                            % elements appear in that row
                            break % out of the col loop
                        else
                            if step==0
                                c_ij.FontSize = getappdata(c_ij,'OrigFontSize');
                            else
                                c_ij.FontSize = max(minSize,min(maxSize,c_ij.FontSize+step));
                            end
                            
                            if decimal(j) && i>Nh
                                cf{i,j}.FontSize = c_ij.FontSize;
                            end
                        end
                    end
                end
            end
        end
        
        function changeFontSize_Layout(dlg)
            % Increment or decrement font size.
            %  - update layout recursively for all subdialogs last
            %
            % Dir:
            %   +1 to increase font size
            %   -1 to decrease font size
            %    0 to restore font size
            
            % Do update before descent.
            % This order is needed so that child uidialogs that participate
            % in parent layout will see their parents update first - then
            % they can react to the (correct/up-to-date) parent layout.
            updateLayoutForFontSizeChange(dlg);
            
            c = dlg.Controls;  % no uidialog in fraction part
            for i = 1:size(c,1) % uidialogs can only be in column 1
                if isa(c{i,1},'dialogmgr.uidialog')
                    changeFontSize_Layout(c{i,1});  % descend
                end
            end
        end
        
        function changeFontSize_CacheOrig(dlg)
            % Cache initial control fontsize in appdata on the control.

            c = dlg.Controls;
            cf = dlg.ControlsFraction;
            decimal = dlg.CachedDecimalAlignCols;
            Nh = dlg.NumHeaderRows;
            
            [Nr,Nc] = size(c);
            for i = 1:Nr
                for j = 1:Nc
                    c_ij = c{i,j};
                    if ~isempty(c_ij)
                        if isa(c_ij,'dialogmgr.uidialog')
                            changeFontSize_CacheOrig(c_ij); % descend
                            % if a uidialog appears in a row, no other
                            % elements appear in that row
                            break % out of the col loop
                        else
                            setappdata(c_ij,'OrigFontSize',c_ij.FontSize);
                            if decimal(j) && i>Nh
                                % Maintain same font size for int/frac
                                setappdata(cf{i,j},'OrigFontSize',c_ij.FontSize);
                            end
                        end
                    end
                end
            end
        end
    end
    
    methods (Access=private)
        function updateLayoutForFontSizeChange(dlg)
            % Update layout due to change in font size.
            
            computeControlExtents(dlg);
            computeRowHeights(dlg);
            computeColWidths(dlg);
            [origin,spans,ymax] = computeTableLayout(dlg);
            updateControlPositions(dlg,origin,spans); % sets .LastLayoutOrigin, .LastLayoutSpans
            changeDialogContentHeight(dlg.DialogBorder,ymax);
        end
    end
    
    % For directly setting values into text controls in DCTableReadout
    %
    methods (Access=protected)
        function updateTextControlFromString(dc,hControl,str)
            % Handles decimal-aligned and non-decimal aligned columns.
            %
            % hControl: uicontrol in Controls matrix
            % val: a string
            %
            % NOTE: dlg object is not used, but left in for ease of use by
            % client code.
            
            if dc.IsLazyLoadPhase
                return
            end
            
            if ~ischar(str) && ~(isstring(str) && isscalar(str))
                dialogmgr.untranslatedError([ ...
                    'Dialog "%s":\n' ...
                    '  Value must be a string.'], ...
                    dc.Name);
            end
            
            % For decimal-aligned controls, we cache info in
            % appdata: DCTableForm_DecimalAlign.
            %
            da = getappdata(hControl,'DCTableForm_DecimalAlign');
            if isempty(da)
                % Not decimal-aligned
                %
                % simple string update:
                hControl.String = str;
            else
                % Control is part of a decimal-aligned control
                % pair.  Class property value must be a numeric
                % scalar.
                %
                % appdata on control is a struct:
                %   .ColumnControlPairs = Nx2 matrix, [uictrls_int uictrls_frac]
                %   .ControlPair        = 1x2 vector, [uictrl_int uictrl_frac]
                setDecimalAlignedElement_intfrac( ...
                    da.ControlPair{1}, ...
                    da.ControlPair{2}, ...
                    str,false);
            end
        end
        
        function updateTextControlWithEngUnitsFromValue( ...
                dc,hControl,val,efmt)
            % Convert from numeric value to uicontrol string, applying
            % engineering-units conversion.
            %
            % Format string efmt must be an "engineering" format, in which
            % both a numeric conversion specifier (like %g or %.2f) and a
            % char specifier (%c) must both be specified, in that order.
            % The string must enable sscanf to construct a string with
            % numeric value and engineering unit char.
            %
            % Example efmt:
            % Default "%g %c", yields "3.1415926 m".
            %
            %  "%g %cV" yields "3.1415926 mV", where the unit
            % of Volts has been added
            %
            %  "%.2f %sHz" yields "3.14 MHz"
            %
            % Ex: property value is 0.00621, efmt="%.2 %cV"
            %     string set into uicontrol is '62.10 mV'
            %
            % NOTE: dlg object is not used, but left in for ease of use by
            % client code.

            if dc.IsLazyLoadPhase
                return
            end
            
            if nargin<4 || isempty(efmt)
                % This is an "engineering unit" format, which is a normal
                % printf-style format string, plus a "%s" after the number
                % somewhere to display the unit.
                efmt = '%g %c';
            end
            
            % Get property value, convert to engineering units
            if ~isnumeric(val)
                dialogmgr.untranslatedError([ ...
                    'Dialog "%s":\n' ...
                    '  Value is not numeric, conversion not supported.'], ...
                    dc.Name);
            end
            
            % Pretty-print scaled value
            % xxx add support for specifying # digits after decimal via fmt
            [v_eng,~,u_eng] = engunits(val);
            str = sprintf(efmt,v_eng,u_eng);
            
            % For decimal-aligned controls, we cache info in
            % appdata: DCTableForm_DecimalAlign.
            %
            da = getappdata(hControl,'DCTableForm_DecimalAlign');
            if isempty(da)
                % Not decimal-aligned
                %
                % simple string update:
                hControl.String = str;
            else
                % Control is part of a decimal-aligned control
                % pair.  Class property value must be a numeric
                % scalar.
                %
                % appdata on control is a struct:
                %   .ColumnControlPairs = Nx2 matrix, [uictrls_int uictrls_frac]
                %   .ControlPair        = 1x2 vector, [uictrl_int uictrl_frac]
                setDecimalAlignedElement_intfrac( ...
                    da.ControlPair(1), ...
                    da.ControlPair(2), ...
                    str, true);
            end
        end
    end
    
    methods (Access=private)
        function updateRadiobuttonGroupFromClassProperty(dlg,hAnyButton,propName)
            % Update group of radiobuttons based on class property.  The
            % string from the class property must match exactly one of the
            % strings defined in the group of radiobutton controls.
            %
            % The uicontrol handle to any button in the button group can be
            % passed; it doesn't matter which is passed.
            %
            % Matching of strings is case-insensitive, but no
            % partial-matching is provided.
            %
            % UserData may contain alternative strings as well, which
            % presumably aren't translated and therefore are stable to use
            % as enumeration strings.
            
            allButtons = getappdata(hAnyButton,'RadiobuttonGroup');
            
            % Find button corresponding to class property string:
            pval = dlg.(propName);
            if ischar(pval) || (isstring(pval) && isscalar(pval))
                % UserData string or String test - sel is logical vector
                sel = strcmp(pval,{allButtons.UserData});
                if ~any(sel)
                    sel = strcmp(pval,{allButtons.String});
                end
                if ~any(sel)
                    dialogmgr.untranslatedError([ ...
                        'Dialog "%s":\n' ...
                        '   String "%s" in property "%s" does not match the String\n' ...
                        '   or UserData strings of any radiobutton in group.'], ...
                        dlg.Name,pval,propName);
                elseif sum(sel)>1
                    dialogmgr.untranslatedError([ ...
                        'Dialog "%s":\n' ...
                        '   String in property "%s" matches multiple Strings or\n' ...
                        '   UserData strings of radiobuttons in group.'], ...
                        dlg.Name,propName);
                end
                
            elseif isnumeric(pval) && isscalar(pval)
                % Index into set of radiobutton group
                N = numel(allButtons);
                if pval<1 || pval>N
                    dialogmgr.untranslatedError([ ...
                        'Dialog "%s":\n' ...
                        '   Radiobutton index %d in property "%s" is out of range.\n' ...
                        '   Must be 1 to %d for radiobutton group.'], ...
                        dlg.Name,pval,propName,N);
                end
                sel = false(size(allButtons));
                sel(pval) = true;
                
            else
                dialogmgr.untranslatedError([ ...
                    'Dialog "%s":\n' ...
                    '   Unsupported data type of size for radiobutton initial condition.\n' ...
                    '   Property: %s\n' ...
                    '   Data type: %s'], ...
                    dlg.Name,propName,class(pval));
            end
            
            % Update buttons for 1-on-N behavior
            hButtonToPress = allButtons(sel);
            hOtherButtons = allButtons(allButtons~=hButtonToPress);
            set(hOtherButtons,'Value',0);
            hButtonToPress.Value = 1;
            
            % Our manual changes to the control above suppress further
            % Action events.
            %notify(hAnyButton,'Action');
        end
        
                
    end
    methods
        
        function updateClassPropertyFromRadiobuttonGroup(dc,propName,hThisButton)
            % Turn "on" specific radiobutton.  Synchronize group of
            % radiobutton controls to provide 1-of-N behavior.  Update
            % class property value based on string of radiobutton that is
            % currently "on".
            %
            % UserData strings can be used as programmatic alternative to
            % the use of the actual display Strings, or instead of
            % enumeration values.
            
            hButtons = getappdata(hThisButton,'RadiobuttonGroup');
            uiOff = hThisButton.Min;
            uiOn = hThisButton.Max;
            
            % Update buttons for 1-of-N behavior
            for i = 1:numel(hButtons)
                h_i = hButtons(i);
                if (h_i ~= hThisButton) && (h_i.Value ~= uiOff)
                    h_i.Value = uiOff;
                end
            end
            if hThisButton.Value ~= uiOn
                hThisButton.Value = uiOn;
            end
            
            % Test for change in property value
            %
            % Identify datatype of property:
            % If string,
            %    test against UserData first,
            %    then test against String
            % If value,
            %    test which radiobutton this is within group.
            %    is it #1, 2, ...
            if dc.ManualApply
                curValue = dc.BackingStore.(propName);
            else
                curValue = dc.(propName);
            end
            if ischar(curValue) || (isstring(curValue) && isscalar(curValue))
                % UserData strings or String test
                same = ...
                    strcmp(curValue,hThisButton.UserData) || ...
                    strcmp(curValue,hThisButton.String);
                if ~same
                    % Need to know if property is using UserData string or
                    % String.
                    %
                    % The only way to know is to test value against
                    % UserData strings and Strings, then see.  We know it
                    % currently doesn't match hThisButton.
                    usingUserData = any(strcmp(curValue,{hButtons.UserData}));
                    if usingUserData
                        newValue = hThisButton.UserData;
                    else
                        newValue = hThisButton.String;
                    end
                end
                
            elseif isnumeric(curValue)
                % Index into set of radiobutton group
                newValue = find(hButtons==hThisButton);
                same = isequal(curValue, newValue);
            else
                dialogmgr.untranslatedError([ ...
                    'Dialog "%s":\n' ...
                    '   Unsupported data type for radiobutton initial condition.\n' ...
                    '   Property: %s\n' ...
                    '   Data type: %s'], ...
                    dc.Name,propName,class(curValue));
            end
            
            if ~same
                % Update property value
                %
                % Suppress all propname listeners for this set of radiobuttons
                li = getappdata(hThisButton,'RadioButtonListenToClassProp');
                for i = 1:numel(li)
                    li(i).Enabled = false;
                end
                
             	dc.StringValues.(propName) = '';

                if ~dc.ManualApply
                    dc.(propName) = newValue;
                  	%notify(dc,'PropertyUpdate',dialogmgr.DCEvent(propName,'single',dc));
                else
                    dc.BackingStore.(propName) = newValue;
                    if any(strcmp(propName,dc.ManualApplyOverrideProps))
                        dc.(propName) = newValue;
                    else
                        dc.PendingProperties{end+1} = propName;
                       	notify(dc,'PendingUpdate');
                    end
                end
                
                for i = 1:numel(li)
                    li(i).Enabled = true;
                end
            end
        end

        function updateClassPropertyFromControl(dc,propName,hControl,arg)
            % Updates class property based on current uicontrol state.
            %
            % The uicontrol property that is read depends on the uicontrol
            % style.
            %
            %    uicontrol Style   uicontrol Property 
            %    ---------------   ---------------------------------
            %    checkbox          .Value
            %    edit              .String
            %    uieditv           .LastValidVal
            %    frame             .Value
            %    listbox           .Value
            %    popupmenu         .Value or .UserData/.String
            %    radiobutton       .Value or .UserData/.String
            %    pushbutton        .Value
            %    slider            .Value
            %    text              .String  (respects decimal alignment)
            %    togglebutton      .Value
            %    uitable           .Data
            %
            % The uicontrol property named above is converted into a new
            % value that is copied into the class property. The default
            % conversion performed is specified below, along with options
            % to modify the conversion performed.
            %
            %  Control Style   Optional arg
            %  -------------   ---------------------------------
            %  'edit','text'  'string' (default)
            %                  two function handles: [v=fcn(h),fcn(h,v)]
            %
            %  'uieditv'       value (default)
            %
            %  'popupmenu'     'selection' (default), the selected string
            %                  'value', the selection index
            %                  two function handles: [v=fcn(h),fcn(h,v)]
            %
            %  'listbox'       'selection' (default), the selected string
            %                  'value', the selection index
            %                  two function handles: [v=fcn(h),fcn(h,v)]
            %
            %  'checkbox','frame','pushbutton','radiobutton',
            %  'slider','togglebutton'
            %                  'value' (default)
            %                  two function handles: [v=fcn(h),fcn(h,v)]
            %
            %  'uitable'       -'data' (default)
            %                  -two function handles: [v=fcn(h),fcn(h,v)]

            
            % Temporarily shut down listener on property.
            %
            % We will change the property and DON'T want another event!
            li = getappdata(hControl,'ControlListenToProperty');
            liEnabled = li.Enabled;
            li.Enabled = false;
            
            isWarn = false;
            
            if isa(hControl,'dialogmgr.uieditv')
                style = 'uieditv';
            else
                style = hControl.Style;
            end
            
            newValue = [];
            if dc.ManualApply
                curValue = dc.BackingStore.(propName);
            else
                curValue = dc.(propName);
            end
            
            default = nargin<5 || isempty(arg);
            if ~default && isa(arg,'function_handle')
                % Forward mapping fcn in arg(1),
                % Inverse mapping fcn in arg(2)
                newValue = feval(arg(1),hControl);
                
            else
                switch style
                    case 'uieditv'
                        % External changes to uieditv value.

                        if ~isempty(arg) && (ischar(arg) || (isstring(arg) && isscalar(arg)))
                            dialogmgr.untranslatedError([ ...
                                'Dialog "%s":\n' ...
                                '  Unsupported optional argument for uieditv control.'], ...
                                dc.Name);
                        end
                        
                        t = hControl.LastValidVal;
                        identical = isequal(t,curValue);
                        if ~identical
                            newValue = t;
                            newString = hControl.LastValidStr;
                        end
                        
                    case 'text'
                        % External changes to the string of a text
                        % uicontrol is unusual, since it is not an "input"
                        % element for customer interaction.
                        %
                        % If it is changed, by programmatic means, this
                        % code will execute.
                        %
                        % First, determine if this is a decimal-align
                        % element. If so, we need to work with int/frac
                        % parts.
                        
                        da = getappdata(hControl,'DCTableForm_DecimalAlign');
                        if isempty(da)
                            % Not a decimal-aligned control element.
                            %
                            % Get the current uicontrol string.
                            if default || strcmpi(arg,'string')
                                % Copy String to property
                                t = hControl.String;
                                identical = strcmp(t,curValue);
                                if ~identical
                                    newValue = t;
                                end
                            elseif ischar(arg) || (isstring(arg) && isscalar(arg))
                                % arg is a format string to be used with
                                % sscanf, to convert string to a numeric
                                % value:
                                t = sscanf(hControl.String,arg);
                                identical = strcmp(t,curValue);
                                if ~identical
                                    newValue = t;
                                    newString = t;
                                end
                            else
                                dialogmgr.untranslatedError([ ...
                                    'Dialog "%s":\n' ...
                                    '  Unsupported optional argument.'], ...
                                    dc.Name);
                            end
                        else
                            % Control is part of a decimal-aligned control
                            % pair.  We need both the integer and fraction
                            % part.
                            dialogmgr.untranslatedError([ ...
                                'Dialog "%s":\n' ...
                                '  External changes to the value of a uicontrol in ' ...
                                '  a decimal-aligned column is unsupported.'], ...
                                dc.Name);
                        end
                        
                    case 'edit'
                        if default || strcmpi(arg,'string')
                            % Copy String to property
                            
                            t = hControl.String;
                            identical = strcmp(t,curValue);
                            if ~identical
                                newValue = t;
                                newString = t;
                            end
                        elseif ischar(arg) || (isstring(arg) && isscalar(arg))
                            % Convert string to specified format using
                            % sscanf.  Typically this is a conversion to a
                            % numeric value.
                            
                            dialogmgr.untranslatedError([ ...
                                'Dialog "%s":\n' ...
                                '  A value conversion format cannot be specified for a uiedit control.' ...
                                '  Use a uieditv control without any format specifier instead.'], ...
                                dc.Name);
                            
                            %{
                            str = strtrim(hControl.String);
                            
                            % Try converting string to value.
                            [t,~,err] = sscanf(str,arg);
                            success = isempty(err);
                            
                            if ~success % && hControl.ResolveAsVariable
                                % Try resolving as an expression in the
                                % base workspace.
                                try %#ok<TRYNC>
                                    t = evalin('base',str);
                                    
                                    % Need to confirm type of result
                                    success = isnumeric(t);
                                end
                            end
                            
                            if ~success
                                % Failed all conversions.
                                %
                                % We don't throw a warning, as faulty user
                                % input is expected and not unusual.  Just
                                % mark as a warning and continue.
                                isWarn = true;
                                
                                % Show color highlight.
                                %
                                % Replace control string with string form
                                % of last valid value.
                                t = getappdata(hControl,'DCTableForm_LastValid_Str');
                                hControl.String = t;
                                %dialogmgr.InvalidateEditControl(hControl,t);
                            end
                                
                            if ~isWarn
                                dlg.(propName) = t;
                                
                                % Record numeric value and string form
                                %setappdata(hControl,'DCTableForm_LastValid_Val',t);
                                setappdata(hControl,'DCTableForm_LastValid_Str',str);
                            end
                            
                            % We don't test for identical conversion, since
                            % trailing 0's from '%g' conversion will nearly
                            % always cause a non-match with the display
                            % string.
                            identical = false;
                                %}
                        else
                            dialogmgr.untranslatedError([ ...
                                'Dialog "%s":\n' ...
                                '  Unsupported optional argument.'], ...
                                dc.Name);
                        end
                        
                    case {'popupmenu','listbox','spintext'}
                        % Copy currently selection from control to
                        % property.
                        %
                        % Value to copy depends on datatype of the class
                        % property:
                        %  - numeric: copy index (1,2,...)
                        %  - string: copy i'th string from .UserData or
                        %      .String, depending on whether current class
                        %      property value matches a string in .UserData
                        %      or in .String.
                        
                        % Test current property value against .UserData
                        % and then .String values.
                        if isnumeric(curValue)
                            t = hControl.Value;
                            identical = isequal(t,curValue);
                            if ~identical
                                newValue = t;
                                newString = hControl.String{hControl.Value};
                            end
                        
                        elseif ischar(curValue) || (isstring(curValue) && isscalar(curValue))
                            % Does class property value match any .UserData
                            % or .String string?
                            t = hControl.UserData;
                            if ischar(t) || (isstring(t) && isscalar(t))
                                t = {t};
                            end
                            if (iscellstr(t) || isstring(t)) && any(strcmp(curValue,t))
                                % Value is a .UserData string
                                val = t{hControl.Value};
                                identical = strcmp(curValue,val);
                                if ~identical
                                    newValue = val;
                                    newString = val;
                                end
                            else
                                % Value doesn't match any UserData string,
                                % or UserData didn't contain any strings.
                                %
                                % Does it match any .String string?
                                % We have no other options at this point.
                                t = hControl.String;
                                if ~any(strcmp(curValue,t))
                                    [Msg, Warn] = dialogmgr.untranslatedWarning([ ...
                                        'Dialog "%s":\n' ...
                                        '  Current value of property ''%s'' does not match\n' ...
                                        '  any string in the .String or .UserData properties of the\n' ...
                                        '  ''%s'' control.'], ...
                                        dc.Name,propName,hControl.Style);
                                    warning(Msg, Warn);
                                end
                                % Value is a .String item
                                val = t{hControl.Value};
                                identical = strcmp(curValue,val);
                                if ~identical
                                    newValue = val;
                                    newString = val;
                                end
                            end
                        else
                            dialogmgr.untranslatedError([ ...
                                'Dialog "%s":\n' ...
                                '  Data type "%s" of property "%s"\n' ...
                                '  not supported for use with "%s" controls.'], ...
                                dc.Name,class(curValue),propName,hControl.Style);
                        end
                        
                    case 'radiobutton'
                        % Copy .Value or .UserData string or .String to
                        % property, based on property data type.
                        %   Value: simply set into .Value property
                        %   String/UserData: if it matches, set Value to
                        %              .Max , otherwise .Min
                        %
                        % Value is one of two values specified by Min and
                        % Max.
                        
                        if default || strcmpi(arg,'value')
                            % update class property based on data type
                            
                            if ischar(curValue) || (isstring(curValue) && isscalar(curValue))
                                dialogmgr.untranslatedError([ ...
                                    'Dialog "%s":\n' ...
                                    '  String property types are unsupported for single radiobuttons.\n' ...
                                    '  Only radiobutton groups support string property values.\n' ...
                                    '  Use connectPropertyAndRadiobuttons to form groups.'], ...
                                    dc.Name);
                            end
                            % numeric index
                            t = hControl.Value;
                            identical = isequal(t,curValue);
                            if ~identical
                                newValue = t;
                                newString = '';
                            end
                        else
                            dialogmgr.untranslatedError([ ...
                                'Dialog "%s":\n' ...
                                '  Unsupported optional argument.'], ...
                                dc.Name);
                        end
                        
                    case {'checkbox','frame','pushbutton', ...
                            'slider','togglebutton'}
                        % Copy Value to property
                        %
                        % For checkbox, radiobutton and togglebutton, Value is
                        % one of two values specified by Min and Max.
                        %
                        % For slider, Value is Min <= Value <= Max.
                        %
                        % For pushbutton, Value never changes via user
                        % interaction with the control.  A button-press event
                        % copies Value to the property.  It is assumed that
                        % some other mechanism changes the property value, or
                        % changes the pushbutton Value.
                        if default || strcmpi(arg,'value')
                            t = hControl.Value;
                            identical = isequal(t,curValue);
                            if ~identical
                                newValue = t;
                                newString = '';
                            end
                        else
                            dialogmgr.untranslatedError([ ...
                                'Dialog "%s":\n' ...
                                '  Unsupported optional argument.'], ...
                                dc.Name);
                        end
                        
                    case 'uitable'
                        if default || strcmpi(arg,'data')
                            % Copy data from control to property.
                            t = hControl.Data;
                            identical = isequal(t,curValue);
                            if ~identical
                                newValue = t;
                                newString = '';
                            end
                        else
                            dialogmgr.untranslatedError([ ...
                                'Dialog "%s":\n' ...
                                '  Unsupported optional argument for uitable control.'], ...
                                dc.Name);
                        end
                        
                    otherwise
                        dialogmgr.untranslatedError([ ...
                            'Dialog "%s":\n' ...
                            '  Unhandled uicontrol style "%s".'], ...
                            dc.Name, hControl.Style)
                end
            end
            
            % Re-enable property listener:
            li.Enabled = liEnabled;
            
            % Only throw event if a change was made
            if ~identical && ~isWarn
                
                dc.StringValues.(propName) = newString;
                if ~dc.ManualApply
                    dc.(propName) = newValue;
                  	%notify(dc,'PropertyUpdate',dialogmgr.DCEvent(propName,'single',dc));
                else
                    dc.BackingStore.(propName) = newValue;
                    if any(strcmp(propName,dc.ManualApplyOverrideProps))
                        dc.(propName) = newValue;
                    else
                        dc.PendingProperties{end+1} = propName;
                       	notify(dc,'PendingUpdate', dialogmgr.DCEvent(propName,'single',dc));
                    end
                end
            end
        end
        
        function updateClassPropertyFromControlStringWithEngUnits( ...
                dc,propName,hControl,efmt)
            % Support for connectControlWithEngUnitsToProperty
            %
            % Convert from uicontrol string, which may have an engineering
            % unit specified after the numeric value, into a fully-scaled
            % numeric value.
            %
            % Format string efmt must be an "engineering" format, in which
            % both a numeric conversion specifier (like %g or %.2f) and a
            % char specifier (%c) must both be specified, in that order.
            % The string must enable textscan to parse the string into
            % numeric value and engineering unit char.
            %
            % Ex: uicontrol string is '62.1 mV', efmt="%g%c"
            %     numeric value set into property is 62.1e-3
            
            if nargin<5
                efmt = '%g%c';  % parses value, scale
                %efmt = '%g%c%s';  % parses value, scale, unit
            end
            
            % Get string from uicontrol
            str = deblank(hControl.String);
            
            % Parse string
            parse = textscan(str,efmt); % e.g., '6.12 mV'
            val = parse{1};             % e.g., 6.12
            scaleStr = parse{2};        % e.g., 'm' for milli
            %unit = parse{3};
            
            % Convert to a scaled value, interpreting to engineering units
            switch scaleStr
                case 'a', f=1e-18;
                case 'f', f=1e-15;
                case 'p', f=1e-12;
                case 'n', f=1e-9;
                case 'u', f=1e-6;
                case 'm', f=1e-3;
                case '', f=1;
                case {'k','K'}, f=1e3;
                case 'M', f=1e6;
                case 'G', f=1e9;
                case 'T', f=1e12;
                case 'P', f=1e15;
                case 'E', f=1e18;
                otherwise
                    dialogmgr.untranslatedError([ ...
                        'Dialog "%s":\n' ...
                        '  Unhandled engineering unit string "%s"'], ...
                        dc.Name, scaleStr);
            end
            val = val*f;  % Fully-scaled value
            
            % Only text-controls participate in decimal-align columns.
            %
            % External changes to the string of a text uicontrol is
            % unusual, since it is not an "input" element for customer
            % interaction.
            %
            % If it is changed, by programmatic means, this code will
            % execute.
            %
            % First, determine if this is a decimal-align element. If so,
            % we need to work with int/frac parts.
            
            da = getappdata(hControl,'DCTableForm_DecimalAlign');
            if isempty(da)
                % Not a decimal-aligned control element.
                %
                % Set scaled value into property
                newValue = val;
            	dc.StringValues.(propName) = str;

            else
                % Control is part of a decimal-aligned control pair.
                dialogmgr.untranslatedError([ ...
                    'Dialog "%s":\n' ...
                    '  External changes to the value of a uicontrol in ' ...
                    '  a decimal-aligned column is unsupported.'], ...
                    dc.Name);
            end
            
            if ~dc.ManualApply
                    dc.(propName) = newValue;
                  	%notify(dc,'PropertyUpdate',dialogmgr.DCEvent(propName,'single',dc));
                else
                    dc.BackingStore.(propName) = newValue;
                    if any(strcmp(propName,dc.ManualApplyOverrideProps))
                        dc.(propName) = newValue;
                    else
                        dc.PendingProperties{end+1} = propName;
                        notify(dc,'PendingUpdate');

                    end
            end

        end
    end
    methods(Access=private)
        function updateControlFromClassProperty(dlg, ...
                hControl,propName,arg,suppressControlListener)
            % Updates uicontrol based on the current value of a class
            % property.
            %
            % Performs identical-value detection, and does not throw the
            % Action event if property is the same value as the control.
            %
            % Invalid values of the class property will throw an ERROR, not
            % a warning.  This is done because class properties are changed
            % in a program, not by mouse-interaction; therefore it is a
            % program error and the program must halt.  This is unlike
            % user-interaction issues, which result in WARNINGS; programs
            % should not halt when faulty user input occurs.  Those faults
            % are normal and expected operation.
            %
            % The uicontrol property that is updated depends on the
            % uicontrol style.
            %
            %    uicontrol Style   uicontrol Property 
            %    ---------------   ---------------------------------
            %    checkbox          .Value
            %    edit              .String
            %    uieditv           .Value
            %    frame             .Value
            %    listbox           .Value
            %    popupmenu         .Value   or .String/.UserData
            %    radiobutton       .Value   or .String/.UserData
            %    pushbutton        .Value
            %    slider            .Value
            %    text              .String  (supports decimal alignment)
            %    togglebutton      .Value
            %
            % The value of the class property is converted into a new
            % value that is copied into the uicontrol property named above.
            % The default conversion performed is specified below, along
            % with options to modify the conversion performed.
            %
            %  Control Style   Optional arg
            %  -------------   ---------------------------------
            %  'edit','text'  'string' (default)
            %                 'eval' - evaluates string in base MATLAB
            %                          workspace
            %                  two function handles: [v=fcn(h),fcn(h,v)]
            %
            %  'popupmenu','listbox'
            %                  'selection' (default), the selected string
            %                  'value', the selection index
            %                  two function handles: [v=fcn(h),fcn(h,v)]
            %
            %  'checkbox','radiobutton','slider','togglebutton'
            %                  'value' (default)
            %                  two function handles: [v=fcn(h),fcn(h,v)]
            %
            %  'frame','pushbutton'
            %                  'value' (default)
            %                  two function handles: [v=fcn(h),fcn(h,v)]
            %
            %  'uitable'       -'data' (default)
            %                  -two function handles: [v=fcn(h),fcn(h,v)]
            
            if nargin<5
                suppressControlListener = false;
            end
            if nargin<4
                arg = '';
            end
            if suppressControlListener
                % Disable listener on this control that will set the class
                % property --- we got here because the class prop changed, so
                % we don't need to listen to that and repeat ourselves.
                li = getappdata(hControl,'ControlListenToControl');
                liEnabled = li.Enabled;
                li.Enabled = false;
            end
            
            [isWarn,identical] = convertClassProperty(dlg,hControl,propName,arg);
            
            if ~isWarn && ~identical
                % Our manual changes to the control above suppress further
                % Action events.
                notify(hControl,'Action');
            end
            
            if suppressControlListener
                % Re-enable control listener on this control
                li.Enabled = liEnabled;
            end
        end
        
        function updateControlStringWithEngUnitsFromClassProperty( ...
                dlg,hControl,propName,efmt)
            % Support for connectPropertyAndControlWithEngUnits
            %
            % Convert from property value to uicontrol string, applying
            % engineering-units conversion.
            %
            % Performs identical-value detection, and does not throw the
            % Action event if property is the same value as the control.
            %
            % Format string efmt must be an "engineering" format, in which
            % both a numeric conversion specifier (like %g or %.2f) and a
            % char specifier (%c) must both be specified, in that order.
            % The string must enable sscanf to construct a string with
            % numeric value and engineering unit char.
            %
            % Example efmt:
            % Default "%g %c", yields "3.1415926 m".
            %
            %  "%g %cV" yields "3.1415926 mV", where the unit
            % of Volts has been added
            %
            %  "%.2f %sHz" yields "3.14 MHz"
            %
            % Ex: property value is 0.00621, efmt="%.2 %cV"
            %     string set into uicontrol is '62.10 mV'

            % Get property value, convert to engineering units
            val = dlg.(propName);
            if ~isnumeric(val)
                dialogmgr.untranslatedError([ ...
                    'Dialog "%s":\n' ...
                    '  Property value "%s" is not numeric, conversion not supported.'], ...
                    dlg.Name, propName);
            end
            
            if nargin<4 || isempty(efmt)
                % This is an "engineering unit" format, which is a normal
                % printf-style format string, plus a "%s" after the number
                % somewhere to display the unit.
                efmt = '%g %c';
            end
            
            % Pretty-print scaled value
            % xxx add support for specifying # digits after decimal via fmt
            [v_eng,~,u_eng] = engunits(val);
            str = sprintf(efmt,v_eng,u_eng);
            
            % For decimal-aligned controls, we cache info in
            % appdata: DCTableForm_DecimalAlign.
            %
            da = getappdata(hControl,'DCTableForm_DecimalAlign');
            if isempty(da)
                % Not decimal-aligned
                %
                % simple string update:
                identical = strcmp(str,hControl.String);
                if ~identical
                    hControl.String = str;
                end
            else
                % Control is part of a decimal-aligned control
                % pair.  Class property value must be a numeric
                % scalar.
                %
                % appdata on control is a struct:
                %   .ColumnControlPairs = Nx2 matrix, [uictrls_int uictrls_frac]
                %   .ControlPair        = 1x2 vector, [uictrl_int uictrl_frac]
                identical = setDecimalAlignedElement_intfrac( ...
                    da.ControlPair(1), ...
                    da.ControlPair(2), ...
                    str, true);
            end
            
            % Our manual changes to the control above suppress further
            % Action events.  So we throw the event manually:
            %
            % Text widgets almost never have side-effects like visibility,
            % and need performance for updates.  We suppress the notify
            % action for text.
            if ~identical && ~strcmpi(hControl.Style,'text')
                notify(hControl,'Action');
            end
        end
    end
    
    % Helper for updateControlFromClassProperty
    %
    methods (Access=private)
        function [isWarn,identical] = convertClassProperty( ...
                dlg,hControl,propName,arg)
            % Determine how to convert class property value,
            % and perform that conversion.
            %
            % Copy value to uicontrol.
            
            if ~isempty(arg) && iscell(arg) && isa(arg{1},'function_handle')
                % Forward mapping fcn in arg{1},
                % Inverse mapping fcn in arg{2}
                feval(arg{2},hControl,dlg.(propName));
                
                isWarn = false;
                identical = false;
                
                return % EARLY EXIT
            end
            
            % Optional mappings
            %
            if isa(hControl,'dialogmgr.uieditvc')
                % NOTE: Must do this test BEFORE uieditv, since the
                % uieditvc is a subclass of uieditv.
                %
                % Map uieditvc (complex-valued numeric scalar) to be a'
                % special conversion from property to control
                if ~isempty(arg)
                    dialogmgr.untranslatedError('uieditvc does not support a format argument.');
                end
                style = 'editvc';
            elseif isa(hControl,'dialogmgr.uieditv')
                % Map uieditv to be a '%g' conversion from property to control
                if ~isempty(arg)
                    dialogmgr.untranslatedError('uieditv does not support a format argument.');
                end
                arg = '%g';
                style = 'edit';
            else
                style = hControl.Style;
            end
            
            switch style
                case 'text'
                    [isWarn,identical] = ...
                        convertClassProperty_text(dlg,hControl,propName,arg);
                case 'edit'
                    [isWarn,identical] = ...
                        convertClassProperty_edit(dlg,hControl,propName,arg);
                case 'editvc'
                    [isWarn,identical] = ...
                        convertClassProperty_editvc(dlg,hControl,propName,arg);
                case {'popupmenu','listbox','spintext'}
                    [isWarn,identical] = ...
                        convertClassProperty_popup_etc(dlg,hControl,propName,arg);
                case 'radiobutton'
                    [isWarn,identical] = ...
                        convertClassProperty_radiobutton(dlg,hControl,propName,arg);
                case {'slider','checkbox','togglebutton'}
                    [isWarn,identical] = ...
                        convertClassProperty_slider_etc(dlg,hControl,propName,arg);
                case {'frame','pushbutton'}
                    [isWarn,identical] = ...
                        convertClassProperty_pushbutton_etc(dlg,hControl,propName,arg);
                case 'uitable'
                    [isWarn,identical] = ...
                        convertClassProperty_uitable(dlg,hControl,propName,arg);
                otherwise
                    dialogmgr.untranslatedError([ ...
                        'Dialog "%s":\n' ...
                        '  Unhandled uicontrol style "%s".'], ...
                        dlg.Name, hControl.Style)
            end
        end
        
        function [isWarn,identical] = convertClassProperty_text( ...
                dlg,hControl,propName,arg)
            % Convert class property for use with text uicontrol
            % Write property value into text uicontrol
            
            isWarn = false;
            
            % Get the class property value
            pval = dlg.(propName);
            
            if isempty(arg) || strcmpi(arg,'string')
                if (ischar(pval) || (isstring(pval) && isscalar(pval))) && ~iscellstr(pval) 
                    str = pval;
                else
                    dialogmgr.untranslatedError([ ...
                        'Dialog "%s":\n' ...
                        '  Cannot update %s uicontrol based on class property %s:\n' ...
                        '  Class property does not contain a string or cell-string.\n' ...
                        '\n' ...
                        '  Consider specifying a format-conversion string or registering\n' ...
                        '  custom conversion functions in the dialog implementation.'], ...
                        dlg.Name, hControl.Style, propName);
                end
            elseif ischar(arg) || (isstring(arg) && isscalar(arg))
                % arg specifies a format-conversion string for
                % use with sprintf:
                str = sprintf(arg,pval);
            else
                dialogmgr.untranslatedError([ ...
                    'Dialog "%s":\n' ...
                    '  Unsupported optional argument.'], ...
                    dlg.Name);
            end
            
            % For decimal-aligned controls, we cache info in
            % appdata: DCTableForm_DecimalAlign.
            %
            da = getappdata(hControl,'DCTableForm_DecimalAlign');
            if isempty(da)
                % Not decimal-aligned
                %  - simple string update
                %
                % Suppress graphical update (and throwing an
                % event) if identical strings
                identical = strcmp(str,hControl.String);
                if ~identical
                    hControl.String = str;
                end
            else
                % Control is part of a decimal-aligned control
                % pair.  Class property value must be a numeric
                % scalar.
                %
                % appdata on control is a struct:
                %   .ColumnControlPairs = Nx2 matrix, [uictrls_int uictrls_frac]
                %   .ControlPair        = 1x2 vector, [uictrl_int uictrl_frac]
                identical = setDecimalAlignedElement_intfrac( ...
                    da.ControlPair(1), ...
                    da.ControlPair(2), ...
                    str,false);
            end
        end
        
        function [isWarn,identical] = convertClassProperty_editvc( ...
                dlg,hControl,propName,arg)
            % Convert class property for use with complex-valued numeric
            %   edit uicontrol.
            %
            % Write property value into uicontrol.

            isWarn = false;
            
            % Copy property to String
            pval = dlg.(propName);
            if isempty(arg)
                arg = '%g';
            end
            if ischar(arg) || (isstring(arg) && isscalar(arg))
                % Use sprintf with specified format string
                % E.g., '%g'
                %
                % This is a complex-valued numeric entry:
                if ~isscalar(pval)
                    t = mat2str(pval);
                else
                    t = num2str(pval,arg);
                end
                
                % Option to only update if the EVALUATED value changes.
                % However, the expression itself may be vastly different
                % even if the value is the same.  So let's always update
                % the string so the user sees the eval result.
                identical = strcmp(t,hControl.String);
                
                % Do the test so we get "fire" callbacks every time.  But
                % we want to update the visible text.
                %if ~identical
                    hControl.String = t;
                %end
                
                % For restoring last good value if an invalid
                % string is entered, we store the first value:
                %setappdata(hControl,'DCTableForm_LastValid_Val',pval);
                setappdata(hControl,'DCTableForm_LastValid_Str',t);
            else
                dialogmgr.untranslatedError([...
                    'Dialog "%s":\n' ...
                    '  Unsupported optional argument.'], ...
                    dlg.Name);
            end
        end
        
        function [isWarn,identical] = convertClassProperty_edit( ...
                dlg,hControl,propName,arg)
            % Convert class property for use with edit uicontrol
            % Write property value into uicontrol

            isWarn = false;
            
            % Copy property to String
            pval = dlg.(propName);
            if isempty(arg) || strcmpi(arg,'string')
                if (ischar(pval) || (isstring(pval) && isscalar(pval))) && ~iscellstr(pval) 
                    identical = strcmp(pval,hControl.String);
                    if ~identical
                        hControl.String = pval;
                    end
                else
                    dialogmgr.untranslatedError([ ...
                        'Dialog "%s":\n' ...
                        '  Cannot directly update %s uicontrol with property %s:\n' ...
                        '  Property does not hold a string or cell-string.\n' ...
                        '  Consider passing a format conversion specifier, such as ''%%g'',\n' ...
                        '  as the last argument to connectPropertyAndControl().'], ...
                        dlg.Name, hControl.Style, propName);
                end
                
            elseif ischar(arg) || (isstring(arg) && isscalar(arg))
                % Use sprintf with specified format string
                % E.g., '%g'
                if ~isscalar(pval)
                    t = mat2str(pval);
                else
                    t = sprintf(arg,pval);
                end
                identical = strcmp(t,hControl.String);
                if ~identical
                    hControl.String = t;
                end
                
                % For restoring last good value if an invalid
                % string is entered, we store the first value:
                %setappdata(hControl,'DCTableForm_LastValid_Val',pval);
                setappdata(hControl,'DCTableForm_LastValid_Str',t);
            else
                dialogmgr.untranslatedError([...
                    'Dialog "%s":\n' ...
                    '  Unsupported optional argument.'], ...
                    dlg.Name);
            end
        end
        
        function [isWarn,identical] = convertClassProperty_radiobutton( ...
                dlg,hControl,propName,arg)
            % Convert class property for radiobutton.
            %
            % Write property value into uicontrol .Value or
            % .String/.UserData property, based on data type of property.

            % We never warn, and never "revert" back to a former value
            % here.
            isWarn = false;
            
            if isempty(arg) || strcmpi(arg,'value')
                % Copy property value to uicontrol .Value or .String or
                % .UserData property, based on data type of property value.
                %
                % .Value must be a scalar numeric value,
                %    vmin<=v<=vmax
                %
                oldVal = hControl.Value;
                pval = dlg.(propName);
                if islogical(pval)
                    pval = pval+0; % force to numeric
                end

                if isnumeric(pval)
                    % .Value update
                    if ~isscalar(pval)
                        %isWarn = true;
                        dialogmgr.untranslatedError([ ...
                            'Dialog ''%s'':\n' ...
                            '  Cannot update ''%s'' uicontrol with value from property ''%s''.\n' ...
                            '  Numeric value is not scalar.'], ...
                            dlg.Name, hControl.Style, propName);
                    end
                    if pval<hControl.Min || pval>hControl.Max
                        %isWarn = true;
                        dialogmgr.untranslatedError([ ...
                            'Dialog "%s":\n' ...
                            '  Cannot update ''%s'' uicontrol with value from  property ''%s''.\n' ...
                            '  Numeric value (%g) is out of range ([%g,%g]).'], ...
                            dlg.Name, hControl.Style, propName, ...
                            pval, hControl.Min, hControl.Max);
                    end
                    identical = isequal(pval,oldVal);
                    if ~identical
                        hControl.Value = pval;
                        %setappdata(hControl,'DCTableForm_LastValid_Val',pval);
                    end
                    
                elseif ischar(pval) || (isstring(pval) && isscalar(pval))
                    % .UserData string or .String update
                    %
                    % If string (from property) matches UserData string or
                    % String, we set the radiobutton .Value to .Max.
                    %
                    % If string does not match either, we set the
                    % radiobutton .Value to .Min, without error.  It's the
                    % proper behavior - there may be a group of
                    % radiobuttons linked together, and the string
                    % presumably matches the UserData/String from one of those
                    % controls.
                    %
                    if strcmp(pval,hControl.UserData) || strcmp(pval,hControl.String)
                        newVal = hControl.Max;
                    else
                        newVal = hControl.Min;
                    end
                    identical = isequal(newVal,oldVal);
                    if ~identical
                        hControl.Value = newVal;
                        %setappdata(hControl,'DCTableForm_LastValid_Val',pval);
                    end
                else
                    dialogmgr.untranslatedError([ ...
                        'Dialog ''%s'':\n' ...
                        '  Cannot update ''%s'' uicontrol with value from property ''%s''.\n' ...
                        '  Property does not hold a numeric or string value.'], ...
                        dlg.Name, hControl.Style, propName);
                end
                
                % We always update the .Value property, not the "hit test"
                % property (hit test may be .Value, .UserData string or
                % .String)
                %
                % We never warn or revert value, so we don't cache the
                % DCTableForm_LastValidValue_Val/_Str:
                assert(~isWarn);
                
            else
                dialogmgr.untranslatedError([ ...
                    'Dialog "%s":\n' ...
                    '  Unsupported optional argument.'], ...
                    dlg.Name);
            end
        end
        
        function [isWarn,identical] = convertClassProperty_popup_etc( ...
                dlg,hControl,propName,arg)
            % Convert class property for use with the following uicontrols:
            %  - popupmenu
            %  - listbox
            % Write property value into uicontrol.

            isWarn = false;
            
            if isempty(arg) || strcmpi(arg,'selection')
                % Copy i'th deblanked string from string matrix
                % to property.
                str = dlg.(propName);
                if ~ischar(str) && ~(isstring(str) && isscalar(str))
                    dialogmgr.untranslatedError([ ...
                        'Dialog "%s":\n' ...
                        '  Cannot initialize %s control from property "%s"; property value must be a string.'], ...
                        dlg.Name, hControl.Style,propName);
                end
                
                v = [];
                ud = hControl.UserData;
                if ischar(ud) || iscellstr(ud) || isstring(ud)
                    v = find(strcmpi(str,cellstr(ud)));
                end
                if isempty(v)
                    v = find(strcmpi(str,cellstr(hControl.String)));
                    if isempty(v)
                        dialogmgr.untranslatedError([ ...
                            'Dialog "%s":\n' ...
                            '  Initial value of property "%s" does not match any Strings or UserData strings in "%s" control.'], ...
                            dlg.Name, propName, hControl.Style);
                    end
                end
                identical = strcmp(v,hControl.Value);
                if ~identical
                    hControl.Value = v;
                end
                
            elseif strcmpi(arg,'value')
                % Copy property value to uicontrol Value property.
                % Must be a scalar numeric value from 1 to N,
                % where N is # of strings.
                val = dlg.(propName);
                if ~isscalar(val) || ~isnumeric(val) || val~=fix(val)
                    dialogmgr.untranslatedError([ ...
                        'Dialog "%s":\n' ...
                        '  Cannot update %s uicontrol with property %s: ' ...
                        '  Property does not hold a scalar integer value.'], ...
                        dlg.Name, hControl.Style, propName);
                end
                vMax = size(hControl.String,1); % #rows=#entries
                if val<1 || val>vMax
                    dialogmgr.untranslatedError([ ...
                        'Dialog "%s":\n' ...
                        '  Cannot update %s uicontrol with property %s: ' ...
                        '  Property value is out of range.'], ...
                        dlg.Name, hControl.Style, propName);
                end
                % Valid - copy it:
                identical = isequal(val,hControl.Value);
                if ~identical
                    hControl.Value = val;
                end
            else
                dialogmgr.untranslatedError([ ...
                    'Dialog "%s":\n' ...
                    '  Unsupported optional argument.'], ...
                    dlg.Name);
            end
        end
        
        function [isWarn,identical] = convertClassProperty_slider_etc( ...
                dlg,hControl,propName,arg)
            % Convert class property for use with the following uicontrols:
            %   - slider
            %   - checkbox
            %   - radiobutton
            %   - togglebutton
            % Write property value into uicontrol

            isWarn = false;
            
            if isempty(arg) || strcmpi(arg,'value')
                % Copy property value to uicontrol Value property.
                % Must be a scalar numeric value,
                % vmin<=v<=vmax.
                val = dlg.(propName);
                
                if islogical(val)
                    val = val+0; % force to numeric
                end
                
                if ~isscalar(val) || ~isnumeric(val)
                    %isWarn = true;
                    dialogmgr.untranslatedError([ ...
                        'Dialog ''%s'':\n' ...
                        '  Cannot update ''%s'' uicontrol with value from property ''%s''.\n' ...
                        '  Property does not hold a scalar numeric value.'], ...
                        dlg.Name, hControl.Style, propName);
                end
                if val<hControl.Min || val>hControl.Max
                    isWarn = true;
                    identical = true; % equivalent to being identical?
                    
                    if isappdata(hControl,'DCTableForm_LastValidValue_Val')
                        prevVal = getappdata(hControl,'DCTableForm_LastValidValue_Val');
                    else
                        % Default: set to min
                        prevVal = hControl.Min;
                    end
                    % Restore last good value to class prop:
                    dlg.(propName) = prevVal;
                    
                    [Msg, Warn] = dialogmgr.untranslatedWarning([ ...
                        'Dialog "%s":\n' ...
                        '  Cannot update %s uicontrol from property %s:\n' ...
                        '  Property value (%g) is out of range ([%g,%g]).\n' ...
                        '  Restoring property to previous value (%g).'], ...
                        dlg.Name, hControl.Style, propName, ...
                        val, hControl.Min, hControl.Max, prevVal);
                    warning(Msg, Warn);
                else
                    % Valid - copy it:
                    identical = isequal(val,hControl.Value);
                    if ~identical
                        hControl.Value = val;
                        setappdata(hControl,'DCTableForm_LastValidValue_Val',val);
                    end
                end
            else
                dialogmgr.untranslatedError([ ...
                    'Dialog "%s":\n' ...
                    '  Unsupported optional argument.'], ...
                    dlg.Name);
            end
        end
        
        function [isWarn,identical] = convertClassProperty_pushbutton_etc( ...
                dlg,hControl,propName,arg)
            % Convert class property for use with the following uicontrols:
            %   - frame
            %   - pushbutton
            % Write property value into uicontrol

            isWarn = false;
            
            % Copy Value to property
            %
            % For pushbutton, Value never changes via user
            % interaction with the control.  A button-press event
            % copies Value to the property.  It is assumed that
            % some other mechanism changes the property value, or
            % changes the pushbutton Value.
            if isempty(arg) || strcmpi(arg,'value')
                t =  dlg.(propName);
                identical = isequal(t,hControl.Value);
                if ~identical
                    hControl.Value = t;
                end
            else
                dialogmgr.untranslatedError([ ...
                    'Dialog "%s":\n' ...
                    '  Unsupported optional argument.'], ...
                    dlg.Name);
            end
        end
        
        function [isWarn,identical] = ...
                    convertClassProperty_uitable(dlg,hControl,propName,arg)
            % Convert class property for use with uitable.
            % Write property value into uitable.
            
            isWarn = false;
            if isempty(arg) || strcmpi(arg,'data')
                t =  dlg.(propName);
                identical = isequal(t,hControl.Data);
                if ~identical
                    hControl.Data = t;
                end
            else
                dialogmgr.untranslatedError([ ...
                    'Dialog "%s":\n' ...
                    '  Unsupported optional argument for uitable.'], ...
                    dlg.Name);
            end
        end
    end
    
    %Manual Apply methods
    methods
        function applyPendingChanges(dc,varargin)
            
            changedProps = dc.PendingProperties;
            %We recursively validate all subdialogs and ourselves and then
            %we recursively apply all values.  We cannot validate and apply
            %each dialog at the same time in one recursion because we may
            %apply dialogs before a validation fails
            err = dc.recursiveValidate();
            if ~isempty(err)
                return;
            end
            changesMade = dc.recursiveApply();
            if changesMade
                %Only have top-level notify of PropertyUpdate so that we
                %don't notify for each saved property
                notify(dc,'PropertyUpdate',dialogmgr.DCEvent(changedProps,'char',dc));
            end
            if ~isempty(dc.ApplyButton)
                dc.ApplyButton.Enable = 'off';
            end
        end
        
        function revertPendingChanges(dc,~,~)
            %Loops through all control-property pairs and reverts the
            %control values to the class properties
                       
            for i=1:length(dc.SubDCTableForms)
                revertPendingChanges(dc.SubDCTableForms{i});
            end
            
            N = length(dc.ControlPropertyPairs);
            for i=1:N
                curPair = dc.ControlPropertyPairs{i};
                dlg = curPair{2};
                propName = curPair{3};
                control = curPair{4};
                arg = curPair{5};
                
                dlg.BackingStore.(propName) = dlg.(propName);
                switch curPair{1}
                    case 'control'
                        updateControlFromClassProperty(dlg,control,propName,arg);
                    case 'control_eng'
                        updateControlStringWithEngUnitsFromClassProperty(dlg,propName,'')
                    case 'radio'
                        updateRadiobuttonGroupFromClassProperty(dlg,control(1),propName)
                    otherwise
                        [Msg, Warn] = dialogmgr.untranslatedWarning(['Unknown control type in revertPendingChanges: ' curPair{1}]);
                        warning(Msg,Warn);
                end
            end
        end
        
        function V = isValid(dc,src,ev)
            %Returns whether any errors are present in the dialog
            %Loops through all controls and looks for uieditv's with errors
            
            if nargin<3
                ev.EventName = '';
            end
            if nargin<2
                src = [];
            end
            
            for i=1:length(dc.SubDCTableForms)
                if ~isValid(dc.SubDCTableForms{i},src,ev)
                    V=false;
                    return;
                end
            end
        
            for i=1:numel(dc.Controls)
                curEdit = dc.Controls{i};
                if isempty(curEdit) || (strcmp(ev.EventName,'Action') && src == curEdit)
                    %Skip the source of the call to isValid because it may
                    %still have an error at this time due to this function
                    %call happening before its HasError property being set
                    continue
                end
                if isa(curEdit,'dialogmgr.uieditv') && strcmp(curEdit.Visible,'on') && curEdit.HasError
                    %Only uieditv has the HasError property
                    V = false;
                    return
                end
            end
            
            V = true;
        end
        
        function setSubDialogsManual(dc,parentIsManual,manualApplyAncestor)
            %Resolve which dialogs should be manual apply
            %Do this by beginning at top-level and recursing downward
            %through the children
            %If we find a dlg that is ManualApply and the parent is not,
            %then set it to the oldest manual apply ancestor and recurse
            %through to set all of its children manual apply
            
            if dc.ManualApply && ~parentIsManual
                %top-level
                manualApplyAncestor = dc;
            end

            dc.ManualApply = dc.ManualApply || parentIsManual;
            if dc.ManualApply && ~isempty(manualApplyAncestor.ApplyButton)
                setupApplyEnableListeners(dc,manualApplyAncestor);
            end
            for i=1:length(dc.SubDCTableForms)
                setSubDialogsManual(dc.SubDCTableForms{i},dc.ManualApply,manualApplyAncestor);
            end
            
        end
        
        function setProperties(dc,varargin)
            %This function must be called in a manual apply setting in
            %order to set properties.  It tries to set them and runs
            %validation.  On success, it sets the values.  On failure, it
            %does not set the values and shows the error in the command
            %window
            %
            %Varargin is pv pairs
            propNames = varargin(1:2:end);
            values = varargin(2:2:end);
            
            if length(propNames)~=length(values)
                dialogmgr.untranslatedError('setProperties takes pv pairs');
            elseif any(cellfun(@(c)~ischar(c) || ~isprop(dc,c),propNames))
                dialogmgr.untranslatedError('Invalid property name sent to setProperties');
            end
            
            oldBackingStore = dc.BackingStore;
            for i=1:numel(propNames)
                dc.BackingStore.(propNames{i}) = values{i};
            end
            controlPropertyPairsToSet = zeros(size(propNames));
            try
                validate(dc);
                
                %Loop through property-control pairs and check all
                %uieditv's validation function or valid attributes
                N = length(dc.ControlPropertyPairs);
                cppIDX = 1;
                for i=1:N
                    curPair = dc.ControlPropertyPairs{i};
                    propName = curPair{3};
                    curPropInd = find(strcmp(propName,propNames));
                    if ~isempty(curPropInd);
                        controlPropertyPairsToSet(cppIDX) = i;
                        cppIDX = cppIDX + 1;
                       	curVal = values{curPropInd};
                        hControl = curPair{4};
                        if isa(hControl,'dialogmgr.uieditv')
                            %Run the validation on uieditv
                            if ~isempty(hControl.ValidationFunction)
                                hControl.ValidationFunction(curVal,'setProperties',propName,hControl.ValidAttributes);
                            elseif ~isempty(hControl.ValidAttributes)
                                validateattributes(curVal, hControl.ValidClasses,hControl.ValidAttributes,'setProperties',propName);
                            end
                        end
                    end
                end
            catch err
                %Revert the BackingStore and throw error to command prompt
                dc.BackingStore = oldBackingStore;
                rethrow(err);
            end
            
            dc.PendingProperties = {};
         
            %trigger updateControlFromClassProperty whose listener was 
            %disabled during applySingleDialog
            for i=controlPropertyPairsToSet
                curPair = dc.ControlPropertyPairs{i};
                hControl = curPair{4};
                propName = curPair{3};
                li2 = getappdata(hControl,'PropertyChangeTriggerUpdate');
                li2.Enabled = false;
                li3 = getappdata(hControl,'ControlListenToControl');
                li3.Enabled = false;               
              	dc.(propName) = dc.BackingStore.(propName);
                dc.StringValues.(propName) = hControl.String;
                li2.Enabled = true;
                li3.Enabled = true;
            end
            notify(dc,'PropertyUpdate',dialogmgr.DCEvent(propNames,'char',dc));
        end
        
    end
    
    methods(Access=protected)
        
        function setManualApply(dc)
           dc.ManualApply=true; 
        end
        
        function addApply(dc)
            setManualApply(dc);
            dc.ApplyButton = uipushbutton(dc,'Apply','Callback',@(src,ev)dc.applyPendingChanges(src,ev),'Enable','off');
            dc.ApplyButton.Tag = 'ApplyTag';
        end
        
        function addApplyAndRevert(dc)
            setManualApply(dc);
            dc.ApplyButton = uipushbutton(dc,'Apply','Callback',@(src,ev)dc.applyPendingChanges(src,ev),'Enable','off');
            dc.ApplyButton.Tag = 'ApplyTag';
            dc.RevertButton = uipushbutton(dc,'Revert','Callback',@(src,ev)dc.revertPendingChanges(src,ev));
            dc.ApplyButton.Tag = 'RevertTag';
        end
        
        function addValidationFunction(dc, func)
            %Adds a validation function to be run on apply press

            %It will call func, passing a struct 'pending' and a cell array
            %'changes' where pending has fields for each connected property
            %and changes has an entry for each property that is being
            %changed
            
            if ~isa(func,'function_handle')
                dialogmgr.untranslatedError('setValidationFunction function parameter is not a function handle ');
            end 
            dc.ValidationFunctions{end+1} = func;
        end
        
        function forceAutoApply(dc, varargin)
            %Make dialog properties act as if they were auto apply
            dc.ManualApplyOverrideProps{end+1} = varargin{:};
        end
        
        function setVisibilityOnState(dc,propNames,states,tag,vis)
            %Sets the visibility of a row depending on the state of
            %properties
            %----------
            %   propNames   : cell array of property names as strings
            %   states      : cell array of values for corresponding properties
            %   tag         : tag of control whose row visibility is altered
            %   vis         : 1/0 depending on if the visibility should be on/off
            %-----------
            %If the properties are in the correct state, set the row
            %visibility to vis, otherwise set it to ~vis
            
         	li = addlistener(dc,'PropertyUpdate',@(~,~)dc.checkStatesForVisibility(propNames,states,tag,vis));
         	li2 = addlistener(dc,'PendingUpdate',@(~,~)dc.checkStatesForVisibility(propNames,states,tag,vis));
            dc.RowVisLis{end+1} = li;
            dc.RowVisLis{end+1} = li2;
        end
        
    end
    
    methods(Access=private)
        
        function checkStatesForVisibility(dc, propNames,states,tag,vis)
            %Loop through properties and verify they are at the correct
            %state.  If so, set tag's row to visibility vis, otherwise set
            %it to visibility ~vis
            
            shouldUpdate = true;
            if dc.ManualApply
                for j = 1:numel(propNames)
                    if ischar(states{j}) || (isstring(states{j}) && isscalar(states{j}))
                        if ~any(strcmp(dc.BackingStore.(propNames{j}),states{j}))
                            shouldUpdate = false;
                        end
                    elseif ~any(dc.BackingStore.(propNames{j})==states{j})
                    	shouldUpdate = false;
                    end
                end
            else
                for j = 1:numel(propNames) 
                    if ischar(states{j}) || (isstring(states{j}) && isscalar(states{j}))
                        if ~any(strcmp(dc.(propNames{j}),states{j}))
                            shouldUpdate = false;
                        end
                    elseif ~any(dc.(propNames{j})==states{j})
                     	shouldUpdate = false;
                    end
                end
            end
            if ~shouldUpdate
                vis = ~vis;
            end
            updateRowVisibilityForTag(dc,tag,vis); 
        end
        
        function updateRowVisibilityForTag(dc,tag,vis)
            %Sets the row visibility for a given tag to vis(1/0)
            action = [];
            action.uiValue = 0;
            [action.RowIdx,action.ColIdx,action.DlgDst] = ...
                findIndicesFromControlTags(dc,tag);
            action.State = vis;
            action.ActionType = 'RowVis';
                
            [~,dc_changed] = setRowVisibility_updateRRV(action,false);

            setRowVisibility_static(action,false,dc_changed);
        end
        
        function err = recursiveValidate(dc)
            %Tries to validate each dialog
            %Runs validation functions and captures errors to display them
            %in an error dialog
            %
            %Errors may be thrown with message 'NOSHOW' to simply error out
            %without displaying a dialog.  This may be useful for apps to
            %handle errors in there own way, e.g. by throwing warnings
            
            %Returns an error which may be empty
            err = [];
            for i=1:length(dc.SubDCTableForms)
                if strcmp(dc.SubDCTableForms{i}.Dialog.DialogBorder.Panel.Visible,'on')
                    %Only validate it if it is displayed on screen
                    err = recursiveValidate(dc.SubDCTableForms{i});
                end
            end
            try
                validate(dc);
            catch err
                %If the error message is NOSHOW, then do not display
                %the error dialog and simply return.  This is to allow
                %users to not have to display an error dialog while
                %still being able to stop the save
                if ~strcmp(err.message,'NOSHOW');
                    errordlg(err.message);
                end
            end
        end
        
        function changesMade = recursiveApply(dc)
            %Save the dialog and its subdialogs
            %Return whether any changes were actually saved so that the
            %top-level knows whether to notify with PropertyUpdate
            changesMade = false;
            for i=1:length(dc.SubDCTableForms)
                if strcmp(dc.SubDCTableForms{i}.Dialog.DialogBorder.Panel.Visible,'on')
                    %Only save it if it is displayed on screen
                    changesMade = changesMade || recursiveApply(dc.SubDCTableForms{i});
                end
            end
            if dc.ChangesPending
                applySingleDialog(dc);
                changesMade = true;
            end
        end
        
        function applySingleDialog(dc)
            %Save all properties to class
            %Disable listeners because this is manual apply so the controls
            %are already updated and we also do not want to trigger
            %PropertyUpdate events for all properties as we are batch
            %saving
            dc.PendingProperties = {};

            N = length(dc.ControlPropertyPairs);
            for i=1:N
                curPair = dc.ControlPropertyPairs{i};
                propName = curPair{3};
                hControl = curPair{4};
                li = getappdata(hControl,'ControlListenToProperty');
                li.Enabled = false;
                li2 = getappdata(hControl,'PropertyChangeTriggerUpdate');
                li2.Enabled = false;
              	dc.(propName) = dc.BackingStore.(propName);
                li.Enabled = true;
                li2.Enabled = true;
            end
            
        end
        
        
        function setupApplyEnableListeners(dc,ancestor)
            %Disables apply button when error is displayed
            %Enables apply button when properties are updated or errors are
            %removed AND there are no other errors
            for i=1:numel(dc.Controls)
                curEdit = dc.Controls{i};
                if isempty(curEdit)
                    continue
                end
                if isa(curEdit,'dialogmgr.uieditv')
                    addlistener(curEdit,'Action',@(src,ev)ancestor.enableApply(src,ev));
                    addlistener(curEdit,'ErrorAction',@(~,~)set(ancestor.ApplyButton,'Enable','off'));
                end
            end
            
         	addlistener(dc,'PendingUpdate',@(~,~)ancestor.enableApply);

        end
        
                
        function enableApply(dc,src,ev)
            %Try to enable the apply button if no errors are displayed
            if nargin<3
                ev.EventName = '';
            end
            if nargin<2;
                src = [];
            end
            if isValid(dc,src,ev)
                dc.ApplyButton.Enable = 'on';
            else
                dc.ApplyButton.Enable = 'off';
            end
        end
        
        function validate(dc)
            %Call all validation functions
            N = length(dc.ValidationFunctions);
            for i=1:N
                func = dc.ValidationFunctions{i};
                func(dc.BackingStore,dc.PendingProperties);
            end
        end
        
    end
    
end

% ============================================
% LOCAL FUNCTIONS
% ============================================

function cw_int = interpResizeableCols(cw,W,ibs,ics)
% Increase width of resizeable columns until W is achieved, if possible.
%
%   W: total inner width measured just inside uipanel BorderWidth
% ibs: inner-border spacing
% ics: inter-column spacing
%
% W includes ics and ibs in its pixel count.

% Initial column widths = minimum column widths: [minW; maxW]
cw_int = cw(1,:);  % [minW_col1 minW_col2 minW_col3 ...]

% Get sum of widths of all column minimums.  These cols will be the
% indicated widths, regardless of spillover past W.  Min widths are never
% inf.
Nc = numel(cw_int);
Wcur = sum(cw_int) + 2*ibs + (Nc-1)*ics;
excess_w = floor(W-Wcur); % NOTE: W may include fractional pixels
if excess_w <= 0
    % No excess.
    %
    % Allow last cols to extend past W.
    return
end

% Excess available.
%
% Give each resizeable column an equal amount of additional pixel width,
% until one resizeable column hits its max. Mark that columns as no longer
% resizeable, then repeat until we exhaust W or have expanded all cols to
% their max.
while 1
    % Find index of resizeable columns remaining
    % Compare [maxW_col1 maxW_col2 maxW_col3 ...] to proposed col widths
    colResize = cw(2,:) > cw_int;
    resizeColsIdx = find(colResize);
    Nc = numel(resizeColsIdx);
    if Nc==0
        % We did not occupy the full W pixel width.
        return
    end
    
    % Find next smallest upper-limit for resizable columns
    [nextSmallestWMax,i] = min(cw(2,resizeColsIdx));
    idx = resizeColsIdx(i);
    
    % Do we need only one more increment to all remaining resizeable cols,
    % based on this new nextSmallestWMax?
    onceAndDone = isinf(nextSmallestWMax); % we're hitting inf max
    if ~onceAndDone
        % We have a finite max, but will it provide sufficient width to
        % allows us to hit W in one more step?
        nextSmallestWIncr = nextSmallestWMax - cw(1,idx);
        max_wstep = Nc*nextSmallestWIncr;
        onceAndDone = max_wstep >= excess_w;
    end
    if onceAndDone
        % All remaining resizeable cols have inf max, and can take all
        % remaining pixel width:
        % Adding this many pixels to the width will exceed the available
        % excess width. We will end iterations with this step, and we need
        % to trim the width a bit to hit excess_w exactly.
        
        % Add uniform number of pixels to widths of all remaining
        % resizeable columns.
        WIncr_per_col = floor(excess_w/Nc);
        cw_int(resizeColsIdx) = cw_int(resizeColsIdx) + WIncr_per_col;
        
        % # pixels left over
        WPartial = excess_w - WIncr_per_col*Nc;
        
        % There may be a few pixels of width (< Nc) remaining.
        % Give these to the earliest resizeable columns first.
        Ni = WPartial;
        cw_int(resizeColsIdx(1:Ni)) = cw_int(resizeColsIdx(1:Ni)) + 1;
        
        % All done - we have achieved W pixels width.
        return
    end
    
    % We can use all of this width AND possibly more
    cw_int(resizeColsIdx) = cw_int(resizeColsIdx) + nextSmallestWIncr;
    excess_w = excess_w - max_wstep;
end

end

function errmsg = updateStringControlFromValueControl(hString,hValue,fmt,listenerToSuppress)
% Update control with .String property from hValue uicontrol.
%
% Syntax of update[A]From[B]:
%   update[A]From[B](A, B, args)  <-- i.e., (dst,src,args)

manage_listener = nargin>3 && ~isempty(listenerToSuppress);
if manage_listener
    % Shut down hString-listener referenced by appdata name:
    lis = getappdata(hValue,listenerToSuppress);
    lis.Enabled = false;
end

val = hValue.Value;
[str,errmsg] = sprintf(fmt,val);
error(errmsg);
hString.String = str;

% hString is modified, and our manual change here suppresses further Action
% events (like visibility listeners, etc)
%notify(hString,'Action');

if manage_listener
    lis.Enabled = true;
end

end

function errmsg = updateValueControlFromStringControl(hValue,hString,fmt,listenerToSuppress)
% Update value of hValue uicontrol by converting and copying current string
% from hString uicontrol.
%
% If conversion of String fails, or is out of range for the Value
% uicontrol, the Value is preserved and the String is restored to the last
% valid string.
%
% Syntax of update[A]From[B]:
%   update[A]From[B](A,B,args)  <-- i.e., (dst,src,args)

if nargin>3 && ~isempty(listenerToSuppress)
    % Shut down hString-listener referenced by appdata name:
    lis = getappdata(hString,listenerToSuppress);
    lis.Enabled = false;
end

str = strtrim(hString.String);
err = isempty(str);

if ~err
    % Try numeric conversion of string
    [val,~,errmsg] = sscanf(str,fmt);
    err = ~isempty(errmsg);
end

% Try resolving string as an expression in the base workspace if we failed
% to convert as an explicit constant (number) above.
if err
    try %#ok<TRYNC>
        val = evalin('base',str);
        err = false;
    end
end

if ~err
    % confirm it's a real scalar
    err = ~isscalar(val) || ~isreal(val);
end

% Test against limits
if ~err
    vmin = get(hValue,'min');
    vmax = get(hValue,'max');
    err = val<vmin || val>vmax;
    if ~err
        hValue.Value = val;
        hValue.UserData = val;
        hString.UserData = str;
    end
end

if err
    % Error condition
    %
    % Leave hValue as-is.  Restore previous string to hString, which we
    % obtain from UserData on hValue from last successful update.  At
    % least one update must have occurred at the time the controls were
    % connected.
    hString.String = hString.UserData;
else
    % hString is modified, and our manual change here suppresses further
    % Action events (like visibility listeners, etc).
    notify(hValue,'Action');
end

if nargin>3 && ~isempty(listenerToSuppress)
    lis.Enabled = true;
end

end

function updateValueControlFromEditControl(hValue,hEdit,listenerToSuppress)
% Update value of hValue uicontrol from current value of uieditv control.
%
% Syntax of update[A]From[B]:
%   update[A]From[B](A,B,args)  <-- i.e., (dst,src,args)

manage_listener = nargin>2 && ~isempty(listenerToSuppress);
if manage_listener
    % Shut down hEdit-listener referenced by appdata name
    lis = getappdata(hEdit,listenerToSuppress);
    lis.Enabled = false;
end

val = hEdit.LastValidVal;
assert(val>=hValue.Min && val<=hValue.Max);
hValue.Value = val;
hValue.UserData = val;

% hString is modified, and our manual change here suppresses further
% Action events (like visibility listeners, etc).
notify(hValue,'Action');

if manage_listener
    lis.Enabled = true;
end

end

function s_ij = getMinControlExtent(ht,tmp_txt)
% Return struct with minimum extent for one uicontrol.
%    .horiz    - minimum horizontal extent, in pixels
%    .vert     - minimum vertical extent
%    .hoffset  - relative offset; 0 is nominal; can be -ve or +ve
%    .voffset  - relative offset
% This calibrates all uicontrols based on the platform.
%
% .horiz and .vert return the minimum extent of the uicontrol.
% Reference is the 'text' uicontrol.
%
% Most uicontrols do not align vertically or horizontally with a text
% uicontrol when placed with the same vertical position and vertical size.
% We assume the reference is the 'text' uicontrol, and adjust the position
% of all other uicontrols relative to that in order to have them align
% visually when rendered.
%
% .voffset is used to correct the vertical position of a uicontrol, so that
% its horizontal center line aligns with that of a text control, such that a
% row containing a text uicontrol and this uicontrol will be visually
% centered.
%
% .hoffset is used to correct the horizontal position of a uicontrol, so
% that its vertical center line aligns with that of a text control, such
% that a column containing a text uicontrol and this uicontrol will be
% visually centered.
%
% Requires a pre-allocated temporary text uicontrol passed in as tmp_txt, to
% use for extent assessment of non-text uicontrols.

% defaults:
h0 = 0; % horiz offset, to align down cols
v0 = 0; % vert offset, to align across rows

if isempty(ht)
    % Do not try to compute extent - it will fail
    h = 0;
    v = 0;
else
    % Normalize temp text attributes:
    if ~isa(ht,'dialogmgr.uidialog')
        set(tmp_txt, ...
            'String',     'I', ...
            'FontName',   ht.FontName, ...
            'FontUnits',  ht.FontUnits, ...
            'FontSize',   ht.FontSize, ...
            'FontWeight', ht.FontWeight);
            %'FontAngle',  ht.FontAngle); % xxx not supported by uitable
    end
    
    switch ht.Style
        case 'text'
            % Reference - no change by definition
            if isempty(ht.String)
                % Vertical will be 4 pix or less when string is empty.
                % Vertical should be a standard height regardless of string
                % being empty.  So we get the height of a typical tall
                % letter.
                ext = tmp_txt.Extent;
                h = ext(3);
                v = ext(4);
                
                % Should do h=ht.Extent(3), but it's faster to take it from
                % tmp_txt.Extent(3).  The "I" will have more width than the
                % blank string, but it won't matter - they're both very
                % narrow.  And the times we have empty strings is generally
                % when we have a column where at least one other string is
                % non-empty and is setting the overall width.  So the max
                % column width is already being taken from some other
                % control - meaning the approximation is not important.
                %h = ht.Extent(3);
            else
                ext = ht.Extent;
                h = ext(3); % horiz
                v = ext(4);
                if ismac
                    v0 = 3;
                end
            end
            
        case 'edit'
            h = ht.Extent(3) + 6; % horiz % +6 for rfBudgetAnalyzer
            if isempty(ht.String)  
              tmp_txt.String = ' ';
            else
              tmp_txt.String = ht.String;
            end
            v = tmp_txt.Extent(4)+5; % pixels
            %ha = ht.HorizontalAlignment;
            %dy = ht.Position(4);
            %fs = ht.FontSize;           
            %[h0,v0] = getControlExtentOffset_edit(ha,dy,fs);
            
        case {'checkbox','radiobutton'}
            if isempty(ht.String)
                h = 18;
                v = 18;
            else
                ext = ht.Extent;
                h = ext(3); % horiz
                v = ext(4); % vert
                % PC:
                %
                % Horiz extent doesn't include width of button (either for the
                % radiobutton circle or checkbox square)
                h = h + 20;
                if ht.FontSize <= 5
                    v = v + 3;
                end
            end
            % Push it up 2 pixels so the text aligns
            v0 = 2;
            
        case 'popupmenu'
            if ispc
                % Use horiz extent from temporary text control.
                %
                % Extent of popup gives horiz-extent of the currently shown
                % text - but what we need is the horiz-extent of the LONGEST of
                % the popup strings! Putting the string into a 'text' control
                % gives us what we need.
                tmp_txt.String = ht.String;
                h = tmp_txt.Extent(3)+24; % 24
                
                v = ht.Extent(4); % vert
                %v = 22;
                
                % Use the vertical position size (dy)from
                % actual control as the vertical extent, and DO
                % NOT vary this height! The control won't
                % resize vertically; it will cause offset to
                % the origin however, which is undesired.
                %ext_t = get(ht,'pos');
                %v = ext_t(4);
                v0 = 4;
                
            elseif ismac
                tmp_txt.String = ht.String;
                h = tmp_txt.Extent(3)*1.3 + 38; % for rfBudgetAnalyzer
                v = 19; % mac always sets popups to 19pix high
                
                h0 = 0;
                v0 = 1;
                
            else % Linux
                tmp_txt.String = ht.String;
                h = tmp_txt.Extent(3)*1.2 + 24; % for rfBudgetAnalyzer
                tmp_txt.String = ht.String{1};
                v = tmp_txt.Extent(4)+4;
                
                h0 = 0;
                v0 = 0;
            end
            
        case {'pushbutton','togglebutton'}
            %h = ht.Extent(3) % horiz
            
            tmp_txt.String = ht.String;
            h = tmp_txt.Extent(3);
            v = tmp_txt.Extent(4)+4;
            
        case 'slider'
            % Careful here!  Vertical is fine to get from Position, but
            % horizontal is not --- that's not "min", that's "actual" (or
            % current).
            %
            % Extent generally returns [0 0 4 4] (pc).
            % Aspect ratio is essential:
            %   - horiz slider: dx > dy
            %   - vertical slider: dy > dx
            % If you violate these, the slider changes orientation.
            
            % PC:
            %  arrows can be as small as 4x4, but need dx>dy or we get
            %  vertical.  So take the user's dy and add 1 to guarantee
            %  minimum size plus proper aspect ratio.
            
            % Use "local text size"
            v = tmp_txt.Extent(4);
            
            h = v+1; % yes, use v!  not ht.Extent, this is a minimum
            v0 = 2;
            
        case 'listbox'
            tmp_txt.String = ht.String;
            h = tmp_txt.Extent(3)+24; % calibration?
            v = ht.Extent(4); % vert

        case 'frame'
            v = ht.Extent(4); % vert
            tmp_txt.String = ht.String;
            h = tmp_txt.Extent(3)+20;
            
        case 'dialog'
            % Need minimum width that uidialog can render at, not its
            % current width.
            [h,v] = getMinSize(ht);
            
        case 'hline'
            % Horizontal line
            
            % Need minimum width that hline can render at, not its
            % current width.
            [h,v] = getMinSize(ht);
            
            % Align to center line of uppercase text
            tmp_txt.String = 'I';
            ext = tmp_txt.Extent;
            v0 = floor(ext(4)/2);
            
        case 'spintext'
            ext = ht.Extent;
            h = ext(3); % horiz
            v = ext(4); % vert
            v0 = +1;

        case 'uitable'
            % Need to extend vertical in order to accommodate a bug in the
            % table.  It expands its row size vertically when focus is
            % moved.  We don't want that expansion to trigger the vertical
            % scrollbar from appearing.
            ext = ht.Extent;
            h = ext(3);   % horiz
            
            % Bug fix: uitable with .RowName=[] shows a bug where the rows
            % want to be "short", but sometimes become "tall".  When the
            % "tall" situation occurs, the vertical scrollbars often become
            % visible and is not desired.  We add height to ensure the
            % change in height does not trigger the addition of scroll
            % bars.  This doesn't stop the short/tall switching.
            v = ext(4)+9; % vert
            
        otherwise
            dialogmgr.untranslatedError('Unsupported style "%s".',ht.Style);
    end
end

% Update information:
s_ij.horiz   = h;
s_ij.vert    = v;
s_ij.hoffset = h0;
s_ij.voffset = v0;

end

function [h0,v0] = getControlExtentOffset_edit(ha,dy,fs)
% Return (x,y) offset to add to position of EDIT uicontrol
% to have it align with reference text control.

if ispc
    % PC calibration

    % Offset based on:
    %   - HorizontalAlignment
    %   - String length
    switch ha
        case 'left'
            offset = [-1 2];
        case 'center'
            % Centering between text/edit depends on String length
            N = numel(fs);
            if rem(N,2)
                offset = [1 2];
            else
                offset = [2 2];
            end
        case 'right'
            offset = [4 2];
    end
    
    % Offset based on:
    %  - Height (Position)
    v1 = fix((dy-16)/2) - 2;
    
    % Offset based on:
    %  - FontSize
    if fs <= 10
        fsDy = [7 7 5 4 4 2 1 0 -1 -3];
        v2 = fsDy(fs);
    else
        % fs=11,12,13,... -> t=-3,-4,-5,...
        v2 = 8-fs;
    end
    if fs>=15 && fs<=23
        %    fs: 15 16 17 18 19 20 21 22 23 pts
        dyLim = [32 34 36 38 38 42 44 40 48];
        if dy >= dyLim(fs-14)
            v2 = v2-1;
        end
    elseif fs>=24
        %  fs: 20 21 22 23 ...
        % lim: 42 44 46 48 ...
        if dy >= 42+(fs-20)*2
            v2 = v2-1;
        end
    end
    
    offset(2) = offset(2) + v1 + v2;
    
elseif ismac
    % MAC calibration
    
    % Offset based on:
    %   - HorizontalAlignment
    %   - String length
    switch ha
        case 'left'
            offset = [-2 11];
        case 'center'
            % Centering between text/edit depends on String length
            N = numel(fs);
            if rem(N,2)
                offset = [2 11];
            else
                offset = [1 11];
            end
        case 'right'
            offset = [5 11];
    end
    
    % Offset y-coord based on:
    %  - Control height, in pixels
    if dy >= 40
        v1 = 0;
    elseif dy <= 0
        v1 = 18;
    else
        tt = [18 18 18 18 17 17 16 16 15 14 14 13 13 12 12 11 11 10 10 10 9 9 8 8 7 7 6 6 6 6 5 5 4 4 3 3 2 2 1];
        v1 = -tt(dy);
    end
    
    % Offset y-coord based on:
    %  - FontSize, in pts
    %
    % Index is points, and must be an integer
    fsDy = [6 5 5 4 4 3 3 2 2 1 1  0  0 -1 -1 -2 -2 -2 -3 -3 -4 -4];
    v2 = fsDy(round(fs));
    
    offset(2) = offset(2) + v1 + v2;
    
else
    % Linux Calibration
    
    % Offset based on:
    %   - HorizontalAlignment
    %   - String length
    switch ha
        case 'left'
            offset = [0 11];
        case 'center'
            % Centering between text/edit depends on String length
            N = numel(fs);
            if rem(N,2)
                offset = [10 11];
            else
                offset = [1 11];
            end
        case 'right'
            offset = [7 11];
    end
    
    % Offset y-coord based on:
    %  - Control height, in pixels
    if dy >= 40
        v1 = 0;
    elseif dy <= 0
        v1 = 18;
    else
        tt = [18 18 18 18 17 17 16 16 15 14 14 13 13 12 12 11 11 10 10 10 9 9 8 8 7 7 6 6 6 6 5 5 4 4 3 3 2 2 1];
        v1 = -tt(dy);
    end
    
    % Offset y-coord based on:
    %  - FontSize, in pts
    %
    % Index is points, and must be an integer
    fsDy = [6 5 5 4 4 3 3 2 2 1 1  0  0 -1 -1 -2 -2 -2 -3 -3 -4 -4];
    v2 = fsDy(round(fs));
    
    offset(2) = offset(2) + v1 + v2;
end

h0 = offset(1);
v0 = offset(2);

end

function identical = setDecimalAlignedElement_intfrac( ...
    hint,hfrac,str,isEngUnits)
% Update one value within a decimal-aligned column.
%
% Split into int/frac string parts, based on location of first decimal
% found.  If no decimal, entire string is int.

str = strtrim(str);
idx = find(str=='.',1); % align at 1st decimal found
if isempty(idx)
    % No decimal point
    %
    if isEngUnits
        % This is an "engineering units" conversion.
        %
        % We might have a string like "3 mV" or "2GHz".  Without a decimal
        % point, we need to split the two parts at the space.  There could
        % be no space, or other spaces after the number is converted.  We
        % split at the first non-digit value we find after at least one
        % digit, starting from the left.  This helps us get past leading
        % blanks which some format specifiers can introduce.
        % Ex:  '  2 mV '
        %
        isdigit = isstrprop(str,'digit');
        idxd = find(isdigit,1); % first digit, achieves deblank
        if isempty(idxd)
            % no digits - degenerate case
            %
            % Copy ENTIRE string to "fraction" part, and clear the integer
            % part:
            identical = isempty(hint.String) && strcmp(hfrac.String,str);
            if ~identical
                hint.String = '';
                hfrac.String = str;
            end
            return % EARLY RETURN
        end
        % At least one digit found.
        % Now find first non-digit, if any present:
        idx = find(~isdigit(idxd+1:end),1);
        if isempty(idx)
            % No non-digits
            identical = isempty(hfrac.String) && strcmp(hint.String,str);
            if ~identical
                hint.String = str;
                hfrac.String = '';
            end
        else
            % Include first non-digit in FRACTION part, so integer parts
            % all line up flush-right.
            idx = idx + idxd; % string was offset by idxd
            ti = str(1:idx-1);
            tf = str(idx:end);
            identical = strcmp(ti,hint.String) && strcmp(tf,hfrac.String);
            if ~identical
                hint.String = ti;
                hfrac.String = tf;
            end
        end
    else
        identical = isempty(hfrac.String) && strcmp(hint.String,str);
        if ~identical
            hint.String = str;
            hfrac.String = '';
        end
    end
    
else
    % Decimal point present.
    %
    % Decimal stays with fraction part.
    %
    % Rationale: strings clip from the right, and we can lose the decimal
    % from view if it's at the right end of the integer string.  So we keep
    % it on the left of the fraction string.
    ti = str(1:idx-1);
    tf = str(idx:end);
    identical = strcmp(ti,hint.String) && strcmp(tf,hfrac.String);
    if ~identical
        hint.String = str(1:idx-1);
        hfrac.String = str(idx:end);
    end
end

end

% ============================================
% CONTROL ENABLE/VIS SUPPORT FUNCTIONS
% ============================================

function newValue = controlHitTest(uiHandle,uiValue,trueValue)
% Determines if uiHandle has a Value or a String equal to
% the specified uiValue.  uiValue may be a scalar or vector of
% values, or may be a string or cell vector of strings.
%
% When test is true, returns trueValue; otherwise returns
% ~trueValue.

switch uiHandle.Style
    case 'uitable'
        dialogmgr.untranslatedError([ ...
            'uitable not supported for testing value changes against a target value.\n' ...
            'Perform table value testing with custom code specified using a\n' ...
            'cell-vector with two function handles, or use uitable callbacks.']);
        
    case 'radiobutton'
        % Check UserData strings then String of "on" radio button.  Do
        % both, in this order, since String may have been translated and
        % not possible to test in code. Do UserData first, since if it's being
        % used, it should be the priority.
        %
        % Check set of UserData strings or Strings if radiobutton is part
        % of a radiobuttongroup.
        %
        % Otherwise check value as a simple test.
        rset = getappdata(uiHandle,'RadiobuttonGroup');
        if isempty(rset)
            % Single button (no group)
            
            keyboard % xxx
            %hitTest = ...
            %    strcmpi(uiValue,uiHandle.UserData) || ...
            %    strcmpi(uiValue,uiHandle.String);
            
        else
            % Have group of buttons
            % Execute regardless of being initialization call
            if ischar(uiValue) || (isstring(uiValue) && isscalar(uiValue))
                sel = [rset.Value]==rset(1).Max; % which one is "on"?
                if sum(sel) ~= 1
                    % All off or multiple on:
                    %  - buttons are in transition
                    %  - uiHandle denotes which radiobutton is "on"
                    hitTest = ...
                        strcmpi(uiValue,uiHandle.UserData) || ...
                        strcmpi(uiValue,uiHandle.String);
                else
                    % One radiobutton is "on"
                    %
                    % Test whether UserData strings or String of that
                    % radiobutton matches specified string:
                    hitTest = ...
                        strcmpi(uiValue,{rset(sel).UserData}) || ...
                        strcmpi(uiValue,{rset(sel).String});
                end
            else
                % Value passed, not string -> enumerates which radiobutton
                % is the one to test for being "on".  Find enumeration
                % index of uiHandle, then test it against Value for a
                % match.
                N = numel(rset);
                if uiValue<1 || uiValue>N
                    % Specified enum index is out of range.  Not an error.
                    hitTest = false;
                else
                    sel = [rset.Value]==rset(1).Max; % which one is "on"?
                    if sum(sel) ~= 1
                        % All off or multiple on:
                        %  - buttons are in transition
                        %  - uiHandle denotes which radiobutton is "on"
                        %
                        % With a confused state, don't test which is "on".
                        % Instead assume uiHandle is "on" (don't check!),
                        % and see if that's the enumerated test value.
                        hitTest = uiHandle == rset(uiValue);
                    else
                        % One radiobutton is "on"
                        %
                        % Test whether that radiobutton is the enumerated idx
                        % (which means its Value is Max).
                        theRadiobutton = rset(uiValue);
                        hitTest = theRadiobutton.Value==theRadiobutton.Max;
                    end
                end
            end
        end
        
    case {'checkbox','listbox','togglebutton','pushbutton','frame'}
        hitTest = any(uiHandle.Value==uiValue);
        
    case 'slider'
        % Use >= as the default test.
        %
        % Otherwise call the custom comparison test specified:
        %    y = fcn(value)
        % where y must return true (non-zero) or false.
        if isa(uiValue,'function_handle')
            hitTest = uiValue(uiHandle.Value);
        else
            hitTest = uiHandle.Value >= uiValue;
        end
        
    case {'edit','text'}
        v = strtrim(uiValue);
        ud = uiHandle.UserData;
        if isempty(ud)
            ud = ''; % ensure '' not [] for strtrim
        end
        hitTest = ...
            any(strcmpi(v,strtrim(ud))) || ...
            any(strcmpi(v,strtrim(uiHandle.String)));
        
    case {'popupmenu','spintext'}
        hv = uiHandle.Value;
        if ischar(uiValue) || iscellstr(uiValue) || isstring(uiValue)
            % Test UserData and String against a string uiValue.
            
            hitTest = false;
            t = uiHandle.UserData;
            if ischar(t) || (isstring(t) && isscalar(t))
                t = {t};
            end
            if (iscellstr(t) || isstring(t)) && hv<=numel(t)
                hitTest = any(strcmp(uiValue,t{hv}));
            end
            if ~hitTest
                hitTest = any(strcmp(uiValue,uiHandle.String{hv}));
            end
        else
            hitTest = isequal(hv,uiValue);
        end
        
    otherwise
        dialogmgr.untranslatedError('Unsupported uicontrol style ''%s''.',uiHandle.Style);
end

% It turns out:
%    dlg.(propName) = xor(~hitTest,trueValue)
% but that's not easily understood!
if hitTest
    newValue = trueValue;
else
    newValue = ~trueValue;
end

end

function idx = groupSameSourceHandles(rowVisArgs)
% Identify entries with same source handle.
%  - return an index vector, with one entry per element of
%    rowVisArgs, with indices into rowVisArgs that have the
%    same uiHandle
%  - returns references to all row entries, repeated or not; it's just that
%    indices are repeated where applicable
%  - uicontrols responsible for notifying us of a mouse
%    interaction are in rowVisArgs{i}{2}
%
% Returns empty if no handles are repeated.

idx = [];
N = numel(rowVisArgs);
if N>0
    src_uiHandle = cell(N,1);
    for i = 1:N
        src_uiHandle{i} = rowVisArgs{i}{2};
    end
    
%    [~,~,idx] = unique(src_uiHandle,'stable');
    idx = zeros(N,1);
    for i = 1:N
      for j = 1:i
        if isequal(src_uiHandle{i},src_uiHandle{j})
          idx(i) = j;
        end
      end
    end
            
end

end

function dlgActions = groupSameDlgHandles(actions)
% Create cell-matrix grouping idx/vis for each unique dialog handle
%
% actions: vector of structs
%    .uiValue
%    .DlgDst, vector of DialogContent
%    .RowIdx, vector of indices
%    .State, vector
%
% dlgActions:
%   cell-matrix grouping idx/vis for each unique dialog handle
%      {dlg1, s1;
%       dlg2, s2;
%       ... }
%   where
%       sN.rowIdx
%         .rowVis

% Flatten the struct of actions into a cell matrix
% Retain IDs separately for unique/sort
id = [];
t = {};
Na = numel(actions);
for i = 1:Na
    action_i = actions(i);
    dc_i = action_i.DlgDst; % could be a vector, DialogContent
    for j = 1:numel(dc_i)
        id(end+1,1) = dc_i(j).ID; %#ok<AGROW>
        t = [t; {dc_i(j), action_i.RowIdx(j), action_i.State(j)}]; %#ok<AGROW>
    end
end

% Find all rows pertaining to the same dialog
% Group all info for same dialog into support struct
uid = unique(id);  % Ex: [1 2 2 1 3 1]
Nuniq = numel(uid);
u = cell(Nuniq,2); % {dlg1, struct1; dlg2, struct2; ...}
for i = 1:Nuniq
    idx = find(id==uid(i)); % all rows pertaining to this dialog
    s.rowIdx = cat(1,t{idx,2});
    s.rowVis = cat(1,t{idx,3});
    u{i,1} = t{idx(1),1}; % any entry in idx will yield same dlg handle
    u{i,2} = s;
end
dlgActions = u;

end

% ============================================
% ROW/ELEMENT VISIBILITY
% ============================================

function [actions,changeList] = setRowVisibility_updateRRV(actions,isInit)
% Update RuntimeRowVisibility (RRV) vector.
% Influences behavior of changeDialogContentHeight.
%
% Removes entries in actions that are not changing visibility.
% The entries in actions may suggest making a control vis or
% invis, but if that control already has that visibility, we
% prune out the change.  This prevents flash, animation, or
% whatever may be specified for a vis change.
%
% actions: vector of structs
%   .RowIdx (vector)
%   .State (vector)
%   .DlgDst (vector of DialogContent)
%
% rowVis = [rowIdx1 visState1; rowIdx2 visState2; ...]

N_i = numel(actions);

if ~isInit
    % Only needed for dynamic (animation).  At isInit time, we do static
    % updates (no animation), so this is not needed.  Besides, it would
    % fail during init, as this would get called REPEATEDLY at init, messing
    % up _Prev.
    %
    % Cache RRV for all dialogs implicated in actions:
    for i = 1:N_i
        d_i = actions(i).DlgDst; % DialogContent
        N_j = numel(d_i);
        for j = 1:N_j
            d_i(j).RuntimeRowVisibility_Prev = ...
                d_i(j).RuntimeRowVisibility;
        end
    end
end

% Change RRV entries
% Remove action entries (idx/vis) that haven't changed state
% Remove actions if all their entries have been removed
del_action = false(N_i,1); % flag i'th action when all entries removed
changeList = {};
idList = [];
for i = 1:N_i
    a_i = actions(i);
    N_j = numel(a_i.DlgDst);
    del_entry = false(N_j,1);
    for j = 1:N_j
        rowIdx = a_i.RowIdx(j);
        rowVis = logical(a_i.State(j));
        dc_j = a_i.DlgDst(j);
        if xor(rowVis, dc_j.RuntimeRowVisibility(rowIdx))
            % the orig and new states differ
            %
            % why xor? we don't want to worry about logical vs
            % numeric discrepancies - just conceptual
            % comparison of "true" vs "false"
            dc_j.RuntimeRowVisibility(rowIdx) = rowVis;
            
            % Retain all DialogContent handles that get changed,
            % and the (unique) ID of each so we can de-dupe this list
            % later.
            changeList{end+1,1} = dc_j; %#ok<AGROW>
            idList(end+1,1) = dc_j.ID; %#ok<AGROW>
        else
            del_entry(j) = true;
        end
    end
    if all(del_entry)
        del_action(i) = true;
    elseif any(del_entry)
        actions(i).DlgDst(del_entry) = [];
        actions(i).RowIdx(del_entry) = [];
        actions(i).State(del_entry) = [];
    end
end
actions(del_action) = [];

% De-dupe the changed-DialogContent list.
%
% It could have multiple references to the same dialogContent, if multiple
% changes are being made to a dialog.
[~,ia] = unique(idList);
changeList = changeList(ia);

end

function setRowVisibility_UpdateControlsStatic(actions)
% Change visibility of controls using non-animated (static) changes.
%
% NOTE: RuntimeRowVis already updated

N_i = numel(actions);
for i = 1:N_i
    a_i = actions(i);
    N_j = numel(a_i.DlgDst);
    for j = 1:N_j
        % Set visibility of all Controls in this row.
        if a_i.State(j)
            st = 'on';
        else
            st = 'off';
        end
        rowIdx = a_i.RowIdx(j);
        dc = a_i.DlgDst(j);
        
        % Skip GraphicsPlaceholder controls - they don't have Visible.
        %
        % Also, RowVis and ElementVis interact:
        % - ElementVis=false: keep that element invis
        % - ElementVis=true: use RowVis
        %
        rev = dc.RuntimeElementVisibility(rowIdx,:);
        nph = dc.NotPlaceholderControl(rowIdx,:);
        for k = 1:size(nph,2) % same as #cols in Controls matrix
            if nph(k) && rev(k)
                dc.Controls{rowIdx,k}.Visible = st;
            end
        end
    end
end

end

function setRowVisibility_static(actions,isInit,dc_changed)
% Adjust visibility of rows in layout
%
% Makes instant changes to row visibility.
% A bit jarring, but quick and easy to do.

setRowVisibility_UpdateControlsStatic(actions);
if ~isInit
    % When isInit, the top-level caller will update height of every dialog,
    % so this would be redundant and waste time.
    for i = 1:numel(dc_changed)
        changeDialogContentHeight(dc_changed{i});
    end
end

end

function setRowVisibility_dynamic(actions,isInit) %#ok<DEFNU>
% Adjust visibility of rows in layout
%
% Makes changes in steps:
%   invis: slide off-screen, make invis, then close up space
%     vis: add space, make vis off-screen, slide left
% Not as jarring.

if isInit
    % No sliding action at init time
    %
    % During init, we have no need to animate visibility
    % changes since the user/mouse didn't initiate the vis
    % change - no jarring to contend with, and dialog is
    % invisible as well.
    %
    % Also, if done here, animation will also slow down initial
    % opening of the dialog.
    setRowVisibility_UpdateControlsStatic(actions);
else
    % Create cell-matrix grouping idx/vis for each unique dialog handle
    %   {dlg1, s1;
    %    dlg2, s2;
    %    ... }
    %    where sN is a struct with fields:
    %          .rowIdx
    %          .rowVis
    dlgActions = groupSameDlgHandles(actions);
    
    % A mix of rows becoming vis and invis, with "shared rows" where
    % rows going vis and rows going invis are animated on the same
    % visible row.
    
    % Each set of changes in a given dialog may be purely vis or invis;
    % that simplifies the animation plan.  Or it can be a combination of
    % vis and invis within a single dialog; that's more involved for
    % row-height placements, shared-row animation, etc.
    %
    % Create a plan for each dialog undergoing a change in visibility:
    plans = createAnimationPlans(dlgActions);
    
    % Execute these plans:
    changeDialogContentHeight_plan(dlgActions,plans,'pre');
    setRowVisibility_UpdateControlsDynamic(dlgActions,true);
    changeDialogContentHeight_plan(dlgActions,plans,'post');
end

end

function setRowVisibility_UpdateControlsDynamic(dlgActions,anySharedRows)
% Change visibility of control rows using animation.

if nargin<3
    anySharedRows = false;
end

% Each part is dialogContent-specific:
%
[ctx,dlgActions] = setRowVisibility_ucd_init(dlgActions,anySharedRows);
setRowVisibility_ucd_slide(ctx,dlgActions);
setRowVisibility_ucd_final(ctx,dlgActions);

end

function [ctx,dlgActions] = setRowVisibility_ucd_init(dlgActions,anySharedRows)
% Initialize vis/invis row animation
%
% Part of: setRowVisibility_UpdateControlsDynamic()

% Make invis:
%   record orig_pos, leave vis in current pos
%   animate: slide right
%   make invis, restore to orig pos
% Make vis:
%   reposition off-screen to right, make vis
%   animate: slide left
%   leave vis in final pos
%
% dlgActions: cell-matrix grouping idx/vis for each unique dialog handle
%   {dlg1, s1;
%    dlg2, s2;
%    ... }
%    where s# is a struct with initial fields
%       .rowIdx
%       .rowVis
%    and we "grow" each struct with more fields that are used for later
%    stages of the animation process.

% Global parameters for animation:
%
ctx.anySharedRows = anySharedRows; % true if vis/invis controls can share a row
ctx.Tslide = 0.3; % total time to perform slide
ctx.fps    = 40;  % animation updates per second
ctx.Nsteps = ceil(ctx.Tslide * ctx.fps);

Nk = size(dlgActions,1); % #rows = #dlgs
for k = 1:Nk
    % Get all vis-change (animation) info pertaining to this dialog
    dc = dlgActions{k,1}; % DialogContent
    s = dlgActions{k,2};
    
    % could be a standalone undocked dialog implicated in set
    [s.slideVis,s.slideInvis] = getAnimationSlideDir(dc.DialogPresenter);
    
    % Determine slide step-size
    %
    % rowVis: [idxColVec stateColVec], new vis state
    W = getOuterWidth(dc.DialogBorder); % #pixels in this dlg width
    dw = max(1,ceil(W/ctx.Nsteps)); % integer #pixels per anim-step
    s.dw = dw;
    incr = s.slideVis * dw * ctx.Nsteps; % exact total shift
    %s.OrigChildOrder = []; % in case we don't fill it in
    
    % RowVis and ElementVis interact:
    % - ElementVis=false: keep that element invis
    % - ElementVis=true: use RowVis
    %
    rev = dc.RuntimeElementVisibility;
    nph = dc.NotPlaceholderControl;

    c = dc.Controls;
    Nc = size(c,2);
    
    rowIdx = s.rowIdx;
    rowVis = s.rowVis;
    Nvis = numel(rowVis); % #rows to CHANGE vis (to vis or to invis)
    
    % Identify all rows changing in this dialog
    if any(rowVis)
        % At least one row is becoming visible
        %
        % Prepare for changing child stacking order
        % - no need to change stacking order for controls that are
        %   already visible and are becoming invisible
        % - no need to change stacking order for controls that
        %   aren't "crossing", where one is becoming invis and the
        %   other vis on the same row
        % - so it's just controls that are becoming vis,
        %   paired with controls becoming invis on same row
        %
        %   xxx We don't pass this info to this method, so we'll
        %   "overshoot" and restack all controls becoming vis.  We
        %   could optimize this, possibly improving performance.
        
        if anySharedRows
            % Same steps to move controls as ~anySharedRows, but a few
            % extra steps to reorder child handles when shared-row
            % animation is enabled.
            
            % Identify all graphical children of parent for reordering
            ch = dc.ContentPanel.Children;
            %s.OrigChildOrder = ch;
            chOnTop = false(size(ch));
            
            % Initial step
            %  make vis:
            %     - set pos off-screen to "invis" side,
            %     - set children/stacking order
            %     - scroll to make vis later
            %  make invis:
            %     - leave as-is (do nothing)
            for i = 1:Nvis
                if rowVis(i) % make vis?
                    
                    % Set pos off-screen to invis side
                    row_i = rowIdx(i);
                    for j = 1:Nc
                        if nph(row_i,j)
                            c_ij = c{row_i,j};
                            
                            % Identify control to move to top of stack
                            if isa(c_ij,'dialogmgr.uidialog')
                                t = c_ij.Dialog.DialogBorder.Panel;
                            else
                                t = c_ij;
                            end
                            chOnTop(ch==t) = true;
                            
                            % Adjust pos and vis of c_ij
                            c_ij.Position(1) = c_ij.Position(1) - incr;
                            
                            % Only make visible if runtimeEleVis=true
                            if rev(row_i,j)
                                c_ij.Visible = 'on';
                            end
                        end
                    end
                end
            end
            
            % Restack children so controls vis are "on top".
            % This changes current "focus", so best if we do this only
            % when needed.
            if any(chOnTop)
                dc.ContentPanel.Children = [ch(chOnTop);ch(~chOnTop)];
            end
        else
            % Subset of steps from "anySharedRows" condition above, where
            % child-order is omitted since shared-row animation is
            % disabled.  See code above for comments.
            for i = 1:Nvis
                if rowVis(i)
                    row_i = rowIdx(i);
                    for j = 1:Nc
                        if nph(row_i,j)
                            c_ij = c{row_i,j};
                            c_ij.Position(1) = c_ij.Position(1) - incr;
                            
                            if rev(row_i,j)
                                c_ij.Visible = 'on';
                            end
                        end
                    end
                end
            end
        end
    end
    
    % Copy struct updates back into dlgActions
    dlgActions{k,2} = s;
end

% Show the controls - which may be off-screen, or just a few
% pix onscreen, but only if any initial pos/vis changes
% were made Change stacking order
drawnow('expose');

end

function setRowVisibility_ucd_slide(ctx,dlgActions)
% Concurrently animate the vis/invis of control rows in affected dialogs.
%
% Part of: setRowVisibility_updateControls_dynamic()

% #rows = #dialogs being animated
Nk = size(dlgActions,1);

% Compute Tstep:
%  - time to delay after each animation step
%  - can only use one delay after ALL dialogs animate one step
%  - each unique dialog wants its own Tstep, however, to maintain the
%    same overall time to complete the animation
%    in the unusual case where some dialogs are undocked and set to a
%    different pixel-width, such that the per-pixel anim rate differs
%  - to minimize the delay, we wait the SHORTEST time so the
%    WORST case delay is Tslide; some dialogs may therefore need to
%    animate faster than specified
Tstep = 1.0 / ctx.fps; % approx time delay per step
Nsteps = ctx.Nsteps;

% Perform one animation step across all dialogs that have a vis-change.
% Loop over anim steps.
t     = 1;
tIncr = 1;
t0    = tic;
while 1
    for k = 1:Nk
        % Get state info for next dialog
        dc         = dlgActions{k,1}; % DialogContent
        s          = dlgActions{k,2};
        rowIdx     = s.rowIdx;
        rowVis     = s.rowVis;
        c          = dc.Controls;
        Nc         = size(c,2); % #cols in dialog layout
        stepVis    = tIncr * s.dw * s.slideVis;
        stepInvis  = tIncr * s.dw * s.slideInvis;
        notPlaceholder = dc.NotPlaceholderControl;
        
        % Move controls "one step" for this dialog
        for i = 1:numel(rowIdx)
            % Move all controls in row
            if rowVis(i)
                step = stepVis;
            else
                step = stepInvis;
            end
            row_i = rowIdx(i);
            for j = 1:Nc
                if notPlaceholder(row_i,j)
                    c_ij = c{row_i,j};
                    c_ij.Position(1) = c_ij.Position(1) + step;
                end
            end
        end % loop over controls in one dialog
    end % loop over all dialogs
    
    drawnow('expose'); % show updated control positions
    
    % Delay after ALL rows in ALL dialogs have updated one step
    if t < Nsteps
        wasAnyTimeLeft = false;
        Telap = t*Tstep; % target time (elapsed) for this anim step
        while toc(t0) < Telap
            wasAnyTimeLeft = true;
        end
        if wasAnyTimeLeft
            tIncr = max(1,tIncr-1); % slow down
        else
            % We fell far enough behind that time expired
            % before we finished this iteration.
            % Accelerate time:
            tIncr = min(Nsteps,tIncr+2); % speed up
            if t+tIncr > Nsteps
                tIncr = Nsteps-t; % always hit t=Nsteps
            end
        end
        t = t+tIncr;
    else
        break
    end
    % debug total time for slide:
    %fprintf(' - total slide time: %f desired, %f actual\n',Tslide,toc(t0_overall));
end

end

function setRowVisibility_ucd_final(ctx,dlgActions)
% Final step
%
% Part of: setRowVisibility_updateControls_dynamic()

%  make vis: leave as-is (do nothing)
%  make invis: set invis, restore to orig pos

Nk = size(dlgActions,1); % #rows = #dlgs
for k = 1:Nk
    dc = dlgActions{k,1}; % DialogContent
    s = dlgActions{k,2};
    
    % We are making elements invisible here, so we can disregard
    % RuntimeElementVisibility since that would only be needed to safeguard
    % against accidental "vis" changes, not "invis".
    
    notPlaceholder = dc.NotPlaceholderControl;
    c = dc.Controls;
    Nc = size(c,2);
    rowVis = s.rowVis;
    rowIdx = s.rowIdx;
    Nvis = numel(rowVis); % #rows to CHANGE vis (to vis or to invis)
    step = s.dw * ctx.Nsteps * s.slideInvis;
    
    for i = 1:Nvis
        if ~rowVis(i) % make invis?
            row_i = rowIdx(i);
            for j = 1:Nc
                if notPlaceholder(row_i,j)
                    c_ij = c{row_i,j};
                    c_ij.Visible = 'off';
                    c_ij.Position(1) = c_ij.Position(1) - step;
                end
            end
        end
    end
    
    %{
    % Restore stacking order
    % Enables last-selected control to remain selected
    if any(rowVis)
        dc.ContentPanel.Children = s.OrigChildOrder;
    end
    %}
end
drawnow('expose');

end

function plans = createAnimationPlans(dlgActions)
% Create a plan for each dialog that is changing the visibility of rows,
% where at least one row is going vis AND one row is going invis.
%
% If not for that complication (vis+invis), we could use simpler special
% case code.

Nk = size(dlgActions,1); % #rows = #dlgs
plans = cell(Nk,1);
for k = 1:Nk
    dc = dlgActions{k,1}; % DialogContent
    
    % Get current and new row visibilities
    rrv_orig = dc.RuntimeRowVisibility_Prev;
    rrv_new  = dc.RuntimeRowVisibility;
    
    % Identify situation:
    delta = rrv_new-rrv_orig;
    if ~any(delta==1)
        % Controls only going invis (no 0->1 vis transitions)
        % Make controls disappear, then shrink vertical.
        plan_k.pre = false;  % don't resize during pre-phase
        plan_k.post = true;  % resize during post-phase
        
    elseif ~any(delta==-1)
        % Controls only going vis (no 1->0 vis transitions)
        % Expand vertical first, then make controls appear.
        plan_k.pre = true;
        plan_k.post = false;
        
    else
        % A mix of vis and invis actions
        rrv = [rrv_orig rrv_new];
        rowSharing_k = createAnimationPlan_rowGroups_fast(dc,rrv);
        plan_k = createAnimationPlan_rowHeights(dc,rrv,rowSharing_k);
    end
    plans{k} = plan_k;
end

end

function changeDialogContentHeight_plan(dlgActions,plans,opt)
% Change dialog row heights to prepare for row visibility
% animation based on a plan.
%
% Performs similar steps to changeDialogContentHeight, but not
% updateTableLayout, and we change RuntimeRowVisibility several
% times.

isPre = strcmpi(opt,'pre'); % 'pre' or 'post' plan step
Nk = size(dlgActions,1); % #rows = #dlgs
for k = 1:Nk
    dc = dlgActions{k,1}; % DialogContent
    if isPre
        plan_k = plans{k}.pre;
    else
        plan_k = plans{k}.post;
    end
    
    if islogical(plan_k)
        % Simple case: all going vis or all going invis in this dialog.
        %
        % A single logical flag is used as the "plan" for
        % this case, where true means to resize the dialog, and false means
        % to skip that.  Thus the resize can occur either on the .pre or on
        % the .post phase --- which is all that is needed for this case.
        %
        % Invoke change on (dc), not on (dc.DialogBorder) as we do for the
        % complex case.
        if plan_k
            changeDialogContentHeight(dc);
        end
    else
        % plan_k is a struct
        % complex case: mix of rows going vis and invis
        
        % Alt 1: more work
        % [origin,spans,ymax] = computeTableLayout_height(dc);
        
        % Alt 2: higher performance
        origin = dc.LastLayoutOrigin;
        origin.y = plan_k.y;
        spans = dc.LastLayoutSpans;
        spans.dy = plan_k.dy;
        
        % Need to temporarily change rrv so methods compute the
        % layouts we need:
        rrv_orig = dc.RuntimeRowVisibility;
        
        dc.RuntimeRowVisibility = plan_k.rrv;
        updateControlPositions(dc,origin,spans);
        
        dc.RuntimeRowVisibility = plan_k.rrv_suppressSharedRows;
        changeDialogContentHeight(dc.DialogBorder,plan_k.ymax);
        
        dc.RuntimeRowVisibility = rrv_orig;
    end
end

end

function updateRowVisibility_Action(uiHandle,actions,isInit)
% Hide or show table row, based on uiHandle having a value or
% string equal to target values.

% Test which conditions hold, and therefore, which actions to
% queue up concurrently
N = numel(actions);
if N > 0
    for i = 1:N
        % Determine if rows should be vis or invis
        %  - Pass "vis state based on uicontrol value"
        %       (initial .State value)
        %  - Get "vis state to install"
        %       (updated .State value)
        actions(i).State = controlHitTest(uiHandle,actions(i).uiValue,actions(i).State);
    end
    [actions,dc_changed] = setRowVisibility_updateRRV(actions,isInit);
    if ~isempty(actions)
        % Get animation state by querying any DialogContent
        % implicated in the action list.  They should all be
        % the same.
        %
        % Get the first - it should be there:
%         isDynamic = actions(1).DlgDst(1).Animation;
%         if isDynamic
%             setRowVisibility_dynamic(actions,isInit);
%         else
%             setRowVisibility_static(actions,isInit,dc_changed);
%         end
%       ADDED
        %No matter what no animation for visibility for now
        setRowVisibility_static(actions,isInit,dc_changed);
%         if ~isInit
%             notify(uiHandle,'Action');
%         end

    end
end

end

function updateElementVis_Action(uiHandle,actions)
% Hide or show table element, based on uiHandle having a value
% or string equal to target values.
%
% NOTE: Changing element visibility will NOT cause a row to disappear, even
% if each element of the row is made invisible.  If row removal is
% required, you must use RowVis control, not EleVis.

% Test which conditions hold, and therefore, which actions to
% queue up
N = numel(actions);
if N > 0
    for i = 1:N
        a_i = actions(i);
        
        % Determine if elements should be visible or not
        %  - Pass "vis state based on uicontrol value"
        %       (initial .State value)
        %  - Get "vis state to install"
        %       (updated .State value)
        visTF = controlHitTest(uiHandle,a_i.uiValue,a_i.State);
        if visTF
            vis = 'on';
        else
            vis = 'off';
        end
        
        N_j = numel(a_i.RowIdx);  % or numel(a_i.ColIdx), etc
        for j = 1:N_j
            % Set vis of Controls element
            rowIdx = a_i.RowIdx(j);
            colIdx = a_i.ColIdx(j);
          	visible = logical(a_i.State(j));

            dc = a_i.DlgDst(j);
            
            % Skip GraphicsPlaceholder controls - they don't have Visible
            nph = dc.NotPlaceholderControl;
            if nph(rowIdx,colIdx)
                % Record REV:
                dc.RuntimeElementVisibility(rowIdx,colIdx) = visible;
                
                % RowVis and ElementVis interact:
                % - RowVis=off: Ignore ElementVis
                % - RowVis=on: Use ElementVis
                %
                if dc.RuntimeRowVisibility(rowIdx)
                    dc.Controls{rowIdx,colIdx}.Visible = vis;
                end
            end
        end
    end
end

end

% ============================================
% ROW/ELEMENT ENABLE
% ============================================

function updateElementEnable_Action(uiHandle,actions)
% Enable or disable table element (rowIdx,colIdx) based on
% uiHandle having a Value or a String equal to the specified
% uiValue.

% Test which conditions hold, and therefore, which actions to
% queue up
N = numel(actions);
if N > 0
    for i = 1:N
        a_i = actions(i);
        
        % Determine if elements should be enabled or not
        %  - Pass "enable state based on uicontrol value"
        %       (initial .State value)
        %  - Get "enable state to install"
        %       (updated .State value)
        if controlHitTest(uiHandle,a_i.uiValue,a_i.State)
            ena = 'on';
        else
            ena = 'off';
        end
        
        N_j = numel(a_i.RowIdx);  % or numel(a_i.ColIdx), etc
        for j = 1:N_j
            % Set enable of Controls element
            rowIdx = a_i.RowIdx(j);
            colIdx = a_i.ColIdx(j);
            dc = a_i.DlgDst(j);
            
            % Skip GraphicsPlaceholder controls - they don't have Enable.
            nph = dc.NotPlaceholderControl;
            if nph(rowIdx,colIdx)
                dc.Controls{rowIdx,colIdx}.Enable = ena;
            end
        end
    end
end

end

function updateRowEnable_Action(uiHandle,actions)
% Enable or disable table row element based on uiHandle having a Value or a
% String equal to the specified uiValue.

% Test which conditions hold, and therefore, which actions to
% queue up
N = numel(actions);
if N > 0
    for i = 1:N
        a_i = actions(i);
        
        % Determine if rows should be enabled or not
        %  - Pass "enable state based on uicontrol value"
        %       (initial .State value)
        %  - Get "enable state to install"
        %       (updated .State value)
        if controlHitTest(uiHandle,a_i.uiValue,a_i.State)
            ena = 'on';
        else
            ena = 'off';
        end
        
        N_j = numel(a_i.RowIdx);  % or numel(a_i.ColIdx), etc
        for j = 1:N_j
            % Set enable of Controls row
            rowIdx = a_i.RowIdx(j);
            dc = a_i.DlgDst(j);
            
            % Skip GraphicsPlaceholder controls - they don't have Enable.
            nph = dc.NotPlaceholderControl;
            for k = 1:size(nph,2) % same as #cols in Controls matrix
                if nph(rowIdx,k)
                    dc.Controls(rowIdx,k).Enable = ena;
                end
            end
        end
    end
end

end

% ============================================
% DEBUGGING FUNCTIONS
% ============================================

function y = hasActionWithTargetDialogTag(actions,tags) %#ok<DEFNU>
% True if the Dialog object in the .DlgDst field of any action in the
% vector of action structs has a tag matching the specified tags string.
%
% Tags argument may be a string or a cell-string.

y = false;
for i = 1:numel(actions)
    dlgs = actions(i).DlgDst;
    for j = 1:numel(dlgs)
        if any(strcmpi(dlgs(j).Tag,tags))
            y = true;
            return
        end
    end
end

end

function showAction(argVec) %#ok<DEFNU>
% DEBUG TOOL: Display of action struct in editor
%  action: vector of structs
%      .uiHandle
%      .actions - vector of structs
%         .uiValue
%         .DlgDst, vector
%         .RowIdx, vector of indices
%         .State, vector
%         .ActionType, string

if isfield(argVec,'uiHandle')
    % Has .uiHandle and .actions
    if ~isfield(argVec,'actions')
        dialogmgr.untranslatedError('Has .uiHandle but not .actions field: invalid struct');
    end
    s = showAction_FullInfo(argVec);
else
    % Just content of the .actions struct, no top-level .actions field
    s = showAction_ActionsInfo(argVec);
end
edit(s);

end

function s = showAction_FullInfo(argVec)
% Full RowVisStruct, with .uiHandle and .actions substruct

s = StringWriter;
[tR,tC] = size(argVec);
P = tR*tC;
if P>1
    s.addcr('RowVisStruct: %dx%d',tR,tC);
end
for k = 1:P
    if P>1
        s.addcr('RowVisStruct(%d):',k);
    end
    arg = argVec(k);
    
    % Top-level struct with:
    %   .uiHandle
    %   .actions
    uiHandle = arg.uiHandle;
    s.addcr('  .uiHandle (%s)',showAction_uiHandleInfo(uiHandle));
    
    s2 = showAction_ActionsInfo(arg.actions);
    s = [s s2]; %#ok<AGROW>
end
edit(s)

end

function s = showAction_ActionsInfo(arg)
% Inner part of RowVisStruct, just the .actions sub-struct
%      .actions - vector of structs
%         .uiValue
%         .DlgDst, vector
%         .RowIdx, vector of indices
%         .State, vector
%         .ActionType

s = StringWriter;
[R,C] = size(arg);
P=R*C;
s.addcr('  .actions: %dx%d struct',R,C);
for i = 1:P
    a_i = arg(i);
    if P>1
        s.addcr('      actions(%d):',i);
    end
    
    t = a_i.uiValue;
    if iscellstr(t) || isstring(t)
        t = uiservices.cellToPipe(t);
    end
    s.addcr('          .ActionType = %s',a_i.ActionType);
    s.addcr('          .uiValue    = %s',mat2str(t));
    s.addcr('          .RowIdx     = %s',mat2str(a_i.RowIdx));
    s.addcr('          .ColIdx     = %s',mat2str(a_i.ColIdx));
    s.addcr('          .State      = %s',mat2str(a_i.State));
    Nd = numel(a_i.DlgDst);
    if Nd==1
        s.addcr('          .DlgDst     = #%d (''%s'')',a_i.DlgDst.ID,a_i.DlgDst.Name);
    else
        for j = 1:Nd
            s.addcr('          .DlgDst(%d)  = #%d (''%s'')',j,a_i.DlgDst(j).ID,a_i.DlgDst(j).Name);
        end
    end
end

end

function s = showAction_uiHandleInfo(uiHandle)
% Provide a useful string describing the uiHandle
% - Style:---
% - Tag:--- if nonempty
% - String:--- if nonempty

s = ['Style=''' uiHandle.Style ''''];

t = uiHandle.Tag;
if ~isempty(t)
    s = [s ', Tag=''' t ''''];
end

t = uiHandle.String;
if ~isempty(t)
    if iscellstr(t) || isstring(t)
        t = uiservices.cellToPipe(t);
    end
    s = [s ', String=''' t ''''];
end

end

% [EOF]
 
