diff --git a/src/ndi/+ndi/+gui/+component/ProgressBarWindow.m b/src/ndi/+ndi/+gui/+component/ProgressBarWindow.m index 5de57f331..b2937e56d 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,... @@ -358,13 +362,31 @@ 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 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',app.ProgressGrid.BackgroundColor,... + 'AutoResizeChildren','off'); % else SizeChangedFcn never runs + 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 = 'pixels'; + app.ProgressBars(barNum).Axes.PositionConstraint = 'innerposition'; + 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, ... @@ -525,7 +547,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,... @@ -536,7 +558,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 @@ -1043,6 +1065,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 c3f3a7879..c25facdff 100644 --- a/tests/+ndi/+unittest/+gui/+component/TestProgressBarWindow.m +++ b/tests/+ndi/+unittest/+gui/+component/TestProgressBarWindow.m @@ -123,6 +123,40 @@ 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, 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'); + 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, '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 + 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 + 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); @@ -248,7 +282,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)