diff --git a/resources/project/Wqze2RguMm8RygQI0Uykdot17AI/oTMHbxZ3j_r4OW7cR1u2IS4RiqUd.xml b/resources/project/Wqze2RguMm8RygQI0Uykdot17AI/oTMHbxZ3j_r4OW7cR1u2IS4RiqUd.xml new file mode 100644 index 0000000..7a6326b --- /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 0000000..7672cf8 --- /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/BaseDialog.m b/widgets/+wt/+abstract/BaseDialog.m index a381f8d..7a3df31 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 new file mode 100644 index 0000000..84f20bd --- /dev/null +++ b/widgets/+wt/+abstract/BaseExternalDialog.m @@ -0,0 +1,584 @@ +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, which support only a + % single figure. Use BaseInternalDialog for web app support. + + % ** 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 double {mustBePositive} = [350 200] + + % Modal (block other figure interaction) + Modal (1,1) logical = false + + end %properties + + + properties (AbortSet, Dependent, Access = public) + + % Modal tooltip + ModalTooltip (1,1) string + + % Position on screen [left bottom width height] + DialogPosition + + % Dialog Title + Title + + end %properties + + + % Accessors + methods + + function set.Modal(obj, value) + obj.Modal = value; + obj.updateModalImage(); + end + + function value = get.Title(obj) + value = string(obj.DialogFigure.Name); + end + function set.Title(obj, 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); + else + value = obj.Size; + end + end + function set.Size(obj, value) + obj.Size = value; + if isscalar(obj.DialogFigure) %#ok + obj.DialogFigure.Position(3:4) = value; %#ok + 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 + + + %% 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 component to fill the figure + OuterGrid matlab.ui.container.GridLayout + + % Inner grid to manage the content grid and status/button row + InnerGrid matlab.ui.container.GridLayout + + % Listeners to reference/parent objects to trigger dialog delete + LifecycleListeners (1,:) event.listener + + % Modal image (optional) + ModalImage matlab.ui.control.Image + + % Dialog buttons (optional) + DialogButtons wt.ButtonGrid + + % Last action when closing dialog + LastAction string = [] + + end %properties + + + properties (Transient, NonCopyable, Hidden, SetAccess = protected) + + % Figure tied to the dialog lifecycle + CallingFigure matlab.ui.Figure + + % This dialog's figure + DialogFigure matlab.ui.Figure + + end %properties + + + + %% Destructor + methods + function delete(obj) + + % Delete the modal image + delete(obj.ModalImage) + + % Delete the figure + delete(obj.DialogFigure) + + end %function + end %methods + + + %% Public methods + methods (Sealed, Access = public) + + 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 + + % Store the parent 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.CallingFigure.Theme; + end + + % 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,[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; + + % Add modal image over the app's 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."; + 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); + + end %function + + + function update(~) + + + 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 + + + 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 + + + %% Private methods + methods (Access = private) + + function updateModalImage(obj) + % Triggered when the Modal property is changed + + % If toggled on, do the following + if obj.Modal + + % Set position to match the figure + posF = getpixelposition(obj.CallingFigure); + szF = posF(3:4); + obj.ModalImage.Position = [1 1 szF]; + + end %if + + % Toggle visibility + obj.ModalImage.Visible = obj.Modal; + + 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, "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 + + % What action is being taken? + 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 + + 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 8e50ee7..138144a 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.