diff --git a/resources/project/-NvNmnLN-CTaezFjMPFOnb8gLqY/X-gwN0u9Qo3fWErumQYWrvHW6b4d.xml b/resources/project/-NvNmnLN-CTaezFjMPFOnb8gLqY/X-gwN0u9Qo3fWErumQYWrvHW6b4d.xml new file mode 100644 index 0000000..99772b4 --- /dev/null +++ b/resources/project/-NvNmnLN-CTaezFjMPFOnb8gLqY/X-gwN0u9Qo3fWErumQYWrvHW6b4d.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/resources/project/-NvNmnLN-CTaezFjMPFOnb8gLqY/X-gwN0u9Qo3fWErumQYWrvHW6b4p.xml b/resources/project/-NvNmnLN-CTaezFjMPFOnb8gLqY/X-gwN0u9Qo3fWErumQYWrvHW6b4p.xml new file mode 100644 index 0000000..7b2a11a --- /dev/null +++ b/resources/project/-NvNmnLN-CTaezFjMPFOnb8gLqY/X-gwN0u9Qo3fWErumQYWrvHW6b4p.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/widgets/+wt/+abstract/BaseInternalDialog.m b/widgets/+wt/+abstract/BaseInternalDialog.m index 30c36c2..beb9c17 100644 --- a/widgets/+wt/+abstract/BaseInternalDialog.m +++ b/widgets/+wt/+abstract/BaseInternalDialog.m @@ -7,6 +7,9 @@ % window. The dialog's lifecycle is tied to the app that launched it. % % This enables compatibility with web apps. + % + % The dialog may flicker when resizing the figure if + % AutoResizeChildren is on. Disabling this is recommended. % ** This is a prototype component that may change in the future. @@ -31,35 +34,12 @@ % Modal (block other figure interaction) Modal (1,1) logical = false - end %properties - - - properties (AbortSet, Dependent, Access = public) - % Dialog Title - Title + Title (1,1) string = "" end %properties - % Accessors - methods - - function set.Modal(obj, value) - obj.Modal = value; - obj.updateModalImage(); - end - - function value = get.Title(obj) - value = string(obj.OuterPanel.Title); - end - function set.Title(obj, value) - obj.OuterPanel.Title = value; - end - - end %methods - - %% Dialog Button Properties % The dialog subclass can change these values properties (Dependent) @@ -149,6 +129,17 @@ %% Internal Properties + properties (Hidden) + + % Minimum allowable size before cropping + MinimumSize (1,2) double {mustBePositive} = [30 20]; + + % Buffer border space required on each side when sizing in figure + % Buffer (1,1) double {mustBeNonnegative} = 0 + + end %properties + + properties (Transient, NonCopyable, Hidden, SetAccess = private) % Outer grid to enable the panel to fill the component @@ -187,6 +178,47 @@ end %properties + %% Constructor + methods + + function obj = BaseInternalDialog(fig, varargin) + + arguments + % Figure parent - Create a figure if not provided + fig (1,1) matlab.ui.Figure = uifigure("AutoResizeChildren","off"); + end + + arguments (Repeating) + % Property-value pairs + varargin + end + + % Get the figure size + posF = getpixelposition(fig); + szFig = posF(3:4); + + % Add modal image + modalImage = uiimage(fig); + modalImage.ImageSource = "overlay_gray.png"; + modalImage.ScaleMethod = "stretch"; + modalImage.Visible = "off"; + modalImage.Position = [1 1 szFig]; + modalImage.Tag = "ModalImage"; + + % Call superclass constructor + obj = obj@wt.abstract.BaseWidget(fig, varargin{:}); + + % Store the modal background image + obj.ModalImage = modalImage; + + % Update the modal image positioning + obj.updateModalImage(); + + end %function + + end %methods + + %% Destructor methods function delete(obj) @@ -211,58 +243,28 @@ function positionOver(obj, refComp) % Reference component size and position refPos = getpixelposition(refComp, true); - refSize = refPos(3:4); + % refSize = refPos(3:4); % Lower left corner depends if it's a figure if isa(refComp, "matlab.ui.Figure") - refCornerA = [1 1]; + % refCornerA = [1 1]; + refPos(1:2) = [1 1]; else - refCornerA = refPos(1:2); + % refCornerA = refPos(1:2); end - % Dialog size - dlgPos = getpixelposition(obj); - dlgSize = dlgPos(3:4); - - % Does it fit entirely within the reference component? - if all(refSize >= dlgSize) - % Yes - center it over the component - - % Calculate lower-left corner - dlgPos = floor((refSize - dlgSize) / 2) + refCornerA; - - else - % NO - position within the figure - - % Get the corners of the figure (bottom left and top right) - figPos = getpixelposition(obj.Parent); - figSize = figPos(3:4); - - % Start with dialog position in lower-left of widget - dlgPos = refCornerA; - dlgCornerB = dlgPos + dlgSize; - - % Move left and down as needed to fit in figure - adj = figSize - dlgCornerB; - adj(adj>0) = 0; - dlgPos = max(dlgPos + adj, [1 1]); - dlgCornerB = dlgPos + dlgSize; - - % If it doesn't fit in the figure, shrink it - adj = figSize - dlgCornerB; - adj(adj>0) = 0; - dlgSize = dlgSize + adj; - - end %if - - % Disable warning - warnState = warning('off','MATLAB:ui:components:noPositionSetWhenInLayoutContainer'); + % Dialog position + posNew = obj.Position; - % Set final position - obj.Position = [dlgPos dlgSize]; + % Calculate the dialog position + % Request to center over refPos + posNew = calculatePositionWithinBounds(obj, posNew, refPos); - % Restore warning - warning(warnState) + % Update dialog position + if ~isequal(obj.Position, posNew) + fprintf(" Change position: posOld = %f posNew = %f\n", obj.Position, posNew); + obj.Position = posNew; + end end %function @@ -384,14 +386,8 @@ function assignOutput(~) function setup(obj) % Configure the dialog - % Disable warning - warnState = warning('off','MATLAB:ui:components:noPositionSetWhenInLayoutContainer'); - - % Defaults - obj.Position(3:4) = obj.Size; - - % Restore warning - warning(warnState) + % Store the figure + obj.Figure = ancestor(obj,'figure'); % Outer grid to enable the dialog panel to fill the component obj.OuterGrid = uigridlayout(obj,[1 1]); @@ -404,7 +400,6 @@ function setup(obj) obj.OuterPanel.FontWeight = "bold"; %obj.OuterPanel.BorderWidth = 1; obj.OuterPanel.AutoResizeChildren = false; - obj.OuterPanel.ResizeFcn = @(~,~)onOuterPanelResize(obj); obj.OuterPanel.ButtonDownFcn = @(~,evt)onTitleButtonDown(obj,evt); % Close Button @@ -452,17 +447,9 @@ function setup(obj) obj.applyCloseButtonColor() % Listen to figure size changes - obj.Figure = ancestor(obj,'figure'); obj.FigureResizeListener = listener(obj.Figure,"SizeChanged",... @(~,evt)onFigureResized(obj,evt)); - % Add modal image - obj.ModalImage = uiimage(obj.Figure); - obj.ModalImage.ImageSource = "overlay_gray.png"; - obj.ModalImage.ScaleMethod = "stretch"; - obj.ModalImage.Visible = "off"; - obj.ModalImage.Position = [1 1 1 1]; - % Add lower buttons obj.DialogButtons = wt.ButtonGrid(obj.InnerGrid,"Text",[],"Icon",[]); obj.DialogButtons.Layout.Row = 2; @@ -471,40 +458,38 @@ function setup(obj) obj.DialogButtons.ButtonPushedFcn = ... @(src,evt)onDialogButtonPushed(obj,evt); - % Ensure it fits in the figure - obj.resizeToFitFigure(); - - % Reposition the close button - obj.repositionCloseButton(); - - % Position over figure by default - if isscalar(obj.Figure) && isvalid(obj.Figure) - obj.positionOver(obj.Figure) - end - % Update component lists obj.ButtonColorableComponents = [obj.DialogButtons]; obj.TitleFontStyledComponents = [obj.OuterPanel]; obj.FontStyledComponents = [obj.DialogButtons]; - end %function - + % Listen to resizing of OuterPanel + % This enables the close button to stay in the correct spot + obj.OuterPanel.ResizeFcn = @(~,~)onOuterPanelResize(obj); - function postSetup(obj) + % Ensure it fits in the figure + obj.resizeToFitFigure(); - % Update modal image now - obj.updateModalImage(); + % Reposition the close button + repositionCloseButton(obj) end %function function update(obj) - % Ensure it fits in the figure - obj.resizeToFitFigure(); + % Update title + if strlength(obj.Title) + obj.OuterPanel.Title = obj.Title; + else + obj.OuterPanel.Title = " "; + end - % Reposition the close button - obj.repositionCloseButton(); + % Ensure it fits in the figure + % This is only needed if AutoResizeChildren is on + if obj.Figure.AutoResizeChildren + obj.resizeToFitFigure(); + end end %function @@ -569,42 +554,92 @@ function checkDeletionCriteria(obj) %% Private methods methods (Access = private) - function updateModalImage(obj) - % Triggered when the Modal property is changed + function resizeToFitFigure(obj) + % Triggered on figure resize + + % Update modal image + obj.updateModalImage(); + + % Get the current positioning + posNew = obj.Position; + % posLowerLeft = posOld(1:2); - % Setup must be complete to run this code - if ~obj.SetupFinished - return + % Calculate the dialog size + szDlg = calculateDialogSize(obj); + posNew(3:4) = szDlg; + + % Calculate the dialog position + if obj.SetupFinished + posNew = calculatePositionWithinBounds(obj, posNew); + else + % Try to center over figure by default + posFig = getpixelposition(obj.Figure); + posFig(1:2) = 1; + posNew = calculatePositionWithinBounds(obj, posNew, posFig); end - % If toggled on, do the following - if obj.Modal - - % Bring the dialog above the modal image - if isMATLABReleaseOlderThan("R2025a") - isDlg = obj.Figure.Children == obj; - isModalImage = obj.Figure.Children == obj.ModalImage; - otherChild = obj.Figure.Children(~isDlg & ~isModalImage); - obj.Figure.Children = vertcat(obj, obj.ModalImage, otherChild); - else - uistack(obj,"top"); % Works in 25a but not earlier - end + % Update dialog position + if ~isequal(obj.Position, posNew) + obj.Position = posNew; + end - % Set position to match the figure - posF = getpixelposition(obj.Figure); - szF = posF(3:4); - obj.ModalImage.Position = [1 1 szF]; + end %function - end %if - % Toggle visibility - obj.ModalImage.Visible = obj.Modal; + function szDlg = calculateDialogSize(obj) + % Calculate the dialog size to use, given the set Size and + % figure constraints + + % Get figure size + posFig = getpixelposition(obj.Figure); + + % Calculate allowed dialog size + szDlg = max( min(obj.Size, posFig(3:4)), obj.MinimumSize); + + end %function + + + function posOut = calculatePositionWithinBounds(obj, posIn, posCenter) + % Confirm and verify the position is within the figure bounds + + arguments + obj (1,1) wt.abstract.BaseInternalDialog + posIn (1,4) double {mustBeFinite} %requested [x,y,w,h] location + posCenter (1,4) double = nan(1,4) %optional - center over this [x,y,w,h] + end + + % Default output + posOut = posIn; + + % Get figure size + figPos = getpixelposition(obj.Figure); + figSize = figPos(3:4); + + % Center over a component? (optional posCenter) + if ~any(ismissing(posCenter)) + centerPoint = floor(posCenter(1:2) + posCenter(3:4)/2); + posOut(1:2) = floor(centerPoint - posOut(3:4)/2); + end + + % Ensure upper right corner is within the figure + dlgUpperRight = posOut(1:2) + posOut(3:4) - [1 1]; + if any(dlgUpperRight > figSize) + dlgAdjust = dlgUpperRight - figSize; + dlgAdjust(dlgAdjust < 0) = 0; + posOut(1:2) = posOut(1:2) - dlgAdjust; + end + + % Ensure lower left corner is within the figure + posOut(1:2) = max(posOut(1:2), [1 1]); end %function function repositionCloseButton(obj) - % Triggered on figure resize + % Called at end of resize + + % Get current position + oldPos = obj.CloseButton.Position; % Outer panel inner/outer position outerPos = obj.OuterPanel.OuterPosition; @@ -624,53 +659,31 @@ function repositionCloseButton(obj) yB = hO - 2*wBorder - hB - 1; wB = hB; xB = wO - 2*wBorder - wB - 1; + newPos = floor([xB yB wB hB]); % Move the close button - set(obj.CloseButton,"Position",[xB yB wB hB]); + if ~isequal(oldPos, newPos) + obj.CloseButton.Position = newPos; + end end %function - function resizeToFitFigure(obj) - % Triggered on figure resize + function updateModalImage(obj) + % Update modal image size and visibility - % Get the current positioning - posD = obj.Position; - szRequest = obj.Size; - posLowerLeft = posD(1:2); + % Only run if ModalImage exists + if isscalar(obj.ModalImage) && isvalid(obj.ModalImage) - % Get figure size - posF = getpixelposition(obj.Figure); - szF = posF(3:4); - buffer = [20 20]; - maxSize = szF - buffer; - - % Size is the smaller of requested size and figure size with - % buffer space - szD = min(szRequest, maxSize); - - % Restrict a minimum size also - minSize = [30 20]; - szD = max(szD, minSize); - - % Calculate fit within figure - posUpperRight = posLowerLeft + szD; - if any(posUpperRight > szF) - posAdjust = szF - posUpperRight; - posLowerLeft = posLowerLeft + posAdjust; - end + % Set modal image position to match the figure + posF = getpixelposition(obj.Figure); + szFig = posF(3:4); + obj.ModalImage.Position = [1 1 szFig]; - % Don't go below 1 - posLowerLeft = max(posLowerLeft, 1); + % Toggle visibility + obj.ModalImage.Visible = obj.Modal; - % Update modal image position - if obj.Modal - set(obj.ModalImage,"Position",[1 1 szF]); - end - - % Update dialog position - posNew = [posLowerLeft szD]; - set(obj,"Position",posNew); + end %if end %function @@ -754,20 +767,14 @@ function onFigureResized(obj,~) % Ensure it fits in the figure obj.resizeToFitFigure(); - % Reposition the close button - obj.repositionCloseButton(); - end %function function onOuterPanelResize(obj) % Triggered when the dialog window is resized - % Ensure it fits in the figure - obj.resizeToFitFigure(); - % Reposition the close button - obj.repositionCloseButton(); + repositionCloseButton(obj) end %function diff --git a/widgets/+wt/+dialog/ListSelection.m b/widgets/+wt/+dialog/ListSelection.m index 02da59d..da8277b 100644 --- a/widgets/+wt/+dialog/ListSelection.m +++ b/widgets/+wt/+dialog/ListSelection.m @@ -132,7 +132,7 @@ function setup(obj) obj.Grid.ColumnWidth = {'1x'}; % Set title - obj.Title = " "; + obj.Title = ""; % Add controls obj.PromptLabel = uilabel(obj.Grid); @@ -153,6 +153,9 @@ function setup(obj) function update(obj) + % Call superclass method + obj.update@wt.abstract.BaseInternalDialog; + % Configure list obj.ListBox.Items = obj.Items; obj.ListBox.ItemsData = obj.ItemsData; diff --git a/widgets/+wt/+dialog/Login.m b/widgets/+wt/+dialog/Login.m index 8d73abb..1055a3d 100644 --- a/widgets/+wt/+dialog/Login.m +++ b/widgets/+wt/+dialog/Login.m @@ -23,6 +23,9 @@ function setup(obj) % Defaults obj.Size = [300,140]; + % This is normally a modal dialog + obj.Modal = true; + % Configure which actions close the dialog obj.DeleteActions = ["close","login","cancel"]; diff --git a/widgets/doc/DialogsList.mlx b/widgets/doc/DialogsList.mlx new file mode 100644 index 0000000..cdd8db9 Binary files /dev/null and b/widgets/doc/DialogsList.mlx differ diff --git a/widgets/doc/GettingStarted.mlx b/widgets/doc/GettingStarted.mlx index 05deffa..e700b94 100644 Binary files a/widgets/doc/GettingStarted.mlx and b/widgets/doc/GettingStarted.mlx differ diff --git a/widgets/doc/MainPage.mlx b/widgets/doc/MainPage.mlx index afccfa2..4b6c19d 100644 Binary files a/widgets/doc/MainPage.mlx and b/widgets/doc/MainPage.mlx differ diff --git a/widgets/doc/UserGuide.mlx b/widgets/doc/UserGuide.mlx index 6e3679b..84511f2 100644 Binary files a/widgets/doc/UserGuide.mlx and b/widgets/doc/UserGuide.mlx differ diff --git a/widgets/examples/ListSelectionDialogExample.mlx b/widgets/examples/ListSelectionDialogExample.mlx index 6bf51c6..aad93f3 100644 Binary files a/widgets/examples/ListSelectionDialogExample.mlx and b/widgets/examples/ListSelectionDialogExample.mlx differ diff --git a/widgets/examples/LoginDialogExample.mlx b/widgets/examples/LoginDialogExample.mlx index c7ad936..7be336f 100644 Binary files a/widgets/examples/LoginDialogExample.mlx and b/widgets/examples/LoginDialogExample.mlx differ diff --git a/widgets/wtExamplesList.m b/widgets/wtExamplesList.m index 2bbf92b..9b83bf8 100644 --- a/widgets/wtExamplesList.m +++ b/widgets/wtExamplesList.m @@ -3,8 +3,20 @@ function wtExamplesList() % Copyright 2025 The MathWorks Inc. + +%% Widgets + % Get the path listPath = fullfile(wt.utility.widgetsRoot, "doc", "WidgetsList"); +% Open the editor file +edit(listPath) + + +%% Dialogs + +% Get the path +listPath = fullfile(wt.utility.widgetsRoot, "doc", "DialogsList"); + % Open the editor file edit(listPath) \ No newline at end of file