From 009ea0d8599123826ba4d86bbef5a23bc2d7b0d3 Mon Sep 17 00:00:00 2001 From: Robyn Jackey Date: Mon, 30 Jun 2025 11:15:12 -0400 Subject: [PATCH 1/3] Initial class for BaseExternalDialog - in progress --- .../oTMHbxZ3j_r4OW7cR1u2IS4RiqUd.xml | 6 + .../oTMHbxZ3j_r4OW7cR1u2IS4RiqUp.xml | 2 + widgets/+wt/+abstract/BaseExternalDialog.m | 812 ++++++++++++++++++ widgets/+wt/+abstract/BaseInternalDialog.m | 5 +- 4 files changed, 824 insertions(+), 1 deletion(-) create mode 100644 resources/project/Wqze2RguMm8RygQI0Uykdot17AI/oTMHbxZ3j_r4OW7cR1u2IS4RiqUd.xml create mode 100644 resources/project/Wqze2RguMm8RygQI0Uykdot17AI/oTMHbxZ3j_r4OW7cR1u2IS4RiqUp.xml create mode 100644 widgets/+wt/+abstract/BaseExternalDialog.m diff --git a/resources/project/Wqze2RguMm8RygQI0Uykdot17AI/oTMHbxZ3j_r4OW7cR1u2IS4RiqUd.xml b/resources/project/Wqze2RguMm8RygQI0Uykdot17AI/oTMHbxZ3j_r4OW7cR1u2IS4RiqUd.xml new file mode 100644 index 00000000..7a6326b9 --- /dev/null +++ b/resources/project/Wqze2RguMm8RygQI0Uykdot17AI/oTMHbxZ3j_r4OW7cR1u2IS4RiqUd.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/resources/project/Wqze2RguMm8RygQI0Uykdot17AI/oTMHbxZ3j_r4OW7cR1u2IS4RiqUp.xml b/resources/project/Wqze2RguMm8RygQI0Uykdot17AI/oTMHbxZ3j_r4OW7cR1u2IS4RiqUp.xml new file mode 100644 index 00000000..7672cf89 --- /dev/null +++ b/resources/project/Wqze2RguMm8RygQI0Uykdot17AI/oTMHbxZ3j_r4OW7cR1u2IS4RiqUp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/widgets/+wt/+abstract/BaseExternalDialog.m b/widgets/+wt/+abstract/BaseExternalDialog.m new file mode 100644 index 00000000..7cd0936e --- /dev/null +++ b/widgets/+wt/+abstract/BaseExternalDialog.m @@ -0,0 +1,812 @@ +classdef BaseExternalDialog < wt.abstract.BaseWidget + % Base class for a dialog that opens externally, in a separate figure + % window. The dialog's lifecycle is tied to the app that launched it. + % + % Note that this is incompatible with web apps. + + % ** This is a prototype component that may change in the future. + + % Copyright 2022-2025 The MathWorks Inc. + + + %% Events + events (HasCallbackProperty) + + % Triggered on dialog button pushed + DialogButtonPushed + + end %properties + + + %% Public Properties + properties (AbortSet, Access = public) + + % Dialog Size + Size (1,2) double {mustBePositive} = [350 200] + + % Modal (block other figure interaction) + Modal (1,1) logical = false + + end %properties + + + properties (AbortSet, Dependent, Access = public) + + % Dialog Title + Title + + end %properties + + + % Accessors + methods + + function set.Modal(obj, value) + obj.Modal = value; + obj.updateModalImage(); + end + + function value = get.Title(obj) + value = 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) + + DialogButtonText + + DialogButtonTag + + DialogButtonTooltip + + DialogButtonEnable + + DialogButtonWidth + + DialogButtonHeight + + end %methods + + % Accessors + methods + + function value = get.DialogButtonText(obj) + value = obj.DialogButtons.Text; + end + function set.DialogButtonText(obj,value) + obj.DialogButtons.Text = value; + end + + function value = get.DialogButtonTag(obj) + value = obj.DialogButtons.ButtonTag; + end + function set.DialogButtonTag(obj,value) + obj.DialogButtons.ButtonTag = value; + end + + function value = get.DialogButtonTooltip(obj) + value = obj.DialogButtons.Tooltip; + end + function set.DialogButtonTooltip(obj,value) + obj.DialogButtons.Tooltip = value; + end + + function value = get.DialogButtonEnable(obj) + value = obj.DialogButtons.ButtonEnable; + end + function set.DialogButtonEnable(obj,value) + obj.DialogButtons.ButtonEnable = value; + end + + function value = get.DialogButtonWidth(obj) + value = obj.DialogButtons.ButtonWidth; + end + function set.DialogButtonWidth(obj,value) + obj.DialogButtons.ButtonWidth = value; + end + + function value = get.DialogButtonHeight(obj) + value = obj.DialogButtons.ButtonHeight; + end + function set.DialogButtonHeight(obj,value) + obj.DialogButtons.ButtonHeight = value; + end + + end %methods + + + %% Dialog Actions Properties + properties (AbortSet, Access = public) + + % Dialog button action names that trigger deletion (button tags/names) + DeleteActions (1,:) string = ["delete","close","ok","cancel","exit"] + + end %properties + + + properties (SetAccess = protected) + + % Results / Output Data from the dialog + Output = [] + + % True if dialog is waiting for output + IsWaitingForOutput (1,1) logical = false + % Pressing a button (ok, cancel, or close) will toggle this false + % and cause the waitForOutput() method to complete. + + end %properties + + + %% Internal Properties + properties (Transient, NonCopyable, Hidden, SetAccess = private) + + % Outer grid to enable the panel to fill the component + OuterGrid matlab.ui.container.GridLayout + + % Outer panel for the dialog + OuterPanel matlab.ui.container.Panel + + % Inner grid to manage the content grid and status/button row + InnerGrid matlab.ui.container.GridLayout + + % Close button + CloseButton matlab.ui.control.Button + + % Temporary drag helper for moving the window + DragHelper wt.utility.FigureDragHelper {mustBeScalarOrEmpty} + + % Listeners to reference/parent objects to trigger dialog delete + LifecycleListeners (1,:) event.listener + + % Figure containing the dialog + Figure matlab.ui.Figure + + % Figure resize listener + FigureResizeListener (1,:) event.listener {mustBeScalarOrEmpty} + + % Modal image (optional) + ModalImage matlab.ui.control.Image + + % Dialog buttons (optional) + DialogButtons wt.ButtonGrid + + % Last action when closing dialog + LastAction string = [] + + end %properties + + + %% Constructor + methods + + function obj = BaseExternalDialog(varargin) + + % Call superclass constructor + obj@wt.abstract.BaseWidget(varargin{:}); + + + + end %function + + end %constructor + + + %% Destructor + methods + function delete(obj) + + % Delete the modal image + delete(obj.ModalImage) + + end %function + end %methods + + + %% Public methods + methods (Sealed, Access = public) + + function positionOver(obj, refComp) + % Positions the dialog centered over a given reference component + + arguments + obj (1,1) wt.abstract.BaseInternalDialog + refComp (1,1) matlab.graphics.Graphics + end + + % Reference component size and position + refPos = getpixelposition(refComp, true); + refSize = refPos(3:4); + + % Lower left corner depends if it's a figure + if isa(refComp, "matlab.ui.Figure") + refCornerA = [1 1]; + else + 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'); + + % Set final position + obj.Position = [dlgPos dlgSize]; + + % Restore warning + warning(warnState) + + end %function + + + function labels = addRowLabels(obj, names, parent, column, startRow) + % Add a group of standard row labels to the grid (or specified + % grid) + + arguments + obj %#ok + names (:,1) string + parent matlab.graphics.Graphics = obj.Grid + column (1,1) double {mustBeInteger} = 1 + startRow (1,1) double {mustBeInteger} = 1 + end + + numRows = numel(names); + labels = gobjects(1,numRows); + hasText = false(1,numRows); + for idx = 1:numel(names) + thisName = names(idx); + hasText(idx) = strlength(thisName) > 0; + if hasText(idx) + h = uilabel(parent); + h.HorizontalAlignment = "right"; + h.Text = thisName; + h.Layout.Column = column; + h.Layout.Row = idx + startRow - 1; + labels(idx) = h; + end + end + + % Remove the empty spaces + labels(~hasText) = []; + + end %function + + end %methods + + + %% Public Methods + methods (Access = public) + + function [output, lastAction] = waitForOutput(obj) + % Puts MATLAB in a wait state until the dialog buttons trigger + % action + + % Wait for action + obj.IsWaitingForOutput = true; + waitfor(obj,'IsWaitingForOutput',false) + + % Produce output + if isvalid(obj) + output = obj.Output; + lastAction = obj.LastAction; + else + % Dialog or figure was deleted + output = []; + lastAction = "close"; + end + + + % Check for deletion criteria + obj.checkDeletionCriteria() + + end %function + + end %methods + + + %% Protected methods + methods (Sealed, Access = public) + + function attachLifecycleListeners(obj, owners) + % Delete the dialog automatically upon destruction of the + % specified "owner" graphics objects + + arguments + obj (1,1) wt.abstract.BaseInternalDialog + owners (1,:) matlab.graphics.Graphics + end + + % Create listeners + % The dialog will be deleted if the listenObj is deleted + newListeners = listener(owners, "ObjectBeingDestroyed",... + @(src,evt)forceCloseDialog(obj)); + + % Add to any existing listeners + obj.LifecycleListeners = horzcat(obj.LifecycleListeners, newListeners); + + end %function + + end %methods + + + %% Protected methods + methods (Access = protected) + + function assignOutput(~) + % Triggered when the dialog should assign the output, generally + % in the case of a blocking dialog. + + % For blocking dialogs, the subclass should implement the + % assignOutput method. + + % Example subclass implementation: + % + % function assignOutput(obj) + % + % % Assign output + % obj.Output = ; + % + % end %function + + + end %function + + + function setup(obj) + % Configure the dialog + + % Disable warning + warnState = warning('off','MATLAB:ui:components:noPositionSetWhenInLayoutContainer'); + + % Defaults + obj.Position(3:4) = [350,200]; + + % Restore warning + warning(warnState) + + % Outer grid to enable the dialog panel to fill the component + obj.OuterGrid = uigridlayout(obj,[1 1]); + obj.OuterGrid.Padding = [0 0 0 0]; + + % Outer dialog panel + obj.OuterPanel = uipanel(obj.OuterGrid); + obj.OuterPanel.Title = "Dialog Title"; + obj.OuterPanel.FontSize = 16; + 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 + obj.CloseButton = uibutton(obj.OuterPanel); + obj.CloseButton.Text = ""; + obj.CloseButton.Tag = "close"; + obj.CloseButton.IconAlignment = "center"; + obj.CloseButton.ButtonPushedFcn = ... + @(src,evt)onDialogButtonPushed(obj,evt); + + % Inner Grid to manage content and button area + obj.InnerGrid = uigridlayout(obj.OuterPanel,[2 2]); + obj.InnerGrid.Padding = 10; + obj.InnerGrid.RowHeight = {'1x','fit'}; + obj.InnerGrid.ColumnWidth = {'1x','fit'}; + obj.InnerGrid.RowSpacing = 5; + + % Grid to place dialog content + obj.Grid = uigridlayout(obj.InnerGrid,[1 1]); + obj.Grid.Layout.Row = 1; + obj.Grid.Layout.Column = [1 2]; + obj.Grid.Padding = 0; + obj.Grid.RowSpacing = 5; + obj.Grid.ColumnSpacing = 5; + obj.Grid.Scrollable = true; + + % Apply theme colors + if ~isMATLABReleaseOlderThan("R2025a") + obj.OuterPanel.ForegroundColor = ... + obj.getThemeColor("--mw-color-primary"); + obj.OuterPanel.BorderColor = ... + obj.getThemeColor("--mw-borderColor-secondary"); + obj.OuterPanel.BackgroundColor = ... + obj.getThemeColor("--mw-backgroundColor-secondary"); + elseif isMATLABReleaseOlderThan("R2023a") + obj.OuterPanel.ForegroundColor = [0.38 0.38 0.38]; + obj.OuterPanel.BackgroundColor = [.9 .9 .9]; + else + obj.OuterPanel.ForegroundColor = [0.38 0.38 0.38]; + obj.OuterPanel.BorderColor = [.5 .5 .5]; + obj.OuterPanel.BackgroundColor = [.9 .9 .9]; + end + + % Apply close button color + 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; + obj.DialogButtons.Layout.Column = 2; + obj.DialogButtons.DefaultSize = 'fit'; + obj.DialogButtons.ButtonPushedFcn = ... + @(src,evt)onDialogButtonPushed(obj,evt); + + % Ensure it fits in the figure + obj.resizeToFitFigure(); + + % Reposition the close button + obj.repositionCloseButton(); + + end %function + + + function update(obj) + + % Ensure it fits in the figure + obj.resizeToFitFigure(); + + % Reposition the close button + obj.repositionCloseButton(); + + end %function + + + function updateBackgroundColorableComponents(obj) + % Update components that are affected by BackgroundColor + % (overrides the superclass method) + + % Update grid color + set([obj.InnerGrid, obj.Grid], "BackgroundColor", obj.BackgroundColor); + + % Call superclass method + obj.updateBackgroundColorableComponents@wt.mixin.BackgroundColorable(); + + end %function + + end %methods + + + methods (Sealed, Access = protected) + + function forceCloseDialog(obj) + % Should the dialog be deleted? + + obj.Output = []; + obj.LastAction = 'delete'; + + if ~obj.IsWaitingForOutput + + % Delete the dialog + delete(obj) + + else + + obj.IsWaitingForOutput = false; + + end + + end %function + + + function checkDeletionCriteria(obj) + % Should the dialog be deleted? + + % Check if ready to delete + isDeleteAction = matches(obj.LastAction, obj.DeleteActions, ... + "IgnoreCase", true); + + if ~obj.IsWaitingForOutput && isDeleteAction + + % Delete the dialog + delete(obj) + + end + + end %function + + end %methods + + + %% Private methods + methods (Access = private) + + function updateModalImage(obj) + % Triggered when the Modal property is changed + + % 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 + + % Set position to match the figure + posF = getpixelposition(obj.Figure); + szF = posF(3:4); + obj.ModalImage.Position = [1 1 szF]; + + end %if + + % Toggle visibility + obj.ModalImage.Visible = obj.Modal; + + end %function + + + function repositionCloseButton(obj) + % Triggered on figure resize + + % Outer panel inner/outer position + outerPos = obj.OuterPanel.OuterPosition; + wO = outerPos(3); + hO = outerPos(4); + + innerPos = obj.OuterPanel.InnerPosition(); + wI = innerPos(3); + hI = innerPos(4); + + % Calculate panel border width and title height + wBorder = (wO - wI) / 2; + hT = hO - hI - 3*wBorder; + + % Calculate close button positioning + hB = max(hT-4, 8) ; + yB = hO - 2*wBorder - hB - 1; + wB = hB; + xB = wO - 2*wBorder - wB - 1; + + % Move the close button + set(obj.CloseButton,"Position",[xB yB wB hB]); + + end %function + + + function resizeToFitFigure(obj) + % Triggered on figure resize + + % Get the current positioning + posD = obj.Position; + szRequest = obj.Size; + posLowerLeft = posD(1:2); + + % 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 + + % Don't go below 1 + posLowerLeft = max(posLowerLeft, 1); + + % 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 %function + + + function onMouseDrag(obj,evt) + % Triggered from DragHelper during drag or release + + % Check the drag event status + switch evt.Status + + case "motion" + obj.Position = evt.NewPosition; + + case "complete" + obj.Position = evt.NewPosition; + delete(obj.DragHelper) + obj.DragHelper(:) = []; + + end %switch + + end %function + + + function onDialogButtonPushed(obj,evt) + % Triggered when a dialog button is pushed (close, ok, etc.) + + % For blocking dialogs, the subclass should implement the + % assignOutput method. The assignOutput will be called based on + % which dialog button was pushed. + + % The pushed button's Tag (or Name if Tag is empty) will be + % set as the LastAction + + % Request to assign output + obj.assignOutput(); + + % What button was pushed? + if isa(evt, "wt.eventdata.ButtonPushedData") + % The lower dialog buttons (wt.ButtonGrid) + srcButton = evt.Button; + else + % Assume a regular button + srcButton = evt.Source; + end + action = srcButton.Tag; + if isempty(action) + action = srcButton.Text; + end + + % Set last action + obj.LastAction = action; + + % Prep event data + evtOut = wt.eventdata.DialogButtonPushedData; + evtOut.Action = obj.LastAction; + evtOut.Output = obj.Output; + + % Notify listeners / callback about output + obj.notify("DialogButtonPushed", evtOut) + + % Should the dialog be deleted? + if obj.IsWaitingForOutput + + % Don't delete here. Toggle status, allowing + % waitForOutput() to complete and handle deletion. + obj.IsWaitingForOutput = false; + + else + + % Check for deletion criteria + obj.checkDeletionCriteria() + + end + + end %function + + + function onFigureResized(obj,~) + % Triggered on figure resize + + % 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(); + + end %function + + + function onTitleButtonDown(obj,~) + % Triggered on title bar button down + + % Instantiate a figure drag helper to begin dragging dialog + obj.DragHelper = wt.utility.FigureDragHelper(obj); + obj.DragHelper.DragFcn = @(dhObj,evt)onMouseDrag(obj,evt); + + end %function + + + function applyCloseButtonColor(obj) + % Set color of close button + + % Create the "X" image mask + persistent imgMask + if isempty(imgMask) + imgMask = eye(16,16,"logical"); + % Make it an X + imgMask = imgMask | flip(imgMask); + % Widen the line + imgMask = imgMask | circshift(imgMask,1,1) | circshift(imgMask,-1,1); + end + + % Determine the color to use + if ~isMATLABReleaseOlderThan("R2025a") + bgColor = obj.getThemeColor("--mw-backgroundColor-secondary"); + iconColor = obj.getThemeColor("--mw-backgroundColor-iconuiFill-primary"); + else + bgColor = [.9 .9 .9]; + iconColor = [.38 .38 .38]; + end + + % Create the RGB components of the image + closeImgPage = zeros(16,16); + closeImg = repmat(closeImgPage,[1 1 3]); + for idx = 1:3 + closeImgPage(imgMask) = iconColor(idx); + closeImgPage(~imgMask) = bgColor(idx); + closeImg(:,:,idx) = closeImgPage; + end + + % Apply theicon + obj.CloseButton.Icon = closeImg; + obj.CloseButton.BackgroundColor = bgColor; + + end %function + + end %methods + + +end %classdef \ No newline at end of file diff --git a/widgets/+wt/+abstract/BaseInternalDialog.m b/widgets/+wt/+abstract/BaseInternalDialog.m index 8e50ee71..138144a9 100644 --- a/widgets/+wt/+abstract/BaseInternalDialog.m +++ b/widgets/+wt/+abstract/BaseInternalDialog.m @@ -1,5 +1,8 @@ classdef BaseInternalDialog < wt.abstract.BaseWidget - % Base class for a dialog that sits internal to the uifigure + % Base class for a dialog that opens as a panel within the figure + % window. The dialog's lifecycle is tied to the app that launched it. + % + % This enables compatibility with web apps. % ** This is a prototype component that may change in the future. From d15387f43c48512bc60ae22d209ae436bb5c2130 Mon Sep 17 00:00:00 2001 From: Robyn Jackey Date: Mon, 30 Jun 2025 15:46:48 -0400 Subject: [PATCH 2/3] first pass at BaseExternalDialog complete. Add a deprecated message to BaseDialog. --- widgets/+wt/+abstract/BaseDialog.m | 4 +- widgets/+wt/+abstract/BaseExternalDialog.m | 355 +++++---------------- 2 files changed, 83 insertions(+), 276 deletions(-) diff --git a/widgets/+wt/+abstract/BaseDialog.m b/widgets/+wt/+abstract/BaseDialog.m index a381f8dc..7a3df31e 100644 --- a/widgets/+wt/+abstract/BaseDialog.m +++ b/widgets/+wt/+abstract/BaseDialog.m @@ -4,8 +4,8 @@ wt.mixin.FieldColorable % Base class for a dialog panel - % Please note this is an experimental component that may change in the - % future. + % This component was a prototype that is now deprecated. Please switch + % to BaseInternalDialog or BaseExternalDialog instead. % Copyright 2022-2025 The MathWorks Inc. diff --git a/widgets/+wt/+abstract/BaseExternalDialog.m b/widgets/+wt/+abstract/BaseExternalDialog.m index 7cd0936e..6f687779 100644 --- a/widgets/+wt/+abstract/BaseExternalDialog.m +++ b/widgets/+wt/+abstract/BaseExternalDialog.m @@ -2,7 +2,8 @@ % Base class for a dialog that opens externally, in a separate figure % window. The dialog's lifecycle is tied to the app that launched it. % - % Note that this is incompatible with web apps. + % Note that this is incompatible with web apps, which support only a + % single figure. Use BaseInternalDialog for web app support. % ** This is a prototype component that may change in the future. @@ -21,9 +22,6 @@ %% Public Properties properties (AbortSet, Access = public) - % Dialog Size - Size (1,2) double {mustBePositive} = [350 200] - % Modal (block other figure interaction) Modal (1,1) logical = false @@ -32,6 +30,15 @@ properties (AbortSet, Dependent, Access = public) + % Modal tooltip + ModalTooltip (1,1) string + + % Position on screen [left bottom width height] + DialogPosition + + % Dialog Size + Size (1,2) double + % Dialog Title Title @@ -47,10 +54,35 @@ end function value = get.Title(obj) - value = obj.OuterPanel.Title; + value = string(obj.DialogFigure.Name); end function set.Title(obj, value) - obj.OuterPanel.Title = value; + obj.DialogFigure.Name = value; + end + + function value = get.DialogPosition(obj) + value = obj.DialogFigure.Position; + end + function set.DialogPosition(obj, value) + obj.DialogFigure.Position = value; + end + + function value = get.Size(obj) + if isscalar(obj.DialogFigure) + value = obj.DialogFigure.Position(3:4); + end + end + function set.Size(obj, value) + if isscalar(obj.DialogFigure) + obj.DialogFigure.Position(3:4) = value; + end + end + + function value = get.ModalTooltip(obj) + value = string(obj.ModalImage.Tooltip); + end + function set.ModalTooltip(obj, value) + obj.ModalImage.Tooltip = value; end end %methods @@ -147,30 +179,15 @@ %% Internal Properties properties (Transient, NonCopyable, Hidden, SetAccess = private) - % Outer grid to enable the panel to fill the component + % Outer grid to enable the component to fill the figure OuterGrid matlab.ui.container.GridLayout - % Outer panel for the dialog - OuterPanel matlab.ui.container.Panel - % Inner grid to manage the content grid and status/button row InnerGrid matlab.ui.container.GridLayout - % Close button - CloseButton matlab.ui.control.Button - - % Temporary drag helper for moving the window - DragHelper wt.utility.FigureDragHelper {mustBeScalarOrEmpty} - % Listeners to reference/parent objects to trigger dialog delete LifecycleListeners (1,:) event.listener - % Figure containing the dialog - Figure matlab.ui.Figure - - % Figure resize listener - FigureResizeListener (1,:) event.listener {mustBeScalarOrEmpty} - % Modal image (optional) ModalImage matlab.ui.control.Image @@ -183,19 +200,16 @@ end %properties - %% Constructor - methods - - function obj = BaseExternalDialog(varargin) + properties (Transient, NonCopyable, Hidden, SetAccess = protected) - % Call superclass constructor - obj@wt.abstract.BaseWidget(varargin{:}); + % Figure tied to the dialog lifecycle + Figure matlab.ui.Figure - + % This dialog's figure + DialogFigure matlab.ui.Figure + + end %properties - end %function - - end %constructor %% Destructor @@ -204,6 +218,9 @@ function delete(obj) % Delete the modal image delete(obj.ModalImage) + + % Delete the figure + delete(obj.DialogFigure) end %function end %methods @@ -388,46 +405,38 @@ function assignOutput(~) % % end %function - end %function function setup(obj) % Configure the dialog - % Disable warning - warnState = warning('off','MATLAB:ui:components:noPositionSetWhenInLayoutContainer'); + % Store the parent figure + obj.Figure = ancestor(obj,'figure'); - % Defaults - obj.Position(3:4) = [350,200]; + % Create a new figure for this dialog + obj.DialogFigure = uifigure(); + obj.DialogFigure.AutoResizeChildren = false; + obj.DialogFigure.Units = "pixels"; - % Restore warning - warning(warnState) + % Apply the same theme (R2025a and later) + if ~isMATLABReleaseOlderThan("R2025a") + obj.DialogFigure.Theme = obj.Figure.Theme; + end - % Outer grid to enable the dialog panel to fill the component - obj.OuterGrid = uigridlayout(obj,[1 1]); - obj.OuterGrid.Padding = [0 0 0 0]; - - % Outer dialog panel - obj.OuterPanel = uipanel(obj.OuterGrid); - obj.OuterPanel.Title = "Dialog Title"; - obj.OuterPanel.FontSize = 16; - 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 - obj.CloseButton = uibutton(obj.OuterPanel); - obj.CloseButton.Text = ""; - obj.CloseButton.Tag = "close"; - obj.CloseButton.IconAlignment = "center"; - obj.CloseButton.ButtonPushedFcn = ... - @(src,evt)onDialogButtonPushed(obj,evt); + % Give the figure a grid layout + obj.OuterGrid = uigridlayout(obj.DialogFigure, [1 1]); + obj.OuterGrid.Padding = 0; + + % Move the content to the new figure + obj.Parent = obj.OuterGrid; + + % Attach figure callbacks + obj.DialogFigure.DeleteFcn = @(~,~)delete(obj); + obj.DialogFigure.CloseRequestFcn = @(~,evt)onDialogButtonPushed(obj,evt); % Inner Grid to manage content and button area - obj.InnerGrid = uigridlayout(obj.OuterPanel,[2 2]); + obj.InnerGrid = uigridlayout(obj,[2 2]); obj.InnerGrid.Padding = 10; obj.InnerGrid.RowHeight = {'1x','fit'}; obj.InnerGrid.ColumnWidth = {'1x','fit'}; @@ -442,35 +451,11 @@ function setup(obj) obj.Grid.ColumnSpacing = 5; obj.Grid.Scrollable = true; - % Apply theme colors - if ~isMATLABReleaseOlderThan("R2025a") - obj.OuterPanel.ForegroundColor = ... - obj.getThemeColor("--mw-color-primary"); - obj.OuterPanel.BorderColor = ... - obj.getThemeColor("--mw-borderColor-secondary"); - obj.OuterPanel.BackgroundColor = ... - obj.getThemeColor("--mw-backgroundColor-secondary"); - elseif isMATLABReleaseOlderThan("R2023a") - obj.OuterPanel.ForegroundColor = [0.38 0.38 0.38]; - obj.OuterPanel.BackgroundColor = [.9 .9 .9]; - else - obj.OuterPanel.ForegroundColor = [0.38 0.38 0.38]; - obj.OuterPanel.BorderColor = [.5 .5 .5]; - obj.OuterPanel.BackgroundColor = [.9 .9 .9]; - end - - % Apply close button color - 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 + % Add modal image over the app's figure obj.ModalImage = uiimage(obj.Figure); obj.ModalImage.ImageSource = "overlay_gray.png"; obj.ModalImage.ScaleMethod = "stretch"; + obj.ModalImage.Tooltip = "Close the dialog box to continue using the app."; obj.ModalImage.Visible = "off"; obj.ModalImage.Position = [1 1 1 1]; @@ -482,22 +467,11 @@ 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(); - end %function - function update(obj) - - % Ensure it fits in the figure - obj.resizeToFitFigure(); + function update(~) - % Reposition the close button - obj.repositionCloseButton(); end %function @@ -567,16 +541,6 @@ function updateModalImage(obj) % 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 - % Set position to match the figure posF = getpixelposition(obj.Figure); szF = posF(3:4); @@ -590,97 +554,6 @@ function updateModalImage(obj) end %function - function repositionCloseButton(obj) - % Triggered on figure resize - - % Outer panel inner/outer position - outerPos = obj.OuterPanel.OuterPosition; - wO = outerPos(3); - hO = outerPos(4); - - innerPos = obj.OuterPanel.InnerPosition(); - wI = innerPos(3); - hI = innerPos(4); - - % Calculate panel border width and title height - wBorder = (wO - wI) / 2; - hT = hO - hI - 3*wBorder; - - % Calculate close button positioning - hB = max(hT-4, 8) ; - yB = hO - 2*wBorder - hB - 1; - wB = hB; - xB = wO - 2*wBorder - wB - 1; - - % Move the close button - set(obj.CloseButton,"Position",[xB yB wB hB]); - - end %function - - - function resizeToFitFigure(obj) - % Triggered on figure resize - - % Get the current positioning - posD = obj.Position; - szRequest = obj.Size; - posLowerLeft = posD(1:2); - - % 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 - - % Don't go below 1 - posLowerLeft = max(posLowerLeft, 1); - - % 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 %function - - - function onMouseDrag(obj,evt) - % Triggered from DragHelper during drag or release - - % Check the drag event status - switch evt.Status - - case "motion" - obj.Position = evt.NewPosition; - - case "complete" - obj.Position = evt.NewPosition; - delete(obj.DragHelper) - obj.DragHelper(:) = []; - - end %switch - - end %function - - function onDialogButtonPushed(obj,evt) % Triggered when a dialog button is pushed (close, ok, etc.) @@ -695,14 +568,20 @@ function onDialogButtonPushed(obj,evt) obj.assignOutput(); % What button was pushed? - if isa(evt, "wt.eventdata.ButtonPushedData") + if isa(evt, "matlab.ui.eventdata.WindowCloseRequestData") + srcButton = "close"; + action = "close"; + elseif isa(evt, "wt.eventdata.ButtonPushedData") % The lower dialog buttons (wt.ButtonGrid) srcButton = evt.Button; + action = srcButton.Tag; else % Assume a regular button srcButton = evt.Source; + action = srcButton.Tag; end - action = srcButton.Tag; + + % What action is being taken? if isempty(action) action = srcButton.Text; end @@ -734,78 +613,6 @@ function onDialogButtonPushed(obj,evt) end %function - - function onFigureResized(obj,~) - % Triggered on figure resize - - % 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(); - - end %function - - - function onTitleButtonDown(obj,~) - % Triggered on title bar button down - - % Instantiate a figure drag helper to begin dragging dialog - obj.DragHelper = wt.utility.FigureDragHelper(obj); - obj.DragHelper.DragFcn = @(dhObj,evt)onMouseDrag(obj,evt); - - end %function - - - function applyCloseButtonColor(obj) - % Set color of close button - - % Create the "X" image mask - persistent imgMask - if isempty(imgMask) - imgMask = eye(16,16,"logical"); - % Make it an X - imgMask = imgMask | flip(imgMask); - % Widen the line - imgMask = imgMask | circshift(imgMask,1,1) | circshift(imgMask,-1,1); - end - - % Determine the color to use - if ~isMATLABReleaseOlderThan("R2025a") - bgColor = obj.getThemeColor("--mw-backgroundColor-secondary"); - iconColor = obj.getThemeColor("--mw-backgroundColor-iconuiFill-primary"); - else - bgColor = [.9 .9 .9]; - iconColor = [.38 .38 .38]; - end - - % Create the RGB components of the image - closeImgPage = zeros(16,16); - closeImg = repmat(closeImgPage,[1 1 3]); - for idx = 1:3 - closeImgPage(imgMask) = iconColor(idx); - closeImgPage(~imgMask) = bgColor(idx); - closeImg(:,:,idx) = closeImgPage; - end - - % Apply theicon - obj.CloseButton.Icon = closeImg; - obj.CloseButton.BackgroundColor = bgColor; - - end %function - end %methods From f7c0a55057a162e98b2b175851c446ade91bf00b Mon Sep 17 00:00:00 2001 From: Robyn Jackey Date: Mon, 30 Jun 2025 16:50:53 -0400 Subject: [PATCH 3/3] work on BaseExternalDialog - positioning over figure --- widgets/+wt/+abstract/BaseExternalDialog.m | 117 ++++++++------------- 1 file changed, 41 insertions(+), 76 deletions(-) diff --git a/widgets/+wt/+abstract/BaseExternalDialog.m b/widgets/+wt/+abstract/BaseExternalDialog.m index 6f687779..84f20bd7 100644 --- a/widgets/+wt/+abstract/BaseExternalDialog.m +++ b/widgets/+wt/+abstract/BaseExternalDialog.m @@ -22,6 +22,9 @@ %% Public Properties properties (AbortSet, Access = public) + % Dialog Size + Size double {mustBePositive} = [350 200] + % Modal (block other figure interaction) Modal (1,1) logical = false @@ -36,9 +39,6 @@ % Position on screen [left bottom width height] DialogPosition - % Dialog Size - Size (1,2) double - % Dialog Title Title @@ -70,11 +70,14 @@ function value = get.Size(obj) if isscalar(obj.DialogFigure) value = obj.DialogFigure.Position(3:4); + else + value = obj.Size; end end function set.Size(obj, value) - if isscalar(obj.DialogFigure) - obj.DialogFigure.Position(3:4) = value; + obj.Size = value; + if isscalar(obj.DialogFigure) %#ok + obj.DialogFigure.Position(3:4) = value; %#ok end end @@ -203,7 +206,7 @@ properties (Transient, NonCopyable, Hidden, SetAccess = protected) % Figure tied to the dialog lifecycle - Figure matlab.ui.Figure + CallingFigure matlab.ui.Figure % This dialog's figure DialogFigure matlab.ui.Figure @@ -229,72 +232,6 @@ function delete(obj) %% Public methods methods (Sealed, Access = public) - function positionOver(obj, refComp) - % Positions the dialog centered over a given reference component - - arguments - obj (1,1) wt.abstract.BaseInternalDialog - refComp (1,1) matlab.graphics.Graphics - end - - % Reference component size and position - refPos = getpixelposition(refComp, true); - refSize = refPos(3:4); - - % Lower left corner depends if it's a figure - if isa(refComp, "matlab.ui.Figure") - refCornerA = [1 1]; - else - 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'); - - % Set final position - obj.Position = [dlgPos dlgSize]; - - % Restore warning - warning(warnState) - - end %function - - function labels = addRowLabels(obj, names, parent, column, startRow) % Add a group of standard row labels to the grid (or specified % grid) @@ -412,16 +349,21 @@ function setup(obj) % Configure the dialog % Store the parent figure - obj.Figure = ancestor(obj,'figure'); + obj.CallingFigure = ancestor(obj,'figure'); + + % Get the size input + sizeInput = obj.Size; % Create a new figure for this dialog obj.DialogFigure = uifigure(); obj.DialogFigure.AutoResizeChildren = false; obj.DialogFigure.Units = "pixels"; + obj.DialogFigure.Position(3:4) = sizeInput; + obj.positionOverCallingFigure() % Apply the same theme (R2025a and later) if ~isMATLABReleaseOlderThan("R2025a") - obj.DialogFigure.Theme = obj.Figure.Theme; + obj.DialogFigure.Theme = obj.CallingFigure.Theme; end % Give the figure a grid layout @@ -452,7 +394,7 @@ function setup(obj) obj.Grid.Scrollable = true; % Add modal image over the app's figure - obj.ModalImage = uiimage(obj.Figure); + obj.ModalImage = uiimage(obj.CallingFigure); obj.ModalImage.ImageSource = "overlay_gray.png"; obj.ModalImage.ScaleMethod = "stretch"; obj.ModalImage.Tooltip = "Close the dialog box to continue using the app."; @@ -529,6 +471,29 @@ function checkDeletionCriteria(obj) end %function + + function positionOverCallingFigure(obj) + % Positions the dialog centered over the reference figure + + % Reference component size and position + refPos = getpixelposition(obj.CallingFigure, true); + refSize = refPos(3:4); + refCornerA = refPos(1:2); + + % Dialog size + dlgSize = obj.DialogFigure.Position(3:4); + + % center it over the figure + + % Calculate lower-left corner + dlgPos = floor((refSize - dlgSize) / 2) + refCornerA; + + % Set final position + obj.DialogFigure.Position = [dlgPos dlgSize]; + + + end %function + end %methods @@ -542,7 +507,7 @@ function updateModalImage(obj) if obj.Modal % Set position to match the figure - posF = getpixelposition(obj.Figure); + posF = getpixelposition(obj.CallingFigure); szF = posF(3:4); obj.ModalImage.Position = [1 1 szF];