From 1e8fc581681760729103b7b0210c08bd3d7e3cd0 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 00:56:51 +0000 Subject: [PATCH 1/4] ProgressBarWindow: the bar fills its row on R2026a In a grid layout MATLAB places a uiaxes by its outer box and keeps room inside it for tick labels and a title, even hidden ones. On R2026a that left a 28 px bar row with a 2.67 px drawing area (measured: OuterPosition [11 11 511 28], InnerPosition [16 16 501 2.67]). Each bar's axes now sit in a borderless panel that the grid lays out, with the axes' inner box set to fill the panel; removeBar moves and deletes the panel. Confirmed by hand on R2026a; testBarFillsItsRow pins the structure. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FxKLA8Tbf9cpvDe7fuVeKf --- .../+ndi/+gui/+component/ProgressBarWindow.m | 24 +++++++++++++----- .../+gui/+component/TestProgressBarWindow.m | 25 +++++++++++++++++++ 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/src/ndi/+ndi/+gui/+component/ProgressBarWindow.m b/src/ndi/+ndi/+gui/+component/ProgressBarWindow.m index ac7c4b316..59ee9363f 100644 --- a/src/ndi/+ndi/+gui/+component/ProgressBarWindow.m +++ b/src/ndi/+ndi/+gui/+component/ProgressBarWindow.m @@ -346,13 +346,25 @@ app.ProgressBars(barNum).Timer.Layout.Row = rowNum - 1; app.ProgressBars(barNum).Timer.Layout.Column = 1:2; - % Add bar axes (background) - app.ProgressBars(barNum).Axes = uiaxes(app.ProgressGrid,... + % Add bar axes (background), inside a borderless panel that + % fills the grid cell. In a grid layout MATLAB places a uiaxes + % by its OUTER box and keeps room inside it for tick labels and + % a title, even hidden ones; R2026a keeps about 25 px, which + % left a 28 px row with a 2.7 px bar. Inside a plain panel the + % axes' inner box can be set to fill it exactly, whatever the + % release. The panel is the axes' parent: it is what the grid + % lays out (removeBar moves and deletes it through Axes.Parent). + barPanel = uipanel(app.ProgressGrid,'BorderType','none',... + 'BackgroundColor','w'); + barPanel.Layout.Row = rowNum; + barPanel.Layout.Column = 1; + app.ProgressBars(barNum).Axes = uiaxes(barPanel,... 'XLim',[0 1],'YLim',[0 1],'XTick',[],'YTick',[],'Box','off',... 'XColor','none','YColor','none','Color','w','Interactions',[]); app.ProgressBars(barNum).Axes.Toolbar.Visible = 'off'; - app.ProgressBars(barNum).Axes.Layout.Row = rowNum; - app.ProgressBars(barNum).Axes.Layout.Column = 1; + app.ProgressBars(barNum).Axes.Units = 'normalized'; + app.ProgressBars(barNum).Axes.PositionConstraint = 'innerposition'; + app.ProgressBars(barNum).Axes.InnerPosition = [0 0 1 1]; % Add bar patch (foreground) app.ProgressBars(barNum).Patch = patch(app.ProgressBars(barNum).Axes, ... @@ -513,7 +525,7 @@ rowNum = app.ProgressBars(barNum).Label.Layout.Row + [0 1]; % Remove progress bar - delete([app.ProgressBars(barNum).Axes,... + delete([app.ProgressBars(barNum).Axes.Parent,... % the bar's panel, and its axes app.ProgressBars(barNum).Percent,... app.ProgressBars(barNum).Button,... app.ProgressBars(barNum).Label,... @@ -524,7 +536,7 @@ for i = 1:numel(openBars) app.ProgressBars(openBars(i)).Label.Layout.Row = 2*i - 1; app.ProgressBars(openBars(i)).Timer.Layout.Row = 2*i - 1; - app.ProgressBars(openBars(i)).Axes.Layout.Row = 2*i; + app.ProgressBars(openBars(i)).Axes.Parent.Layout.Row = 2*i; app.ProgressBars(openBars(i)).Percent.Layout.Row = 2*i; app.ProgressBars(openBars(i)).Button.Layout.Row = 2*i; end diff --git a/tests/+ndi/+unittest/+gui/+component/TestProgressBarWindow.m b/tests/+ndi/+unittest/+gui/+component/TestProgressBarWindow.m index ca708fc25..73f5ee2d8 100644 --- a/tests/+ndi/+unittest/+gui/+component/TestProgressBarWindow.m +++ b/tests/+ndi/+unittest/+gui/+component/TestProgressBarWindow.m @@ -123,6 +123,31 @@ function testAddOneBar(testCase) testCase.verifyNotEmpty(app.ProgressBars(1).Axes, 'Axes handle should not be empty.'); testCase.verifyNotEmpty(app.ProgressBars(1).Patch, 'Patch handle should not be empty.'); end + + function testBarFillsItsRow(testCase) + % The bar's axes sit in a borderless panel that the grid lays + % out, with the axes' INNER box filling the panel. In a grid cell + % a uiaxes keeps room for hidden tick labels; on R2026a that + % left a 2.7 px bar in a 28 px row. + app = ndi.gui.component.ProgressBarWindow('Fill Test', 'Visible', 'off'); + testCase.addTeardown(@delete, app.ProgressFigure); + app.addBar('Label', 'Task 1', 'Tag', 'T1'); + app.addBar('Label', 'Task 2', 'Tag', 'T2'); + ax = app.ProgressBars(2).Axes; + testCase.verifyClass(ax.Parent, 'matlab.ui.container.Panel'); + testCase.verifyEqual(ax.Parent.Layout.Row, 4, 'the panel takes the bar row'); + testCase.verifyEqual(ax.PositionConstraint, 'innerposition'); + testCase.verifyEqual(ax.Units, 'normalized'); + testCase.verifyEqual(ax.InnerPosition, [0 0 1 1], 'AbsTol', 1e-9); + + % removing the first bar moves the second panel up and deletes + % the first bar's panel along with its axes + firstPanel = app.ProgressBars(1).Axes.Parent; + app.updateBar('T1', 1); + app.removeBar('T1'); + testCase.verifyFalse(isvalid(firstPanel), 'the removed bar''s panel is deleted'); + testCase.verifyEqual(ax.Parent.Layout.Row, 2, 'the remaining bar''s panel moves up'); + end function testAddMultipleBars(testCase) app = ndi.gui.component.ProgressBarWindow('Multi Bar Test', 'Visible', 'off'); testCase.addTeardown(@delete, app.ProgressFigure); From d367f621859c3444cac7dacc140c97cf6dcce699 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 01:06:54 +0000 Subject: [PATCH 2/4] ProgressBarWindow: the bar sits 5 px inside its row, on the window's background Matches how R2024b drew it: the previous commit filled the row edge to edge on a white panel. The panel now takes the grid's background colour, and fitBarAxes places the axes' inner box BarMargin (5) px inside the panel, in pixels, again whenever the panel resizes (SizeChangedFcn). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FxKLA8Tbf9cpvDe7fuVeKf --- .../+ndi/+gui/+component/ProgressBarWindow.m | 36 +++++++++++++++---- .../+gui/+component/TestProgressBarWindow.m | 13 +++++-- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/ndi/+ndi/+gui/+component/ProgressBarWindow.m b/src/ndi/+ndi/+gui/+component/ProgressBarWindow.m index 59ee9363f..d0f4a7db0 100644 --- a/src/ndi/+ndi/+gui/+component/ProgressBarWindow.m +++ b/src/ndi/+ndi/+gui/+component/ProgressBarWindow.m @@ -45,6 +45,10 @@ % ndi.gui.component.ProgressBarWindow.isHeadless, % ndi.gui.component.ProgressBarWindow.silentModeDefault + properties (Constant, Hidden) + BarMargin double = 5 % Pixels between a bar and the edges of its row (as R2024b drew it). + end + properties (Hidden) ScreenFrac double = 0.025 % Fraction of screen height used per bar row. IconClose char = fullfile(ndi.common.PathConstants.RootFolder,... @@ -351,20 +355,25 @@ % by its OUTER box and keeps room inside it for tick labels and % a title, even hidden ones; R2026a keeps about 25 px, which % left a 28 px row with a 2.7 px bar. Inside a plain panel the - % axes' inner box can be set to fill it exactly, whatever the - % release. The panel is the axes' parent: it is what the grid - % lays out (removeBar moves and deletes it through Axes.Parent). + % axes' inner box is placed exactly, BarMargin px in from each + % side -- the margin R2024b drew -- and kept there when the + % panel resizes. The panel is the axes' parent: it is what the + % grid lays out (removeBar moves and deletes it through + % Axes.Parent). barPanel = uipanel(app.ProgressGrid,'BorderType','none',... - 'BackgroundColor','w'); + 'BackgroundColor',app.ProgressGrid.BackgroundColor); barPanel.Layout.Row = rowNum; barPanel.Layout.Column = 1; app.ProgressBars(barNum).Axes = uiaxes(barPanel,... 'XLim',[0 1],'YLim',[0 1],'XTick',[],'YTick',[],'Box','off',... 'XColor','none','YColor','none','Color','w','Interactions',[]); app.ProgressBars(barNum).Axes.Toolbar.Visible = 'off'; - app.ProgressBars(barNum).Axes.Units = 'normalized'; + app.ProgressBars(barNum).Axes.Units = 'pixels'; app.ProgressBars(barNum).Axes.PositionConstraint = 'innerposition'; - app.ProgressBars(barNum).Axes.InnerPosition = [0 0 1 1]; + barAxes = app.ProgressBars(barNum).Axes; + barPanel.SizeChangedFcn = @(panel,~) ... + ndi.gui.component.ProgressBarWindow.fitBarAxes(barAxes, panel); + ndi.gui.component.ProgressBarWindow.fitBarAxes(barAxes, barPanel); % Add bar patch (foreground) app.ProgressBars(barNum).Patch = patch(app.ProgressBars(barNum).Axes, ... @@ -1031,6 +1040,21 @@ function bringToFront(app) end methods (Static) + function fitBarAxes(ax, panel) + %fitBarAxes Place a bar's axes BarMargin px inside its panel. + % + % ndi.gui.component.ProgressBarWindow.fitBarAxes(AX, PANEL) + % sets AX's inner box (pixels) to PANEL's area less BarMargin + % on each side, at least 1 x 1. addBar calls it once and as + % PANEL's SizeChangedFcn. + if ~isvalid(ax) || ~isvalid(panel) + return + end + m = ndi.gui.component.ProgressBarWindow.BarMargin; + pos = getpixelposition(panel); + ax.InnerPosition = [1 + m, 1 + m, max(pos(3) - 2*m, 1), max(pos(4) - 2*m, 1)]; + end + function tf = isHeadless() %isHeadless True when this MATLAB has no usable display. % diff --git a/tests/+ndi/+unittest/+gui/+component/TestProgressBarWindow.m b/tests/+ndi/+unittest/+gui/+component/TestProgressBarWindow.m index 73f5ee2d8..af9a20054 100644 --- a/tests/+ndi/+unittest/+gui/+component/TestProgressBarWindow.m +++ b/tests/+ndi/+unittest/+gui/+component/TestProgressBarWindow.m @@ -126,7 +126,7 @@ function testAddOneBar(testCase) function testBarFillsItsRow(testCase) % The bar's axes sit in a borderless panel that the grid lays - % out, with the axes' INNER box filling the panel. In a grid cell + % out, the axes' INNER box BarMargin px inside it. In a grid cell % a uiaxes keeps room for hidden tick labels; on R2026a that % left a 2.7 px bar in a 28 px row. app = ndi.gui.component.ProgressBarWindow('Fill Test', 'Visible', 'off'); @@ -137,8 +137,15 @@ function testBarFillsItsRow(testCase) testCase.verifyClass(ax.Parent, 'matlab.ui.container.Panel'); testCase.verifyEqual(ax.Parent.Layout.Row, 4, 'the panel takes the bar row'); testCase.verifyEqual(ax.PositionConstraint, 'innerposition'); - testCase.verifyEqual(ax.Units, 'normalized'); - testCase.verifyEqual(ax.InnerPosition, [0 0 1 1], 'AbsTol', 1e-9); + testCase.verifyEqual(ax.Units, 'pixels'); + testCase.verifyNotEmpty(ax.Parent.SizeChangedFcn, 'the bar follows its panel''s size'); + % BarMargin px inside the panel on every side + m = ndi.gui.component.ProgressBarWindow.BarMargin; + drawnow + ndi.gui.component.ProgressBarWindow.fitBarAxes(ax, ax.Parent); + pos = getpixelposition(ax.Parent); % the grid sets the panel's size + testCase.verifyEqual(ax.InnerPosition, ... + [1+m, 1+m, max(pos(3)-2*m, 1), max(pos(4)-2*m, 1)], 'AbsTol', 1e-6); % removing the first bar moves the second panel up and deletes % the first bar's panel along with its axes From ac416b8e9af42a46bd8466ee1dae2787a3cca812 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 01:08:22 +0000 Subject: [PATCH 3/4] Progress bar tests: the bar's axes sit in a panel the grid lays out testRemoveBar and testDockedBarRenders asserted the old structure (axes directly in the grid); they now read the panel, Axes.Parent. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FxKLA8Tbf9cpvDe7fuVeKf --- tests/+ndi/+unittest/+gui/+component/TestProgressBarWindow.m | 2 +- .../+unittest/+gui/+component/TestProgressBarWindowDocked.m | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/+ndi/+unittest/+gui/+component/TestProgressBarWindow.m b/tests/+ndi/+unittest/+gui/+component/TestProgressBarWindow.m index af9a20054..cb5594832 100644 --- a/tests/+ndi/+unittest/+gui/+component/TestProgressBarWindow.m +++ b/tests/+ndi/+unittest/+gui/+component/TestProgressBarWindow.m @@ -280,7 +280,7 @@ function testRemoveBar(testCase) testCase.verifyEqual(app.ProgressBars(2).State, 'Open', 'Remaining bar state incorrect.'); testCase.verifyNumElements(app.ProgressGrid.RowHeight, initialRowCount - 2, 'Grid rows not removed correctly.'); testCase.verifyEqual(app.ProgressBars(2).Label.Layout.Row, 1); - testCase.verifyEqual(app.ProgressBars(2).Axes.Layout.Row, 2); + testCase.verifyEqual(app.ProgressBars(2).Axes.Parent.Layout.Row, 2); % the bar's panel % Verify properties of removed bar testCase.verifyEqual(app.ProgressBars(1).Tag, 'TRemove', 'Removed bar is incorrect.'); testCase.verifyEqual(app.ProgressBars(1).State, 'Closed', 'Removed bar state incorrect.'); diff --git a/tests/+ndi/+unittest/+gui/+component/TestProgressBarWindowDocked.m b/tests/+ndi/+unittest/+gui/+component/TestProgressBarWindowDocked.m index 35c624ef0..303177770 100644 --- a/tests/+ndi/+unittest/+gui/+component/TestProgressBarWindowDocked.m +++ b/tests/+ndi/+unittest/+gui/+component/TestProgressBarWindowDocked.m @@ -75,8 +75,9 @@ function testDockedBarRenders(testCase) testCase.verifyEqual(app.ProgressBars(1).Progress, 0.5); testCase.verifyEqual(app.ProgressBars(1).Percent.Text, '50%'); - testCase.verifySameHandle(app.ProgressBars(1).Axes.Parent, testCase.Pane.BarGrid, ... - 'Bar axes should be parented to the pane bar grid.'); + % the axes sit in the bar's panel, which the pane's grid lays out + testCase.verifySameHandle(app.ProgressBars(1).Axes.Parent.Parent, testCase.Pane.BarGrid, ... + 'Bar panel should be parented to the pane bar grid.'); end function testCascadeMultipleBars(testCase) From 175b08afeec4d30546ea32a28974ee90600ac022 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 01:12:11 +0000 Subject: [PATCH 4/4] ProgressBarWindow: the bar's panel turns AutoResizeChildren off Otherwise MATLAB warns that SizeChangedFcn will not execute, and the bar keeps the size it had before the grid laid the panel out (found on R2026a). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FxKLA8Tbf9cpvDe7fuVeKf --- src/ndi/+ndi/+gui/+component/ProgressBarWindow.m | 3 ++- tests/+ndi/+unittest/+gui/+component/TestProgressBarWindow.m | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ndi/+ndi/+gui/+component/ProgressBarWindow.m b/src/ndi/+ndi/+gui/+component/ProgressBarWindow.m index d0f4a7db0..cac62b843 100644 --- a/src/ndi/+ndi/+gui/+component/ProgressBarWindow.m +++ b/src/ndi/+ndi/+gui/+component/ProgressBarWindow.m @@ -361,7 +361,8 @@ % grid lays out (removeBar moves and deletes it through % Axes.Parent). barPanel = uipanel(app.ProgressGrid,'BorderType','none',... - 'BackgroundColor',app.ProgressGrid.BackgroundColor); + 'BackgroundColor',app.ProgressGrid.BackgroundColor,... + 'AutoResizeChildren','off'); % else SizeChangedFcn never runs barPanel.Layout.Row = rowNum; barPanel.Layout.Column = 1; app.ProgressBars(barNum).Axes = uiaxes(barPanel,... diff --git a/tests/+ndi/+unittest/+gui/+component/TestProgressBarWindow.m b/tests/+ndi/+unittest/+gui/+component/TestProgressBarWindow.m index cb5594832..9b3184444 100644 --- a/tests/+ndi/+unittest/+gui/+component/TestProgressBarWindow.m +++ b/tests/+ndi/+unittest/+gui/+component/TestProgressBarWindow.m @@ -139,6 +139,8 @@ function testBarFillsItsRow(testCase) testCase.verifyEqual(ax.PositionConstraint, 'innerposition'); testCase.verifyEqual(ax.Units, 'pixels'); testCase.verifyNotEmpty(ax.Parent.SizeChangedFcn, 'the bar follows its panel''s size'); + testCase.verifyEqual(char(ax.Parent.AutoResizeChildren), 'off', ... + 'SizeChangedFcn does not run while AutoResizeChildren is on'); % BarMargin px inside the panel on every side m = ndi.gui.component.ProgressBarWindow.BarMargin; drawnow