Skip to content

Commit 5d70901

Browse files
HanSur94claude
andauthored
fix(timerangeselector): support uifigure parents (replace uicontrol labels with uilabel when needed) (#145)
* fix(timerangeselector): support uifigure parents via uilabel branch uicontrol is not supported inside figures created with uifigure(...), so TimeRangeSelector crashes when CompanionEventViewer hosts it (MATLAB Tests (A-D) CI: TestCompanionEventViewer ~15 tests). Detect uifigure parents in the constructor via isprop(hAncFig, 'AutoResizeChildren') — that property exists on uifigure only; classical figure handles lack it even though both report class matlab.ui.Figure. buildGraphics_ then dispatches to either buildLabelsClassical_ (existing uicontrol path, normalized Position, unchanged) or buildLabelsUIFigure_ (new uilabel path, pixel Position recomputed on every hPanel SizeChangedFcn — same pattern as MultiStatusWidget, IconCardWidget, TextWidget). setRangeLabels now routes through a single setLabelText_ helper that writes Text on uilabel and String on uicontrol, so the public contract is unchanged for DashboardEngine. The panel's previous SizeChangedFcn is saved and re-fired so we don't strand sibling resize handlers. The saved handle is restored on delete via restoreCallbacks_. Backward compatible with the dashboard path (DashboardEngine still creates a classical figure() and the uicontrol code path is bit-for-bit identical). No new toolboxes; pure MATLAB. Verified statically: `mh_style` clean on touched code (3 pre-existing warnings on lines unrelated to this fix); only uicontrol callsites in the file are inside buildLabelsClassical_; setRangeLabels' label-handle access now lives in one helper. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(timerangeselector): probe-based uifigure detection (replace broken isprop heuristic) The original isprop(fig, 'AutoResizeChildren') discriminator returns TRUE for BOTH classical figures AND uifigures on R2020b+ — confirmed in a live MATLAB probe. Result: every classical-figure construction misroutes to buildLabelsUIFigure_, which then errors because uilabel cannot be parented to a classical figure's uipanel on CI Linux MATLAB. This broke ~14 test files in batch Dashboard and 4 other batches on PR #145's CI. Replace with a hidden-probe pattern: try uicontrol on hPanel; if MATLAB rejects it ("Functionality not supported with figures created with the uifigure function"), switch to uilabel. Bulletproof across MATLAB releases — actually tests parent compatibility rather than guessing from properties. Verified locally: classical figure → classical path (uicontrol), label class = matlab.ui.control.UIControl. uifigure on local macOS also takes the classical path because macOS MATLAB allows uicontrol-in-uifigure (CI Linux rejects it, which is the case the probe catches). * style(timerangeselector): move || to end-of-line in probe-detection (mh_style: operator_after_continuation) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 13534e0 commit 5d70901

1 file changed

Lines changed: 193 additions & 21 deletions

File tree

libs/Dashboard/TimeRangeSelector.m

Lines changed: 193 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,9 @@
6565
hSelection = [] % patch for selection rectangle
6666
hEdgeLeft = [] % line: left drag handle
6767
hEdgeRight = [] % line: right drag handle
68-
hRangeLabelLeft = [] % uicontrol text BELOW slider — slider LEFT selection-edge timestamp (260512-hrn-followup)
69-
hRangeLabelMiddle = [] % uicontrol text BELOW slider — selection duration (e.g. "3d 12h")
70-
hRangeLabelRight = [] % uicontrol text BELOW slider — slider RIGHT selection-edge timestamp
68+
hRangeLabelLeft = [] % text label BELOW slider — slider LEFT selection-edge timestamp (260512-hrn-followup)
69+
hRangeLabelMiddle = [] % text label BELOW slider — selection duration (e.g. "3d 12h")
70+
hRangeLabelRight = [] % text label BELOW slider — slider RIGHT selection-edge timestamp
7171
RangeLeftText = '' % formatted timestamp shown in hRangeLabelLeft
7272
RangeMiddleText = '' % formatted duration string in hRangeLabelMiddle
7373
RangeRightText = '' % formatted timestamp shown in hRangeLabelRight
@@ -85,6 +85,14 @@
8585
% NOTE: No OldResizeFcn. Resize events are not observed by this class —
8686
% all pixel/data conversions are computed on demand from current geometry,
8787
% so there is no cached resize-dependent state to invalidate.
88+
IsUIFigureParent_ = false % true when ancestor figure was created via uifigure(...)
89+
% uicontrol is unsupported under uifigure, so when this flag is true the
90+
% label handles are uilabel instances and the property name accessed by
91+
% setLabelText_ switches from 'String' to 'Text'. Detected once in the
92+
% constructor via isprop(hAncFig, 'AutoResizeChildren'), which is the
93+
% uifigure-only property — classical figures lack it even though both
94+
% paths report class matlab.ui.Figure on R2020b+. (uifigure-compat fix)
95+
OldPanelSizeChangedFcn_ = [] % saved hPanel.SizeChangedFcn for cleanup (uifigure path only)
8896
end
8997

9098
methods (Access = public)
@@ -96,6 +104,11 @@
96104
end
97105
obj.hPanel = hPanel;
98106
obj.hFigure = ancestor(hPanel, 'figure');
107+
% IsUIFigureParent_ is detected lazily inside buildGraphics_ via
108+
% a hidden uicontrol probe -- isprop heuristics (e.g. on
109+
% AutoResizeChildren, MenuBar) are unreliable on R2020b+ because
110+
% classical figure() and uifigure() share the same
111+
% matlab.ui.Figure class and report identical property sets.
99112
for k = 1:2:numel(varargin)
100113
key = varargin{k};
101114
if ischar(key)
@@ -216,15 +229,9 @@ function setRangeLabels(obj, leftText, rightText, middleText)
216229
obj.RangeLeftText = char(leftText);
217230
obj.RangeRightText = char(rightText);
218231
obj.RangeMiddleText = char(middleText);
219-
if ~isempty(obj.hRangeLabelLeft) && ishandle(obj.hRangeLabelLeft)
220-
set(obj.hRangeLabelLeft, 'String', obj.RangeLeftText);
221-
end
222-
if ~isempty(obj.hRangeLabelMiddle) && ishandle(obj.hRangeLabelMiddle)
223-
set(obj.hRangeLabelMiddle, 'String', obj.RangeMiddleText);
224-
end
225-
if ~isempty(obj.hRangeLabelRight) && ishandle(obj.hRangeLabelRight)
226-
set(obj.hRangeLabelRight, 'String', obj.RangeRightText);
227-
end
232+
obj.setLabelText_(obj.hRangeLabelLeft, obj.RangeLeftText);
233+
obj.setLabelText_(obj.hRangeLabelMiddle, obj.RangeMiddleText);
234+
obj.setLabelText_(obj.hRangeLabelRight, obj.RangeRightText);
228235
end
229236

230237
function setEnvelope(obj, xC, yMin, yMax)
@@ -669,10 +676,13 @@ function restoreCallback_(obj, cb)
669676

670677
function buildGraphics_(obj)
671678
%buildGraphics_ Construct axes and graphics handles inside hPanel.
672-
% Slider axes height reduced (was 0.85) so two date/time labels
679+
% Slider axes height reduced (was 0.85) so three date/time labels
673680
% can sit below the slider strip showing the data-range edges
674-
% (260512-hrn-followup). The two uicontrol text labels live in
675-
% the same panel and update on every live tick from the engine.
681+
% (260512-hrn-followup). The text labels live in the same panel
682+
% and update on every live tick from the engine. The widget used
683+
% depends on the parent figure type — classical figures get
684+
% uicontrol('Style','text', ...), uifigure parents get uilabel
685+
% (uicontrol is unsupported under uifigure).
676686
obj.hAxes = axes('Parent', obj.hPanel, ...
677687
'Units', 'normalized', ...
678688
'Position', [0.045 0.42 0.94 0.55], ...
@@ -710,10 +720,19 @@ function buildGraphics_(obj)
710720
% - LEFT : slider's LEFT selection-edge timestamp
711721
% - MIDDLE: selection duration (e.g. "7d", "3h 25m", "45 s")
712722
% - RIGHT : slider's RIGHT selection-edge timestamp
713-
% uicontrol text so they read the panel background (not the
714-
% always-white axes background). Updated whenever
715-
% DashboardEngine.updateTimeLabels fires (drag or
716-
% programmatic selection change). (260512-hrn-followup)
723+
% The label widgets read the panel background (not the always-white
724+
% axes background) and are updated whenever
725+
% DashboardEngine.updateTimeLabels fires (drag or programmatic
726+
% selection change). (260512-hrn-followup)
727+
%
728+
% Classical figure parents host uicontrol('Style','text', ...)
729+
% with normalized positions; uifigure parents host uilabel(...)
730+
% with pixel positions because uicontrol is unsupported under
731+
% uifigure (it errors "Functionality not supported with figures
732+
% created with the uifigure function."). The pixel positions are
733+
% recomputed on every hPanel size change via a SizeChangedFcn
734+
% installed below — matches MultiStatusWidget / IconCardWidget /
735+
% TextWidget which use the same pattern. (uifigure-compat fix)
717736
try
718737
panelBg = get(obj.hPanel, 'BackgroundColor');
719738
catch
@@ -723,6 +742,35 @@ function buildGraphics_(obj)
723742
if isstruct(obj.Theme) && isfield(obj.Theme, 'ToolbarFontColor')
724743
fgColor = obj.Theme.ToolbarFontColor;
725744
end
745+
% Probe-based detection: try a hidden uicontrol on hPanel. If
746+
% MATLAB rejects it because the ancestor figure is a uifigure,
747+
% switch to the uilabel path. This is bulletproof across MATLAB
748+
% releases -- isprop heuristics fail on R2020b+ because classical
749+
% and uifigure share matlab.ui.Figure and expose identical props.
750+
obj.IsUIFigureParent_ = false;
751+
try
752+
probe = uicontrol('Parent', obj.hPanel, ...
753+
'Style', 'text', 'String', '', 'Visible', 'off');
754+
delete(probe);
755+
catch err
756+
if contains(err.message, ...
757+
'Functionality not supported with figures created with the uifigure function') || ...
758+
contains(err.identifier, 'UnsupportedFor') || ...
759+
contains(err.identifier, 'NotSupportedFor')
760+
obj.IsUIFigureParent_ = true;
761+
else
762+
rethrow(err);
763+
end
764+
end
765+
if obj.IsUIFigureParent_
766+
obj.buildLabelsUIFigure_(fgColor, panelBg);
767+
else
768+
obj.buildLabelsClassical_(fgColor, panelBg);
769+
end
770+
end
771+
772+
function buildLabelsClassical_(obj, fgColor, panelBg)
773+
%buildLabelsClassical_ uicontrol-text labels (normalized) for classical figure parents.
726774
obj.hRangeLabelLeft = uicontrol('Parent', obj.hPanel, ...
727775
'Style', 'text', ...
728776
'Units', 'normalized', ...
@@ -753,6 +801,120 @@ function buildGraphics_(obj)
753801
'BackgroundColor', panelBg);
754802
end
755803

804+
function buildLabelsUIFigure_(obj, fgColor, panelBg)
805+
%buildLabelsUIFigure_ uilabel labels (pixel) for uifigure parents.
806+
% uilabel has no Units property and Position is always in pixels.
807+
% The pixel rectangles match the normalized layout used by the
808+
% classical path ([0.045 0.05 0.30 0.32], [0.36 ...], [0.66 ...])
809+
% and are recomputed on every hPanel size change so the labels
810+
% track the panel as the user resizes the figure.
811+
obj.hRangeLabelLeft = uilabel(obj.hPanel, ...
812+
'Text', '', ...
813+
'FontSize', 9, ...
814+
'HorizontalAlignment', 'left', ...
815+
'FontColor', fgColor, ...
816+
'BackgroundColor', panelBg);
817+
obj.hRangeLabelMiddle = uilabel(obj.hPanel, ...
818+
'Text', '', ...
819+
'FontSize', 9, ...
820+
'FontWeight', 'bold', ...
821+
'HorizontalAlignment', 'center', ...
822+
'FontColor', fgColor, ...
823+
'BackgroundColor', panelBg);
824+
obj.hRangeLabelRight = uilabel(obj.hPanel, ...
825+
'Text', '', ...
826+
'FontSize', 9, ...
827+
'HorizontalAlignment', 'right', ...
828+
'FontColor', fgColor, ...
829+
'BackgroundColor', panelBg);
830+
obj.layoutUIFigureLabels_();
831+
% Chain any existing SizeChangedFcn rather than clobbering it so
832+
% siblings that already listen to panel resize (e.g. parent
833+
% widgets) keep working. The saved handle is restored in delete.
834+
try
835+
obj.OldPanelSizeChangedFcn_ = get(obj.hPanel, 'SizeChangedFcn');
836+
catch
837+
obj.OldPanelSizeChangedFcn_ = [];
838+
end
839+
try
840+
set(obj.hPanel, 'SizeChangedFcn', @(~,~) obj.onPanelResized_());
841+
catch
842+
% Some parents (uigridlayout cells) may refuse SizeChangedFcn —
843+
% treat as best-effort. The labels stay at their initial pixel
844+
% positions in that case which is acceptable for a fixed-height
845+
% slider strip.
846+
end
847+
end
848+
849+
function layoutUIFigureLabels_(obj)
850+
%layoutUIFigureLabels_ Recompute uilabel pixel positions from current hPanel size.
851+
% Mirrors the normalized layout used by the classical uicontrol
852+
% path so both runtimes render the labels in the same place
853+
% relative to the slider axes above them.
854+
if ~ishandle(obj.hPanel); return; end
855+
px = getpixelposition(obj.hPanel);
856+
w = px(3); h = px(4);
857+
if w <= 0 || h <= 0; return; end
858+
yPx = round(0.05 * h);
859+
hPx = max(1, round(0.32 * h));
860+
leftRect = [round(0.045 * w), yPx, round(0.30 * w), hPx];
861+
middleRect = [round(0.36 * w), yPx, round(0.28 * w), hPx];
862+
rightRect = [round(0.66 * w), yPx, round(0.30 * w), hPx];
863+
if ~isempty(obj.hRangeLabelLeft) && ishandle(obj.hRangeLabelLeft)
864+
obj.hRangeLabelLeft.Position = leftRect;
865+
end
866+
if ~isempty(obj.hRangeLabelMiddle) && ishandle(obj.hRangeLabelMiddle)
867+
obj.hRangeLabelMiddle.Position = middleRect;
868+
end
869+
if ~isempty(obj.hRangeLabelRight) && ishandle(obj.hRangeLabelRight)
870+
obj.hRangeLabelRight.Position = rightRect;
871+
end
872+
end
873+
874+
function onPanelResized_(obj)
875+
%onPanelResized_ Re-layout uifigure labels and chain to any saved handler.
876+
try
877+
obj.layoutUIFigureLabels_();
878+
catch
879+
% Swallow layout errors — never let resize handling break the
880+
% rest of the figure's event chain.
881+
end
882+
cb = obj.OldPanelSizeChangedFcn_;
883+
if isempty(cb); return; end
884+
try
885+
if isa(cb, 'function_handle')
886+
feval(cb, obj.hPanel, []);
887+
elseif iscell(cb) && ~isempty(cb) && isa(cb{1}, 'function_handle')
888+
feval(cb{1}, obj.hPanel, [], cb{2:end});
889+
end
890+
catch
891+
% Defensive: a prior SizeChangedFcn that errors must not
892+
% cascade into TimeRangeSelector's own resize handling.
893+
end
894+
end
895+
896+
function setLabelText_(obj, hLabel, str)
897+
%setLabelText_ Set label text using the correct property for the widget type.
898+
% uilabel uses Text; uicontrol-text uses String. Single dispatch
899+
% point keeps setRangeLabels free of branching.
900+
if isempty(hLabel) || ~ishandle(hLabel); return; end
901+
if obj.IsUIFigureParent_
902+
try
903+
hLabel.Text = char(str);
904+
catch
905+
% Fallback to String (e.g. if a future refactor parents a
906+
% uicontrol under a uifigure-detected panel somehow).
907+
try set(hLabel, 'String', char(str)); catch, end
908+
end
909+
else
910+
try
911+
set(hLabel, 'String', char(str));
912+
catch
913+
try hLabel.Text = char(str); catch, end
914+
end
915+
end
916+
end
917+
756918
function redraw_(obj)
757919
%redraw_ Push current DataRange/Selection to the graphics handles.
758920
% Pads the axes XLim with 5% of the span on each side so the
@@ -767,8 +929,9 @@ function redraw_(obj)
767929
set(obj.hEdgeLeft, 'XData', [xL xL], 'YData', [0 1]);
768930
set(obj.hEdgeRight, 'XData', [xR xR], 'YData', [0 1]);
769931
% Inline in-axes edge labels removed (260512-hrn-followup).
770-
% Edge timestamps now live in the uicontrol text labels BELOW
771-
% the slider — populated via setRangeLabels from the engine.
932+
% Edge timestamps now live in the text labels BELOW the slider —
933+
% populated via setRangeLabels from the engine. Widget kind is
934+
% uicontrol-text (classical figure) or uilabel (uifigure).
772935
end
773936

774937
function installCallbacks_(obj)
@@ -788,6 +951,15 @@ function restoreCallbacks_(obj)
788951
set(obj.hFigure, 'WindowButtonMotionFcn', obj.OldWindowButtonMotionFcn);
789952
set(obj.hFigure, 'WindowButtonUpFcn', obj.OldWindowButtonUpFcn);
790953
end
954+
% Restore the panel SizeChangedFcn if we hijacked it for uilabel
955+
% positioning. Guarded — only fires on the uifigure-parent path,
956+
% and only when the panel is still alive.
957+
if obj.IsUIFigureParent_ && ~isempty(obj.hPanel) && ishandle(obj.hPanel)
958+
try
959+
set(obj.hPanel, 'SizeChangedFcn', obj.OldPanelSizeChangedFcn_);
960+
catch
961+
end
962+
end
791963
end
792964

793965
function [inAxes, xData] = pointerInAxes_(obj)

0 commit comments

Comments
 (0)