diff --git a/deploy/wtDeployVersion.txt b/deploy/wtDeployVersion.txt index 79a6144..59aa62c 100644 --- a/deploy/wtDeployVersion.txt +++ b/deploy/wtDeployVersion.txt @@ -1 +1 @@ -2.4.4 +2.4.5 diff --git a/release/Widgets Toolbox 2.4.5.mltbx b/release/Widgets Toolbox 2.4.5.mltbx new file mode 100644 index 0000000..829d001 Binary files /dev/null and b/release/Widgets Toolbox 2.4.5.mltbx differ diff --git a/resources/project/FVIFMHuse2VWlFgbqX2S_OQzxiU/dmci6U3XJ-vPU9J7ufENgm2ZSY4d.xml b/resources/project/FVIFMHuse2VWlFgbqX2S_OQzxiU/dmci6U3XJ-vPU9J7ufENgm2ZSY4d.xml new file mode 100644 index 0000000..4356a6a --- /dev/null +++ b/resources/project/FVIFMHuse2VWlFgbqX2S_OQzxiU/dmci6U3XJ-vPU9J7ufENgm2ZSY4d.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/FVIFMHuse2VWlFgbqX2S_OQzxiU/dmci6U3XJ-vPU9J7ufENgm2ZSY4p.xml b/resources/project/FVIFMHuse2VWlFgbqX2S_OQzxiU/dmci6U3XJ-vPU9J7ufENgm2ZSY4p.xml new file mode 100644 index 0000000..10dc37c --- /dev/null +++ b/resources/project/FVIFMHuse2VWlFgbqX2S_OQzxiU/dmci6U3XJ-vPU9J7ufENgm2ZSY4p.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/test/+wt/+test/BaseTest.m b/test/+wt/+test/BaseTest.m index 4567a86..bff07c1 100644 --- a/test/+wt/+test/BaseTest.m +++ b/test/+wt/+test/BaseTest.m @@ -1,7 +1,7 @@ classdef BaseTest < matlab.uitest.TestCase % Implements a unit test with some added helper methods - % Copyright 2020-2025 The MathWorks Inc. + % Copyright 2020-2026 The MathWorks Inc. %% Protected Helper Methods @@ -115,6 +115,55 @@ function verifyNotVisible(testCase, component) end %function + function verifyEventuallyHasParent(testCase, component, parent) + % Verify the specified component eventually has a parent + + arguments + testCase matlab.uitest.TestCase + component + parent matlab.graphics.Graphics {mustBeScalarOrEmpty} = gobjects(0) + end + + import matlab.unittest.constraints.Eventually + import matlab.unittest.constraints.IsScalar + import matlab.unittest.constraints.IsEqualTo + + % Verify values + if isempty(parent) + diag = "Expected the component to have a parent."; + testCase.verifyThat(... + @()get(component, "Parent"),... + Eventually(IsScalar, "WithTimeoutOf", 5), diag); + else + diag = "Expected the component to have the specified parent."; + testCase.verifyThat(... + @()get(component, "Parent"),... + Eventually(IsEqualTo(parent), "WithTimeoutOf", 5), diag); + end + + end %function + + + function verifyEventuallyHasNoParent(testCase, component) + % Verify the specified component gets unparented + + arguments + testCase matlab.uitest.TestCase + component + end + + import matlab.unittest.constraints.Eventually + import matlab.unittest.constraints.IsEmpty + + % Verify values + diag = "Expected the component to have its Parent be empty."; + testCase.verifyThat(... + @()get(component, "Parent"),... + Eventually(IsEmpty, "WithTimeoutOf", 5), diag); + + end %function + + function verifyEnabled(testCase, component) % Verify the specified component is set to enabled diff --git a/test/+wt/+test/FileSelector.m b/test/+wt/+test/FileSelector.m index 3d06653..e24755b 100644 --- a/test/+wt/+test/FileSelector.m +++ b/test/+wt/+test/FileSelector.m @@ -1,7 +1,7 @@ classdef FileSelector < wt.test.BaseWidgetTest % Implements a unit test for a widget or component - % Copyright 2020-2025 The MathWorks Inc. + % Copyright 2020-2026 The MathWorks Inc. @@ -170,6 +170,28 @@ function testFileEditField(testCase) %testCase.verifyTrue(logical(testCase.Widget.WarnImage.Visible)); end %function + + + function testWebAddress(testCase) + + % Get the edit field + editControl = testCase.Widget.EditControl; + + % Set the type + testCase.verifySetProperty("SelectionType", "folder"); + + % Type a valid value + newValue = "s3://abucket/afolder"; + testCase.verifyTypeAction(editControl, newValue, "Value"); + + % Verify the ValueIsValidPath value (false because not local + % folder) + testCase.verifyFalse(testCase.Widget.ValueIsValidPath) + + % Verify the warn image does not show (we ignore web address) + testCase.verifyFalse(logical(testCase.Widget.WarnImage.Visible)); + + end %function function testRootDirectoryAndHistory(testCase) @@ -232,6 +254,7 @@ function testRootDirectoryAndHistory(testCase) end %function + function testButtonLabel(testCase) % Get the button control @@ -251,6 +274,63 @@ function testButtonLabel(testCase) end %function + + function testButtonVisibility(testCase) + + % Get the button control + buttonControl = testCase.Widget.ButtonControl; + + % Sample paths to show + sampleFile = mfilename("fullpath"); + sampleFolder = fileparts(sampleFile); + + + % --- SelectionType == folder --- % + testCase.Widget.SelectionType = wt.enum.FileFolderState.folder; + testCase.Widget.Value = sampleFolder; + + % Normal App - should show button + testCase.Widget.IsWebApp = false; + testCase.verifyEventuallyHasParent(buttonControl); + testCase.Widget.forceUpdate(); + + % Web App - should NOT show button + testCase.Widget.IsWebApp = true; + testCase.verifyEventuallyHasNoParent(buttonControl); % NO Parent + testCase.Widget.forceUpdate(); + + + % --- SelectionType == file --- % + testCase.Widget.SelectionType = wt.enum.FileFolderState.file; + testCase.Widget.Value = sampleFile; + + % Normal App - should show button + testCase.Widget.IsWebApp = false; + testCase.verifyEventuallyHasParent(buttonControl); + testCase.Widget.forceUpdate(); + + % Web App - should show button + testCase.Widget.IsWebApp = true; + testCase.verifyEventuallyHasParent(buttonControl); + testCase.Widget.forceUpdate(); + + + % --- SelectionType == putfile --- % + testCase.Widget.SelectionType = wt.enum.FileFolderState.putfile; + testCase.Widget.Value = sampleFile; + + % Normal App - should show button + testCase.Widget.IsWebApp = false; + testCase.verifyEventuallyHasParent(buttonControl); + testCase.Widget.forceUpdate(); + + % Web App - should show button + testCase.Widget.IsWebApp = true; + testCase.verifyEventuallyHasParent(buttonControl); + testCase.Widget.forceUpdate(); + + end %function + % Since this test-case unlocks the test figure it should be last in % line. %RJ - Commented out the below test that fails in R2025a and later @@ -306,25 +386,25 @@ function testButtonLabel(testCase) end %classdef -function localPressEscape(fig) - -% Unlock the figure, otherwise escape will not work. -matlab.uitest.unlock(fig); - -% Bring focus to figure -figure(fig) - -% Press ESCAPE -r = java.awt.Robot; -r.keyPress(java.awt.event.KeyEvent.VK_ESCAPE); -pause(0.1); -r.keyRelease(java.awt.event.KeyEvent.VK_ESCAPE); - -end - -function localRevertShowInWebAppsSetting(s, val) - -% Revert setting on cleanup -s.matlab.ui.dialog.fileIO.ShowInWebApps.TemporaryValue = val; - -end \ No newline at end of file +% function localPressEscape(fig) +% +% % Unlock the figure, otherwise escape will not work. +% matlab.uitest.unlock(fig); +% +% % Bring focus to figure +% figure(fig) +% +% % Press ESCAPE +% r = java.awt.Robot; +% r.keyPress(java.awt.event.KeyEvent.VK_ESCAPE); +% pause(0.1); +% r.keyRelease(java.awt.event.KeyEvent.VK_ESCAPE); +% +% end +% +% function localRevertShowInWebAppsSetting(s, val) +% +% % Revert setting on cleanup +% s.matlab.ui.dialog.fileIO.ShowInWebApps.TemporaryValue = val; +% +% end \ No newline at end of file diff --git a/widgets/+wt/+utility/cleanPath.m b/widgets/+wt/+utility/cleanPath.m index 2e004ba..c49bd43 100644 --- a/widgets/+wt/+utility/cleanPath.m +++ b/widgets/+wt/+utility/cleanPath.m @@ -1,4 +1,4 @@ -function path = cleanPath(path) +function out = cleanPath(in) % cleanPath - Utility to clean and standardize a file/folder path % % This function will clean and standardize a file or folder path. It @@ -24,15 +24,60 @@ % "C:\Program Files\MATLAB" % -% Copyright 2020-2025 The MathWorks Inc. +% Copyright 2020-2026 The MathWorks Inc. % --------------------------------------------------------------------- -% File separator - in case of regional variants -fsep = regexptranslate("escape",filesep); +arguments + in string % accepts string arrays of any size +end -% Pattern for regexp -fsepOpts = join(["\\","/",fsep],"|"); -pattern = "^\s+|(?<=\S)(\s|" + fsepOpts + ")+$"; +if isempty(in) + out = in; + return +end -% Perform replacement -path = regexprep(path,pattern,""); +fs = filesep; +sz = size(in); +out = strings(sz); % allocate same size + +for idx = 1:numel(in) + sIn = char(in(idx)); + if strlength(in(idx)) == 0 + out(idx) = in(idx); + continue + end + + % If URI-like (scheme://) then leave unchanged + if ~isempty(regexp(sIn, '^[A-Za-z][A-Za-z0-9+.\-]*://', 'once')) + out(idx) = in(idx); + continue + end + + % Detect UNC-style leading slashes + isUNC = ~isempty(regexp(sIn, '^[\\/]{2,}', 'once')); + + % Collapse any run of slashes/backslashes to single platform filesep + s = regexprep(sIn, '[\\/]+', fs); + + if isUNC + % Remove any leading separators then ensure exactly two leading filesep + s = regexprep(s, ['^' regexptranslate('escape', fs) '+'], ''); + s = [repmat(fs,1,2) s]; + else + % Preserve drive-letter prefix like 'C:' and ensure at most one filesep after it + m = regexp(sIn, '^([A-Za-z]:)[\\/]*', 'tokens', 'once'); + if ~isempty(m) + drive = m{1}; + % Strip any leading drive+sep from s, then reapply drive and single filesep if needed + s = regexprep(s, ['^' regexptranslate('escape', drive) regexptranslate('escape', fs) '?'], ''); + if isempty(s) + s = drive; + else + s = [drive fs s]; + end + end + end + + out(idx) = string(s); +end +end \ No newline at end of file diff --git a/widgets/+wt/FileSelector.m b/widgets/+wt/FileSelector.m index b3b6994..68b9a77 100644 --- a/widgets/+wt/FileSelector.m +++ b/widgets/+wt/FileSelector.m @@ -6,7 +6,7 @@ wt.mixin.Tooltipable % File or folder selection control with browse button - % Copyright 2020-2025 The MathWorks Inc. + % Copyright 2020-2026 The MathWorks Inc. %% Public properties @@ -93,6 +93,14 @@ end %properties + properties (Hidden) + + % Indicates if this is a web app + IsWebApp (1,1) logical = false + + end %properties + + %% Protected methods methods (Access = protected) @@ -105,6 +113,13 @@ function setup(obj) % Adjust default size obj.Position(3:4) = [400 25]; + % Is this a web app? + try %#ok + % This is undocumented but was mentioned here: + % https://www.mathworks.com/matlabcentral/answers/584102-check-if-is-deployed-as-web-app + obj.IsWebApp = matlab.internal.environment.context.isWebAppServer(); + end + % Configure Grid obj.Grid.ColumnWidth = {'1x',25,25,25}; @@ -194,7 +209,8 @@ function update(obj) end %if obj.ShowHistory % Show the warning icon? - showWarn = strlength(obj.Value) && ~obj.ValueIsValidPath; + isWeb = startsWith(obj.Value, alphanumericsPattern + "://"); + showWarn = strlength(obj.Value) && ~obj.ValueIsValidPath && ~isWeb; obj.WarnImage.Visible = showWarn; % Set warning icon tooltip @@ -210,10 +226,24 @@ function update(obj) % Update button appearance obj.ButtonControl.Text = obj.ButtonLabel; - if strlength(obj.ButtonLabel) + if obj.IsWebApp && obj.SelectionType == "folder" + + % Hide button - can't do uigetdir on a webapp + obj.ButtonControl.Parent = []; + obj.Grid.ColumnWidth(4:end) = []; + + elseif strlength(obj.ButtonLabel) + + % Button has a label - make room + obj.ButtonControl.Parent = obj.Grid; obj.Grid.ColumnWidth{4} = 125; + else + + % Button is only an icon + obj.ButtonControl.Parent = obj.Grid; obj.Grid.ColumnWidth{4} = 25; + end end %function @@ -301,13 +331,26 @@ function onButtonPushed(obj,~) fig = ancestor(obj,"figure"); % Prompt user for the path - if obj.SelectionType == "file" - [fileName,pathName] = uigetfile(filter,"Select a file",initialPath); - elseif obj.SelectionType == "putfile" - [fileName,pathName] = uiputfile(filter,"Specify an output file",initialPath); + if obj.IsWebApp + % Web app - + if obj.SelectionType == "file" + [fileName,pathName] = uigetfile(filter,"Select a file"); + elseif obj.SelectionType == "putfile" + [fileName,pathName] = uiputfile(filter,"Specify an output file"); + else + % Do nothing - no uiputfile on web app + obj.throwError("INTERNAL ERROR: Folder selection dialog not available on web app.") + return + end else - pathName = uigetdir(initialPath, "Select a folder"); - fileName = ""; + if obj.SelectionType == "file" + [fileName,pathName] = uigetfile(filter,"Select a file",initialPath); + elseif obj.SelectionType == "putfile" + [fileName,pathName] = uiputfile(filter,"Specify an output file",initialPath); + else + pathName = uigetdir(initialPath, "Select a folder"); + fileName = ""; + end end % Restore figure focus diff --git a/widgets/doc/LoginDialogExample.html b/widgets/doc/LoginDialogExample.html index 715d366..de1c1f4 100644 --- a/widgets/doc/LoginDialogExample.html +++ b/widgets/doc/LoginDialogExample.html @@ -40,9 +40,9 @@ .inlineElement .textElement {} .embeddedOutputsTextElement.rightPaneElement,.embeddedOutputsVariableStringElement.rightPaneElement { min-height: 16px;} .rightPaneElement .textElement { padding-top: 2px; padding-left: 9px;} -.S8 { border-left: 0.666667px solid rgb(217, 217, 217); border-right: 0.666667px solid rgb(217, 217, 217); border-top: 0.666667px solid rgb(217, 217, 217); border-bottom: 0.666667px solid rgb(217, 217, 217); border-radius: 0px 0px 4px 4px; padding: 6px 45px 4px 13px; line-height: 18.004px; min-height: 0px; white-space: nowrap; color: rgb(33, 33, 33); font-family: Menlo, Monaco, Consolas, "Courier New", monospace, Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 14px; }

Login Dialog

The Login dialog is intended to prompt a user to provide their username and password credentials.
Create a figure:
fig = uifigure;
fig.Position(3:4) = [500 300];
fig.Name = "Internal Dialog Example";
Create a modal login dialog:
dlg = wt.dialog.Login(fig);
dlg.Title = "Please log in";
Wait for output:
[output, lastAction] = dlg.waitForOutput()
output = struct with fields:
Login: "user" +.S8 { border-left: 0.666667px solid rgb(217, 217, 217); border-right: 0.666667px solid rgb(217, 217, 217); border-top: 0.666667px solid rgb(217, 217, 217); border-bottom: 0.666667px solid rgb(217, 217, 217); border-radius: 0px 0px 4px 4px; padding: 6px 45px 4px 13px; line-height: 18.004px; min-height: 0px; white-space: nowrap; color: rgb(33, 33, 33); font-family: Menlo, Monaco, Consolas, "Courier New", monospace, Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 14px; }

Login Dialog

The Login dialog is intended to prompt a user to provide their username and password credentials.
Create a figure:
fig = uifigure;
fig.Position(3:4) = [500 300];
fig.Name = "Internal Dialog Example";
Create a modal login dialog:
dlg = wt.dialog.Login(fig);
dlg.Title = "Please log in";
Wait for output:
[output, lastAction] = dlg.waitForOutput()
output = struct with fields:
Login: "user" Password: "1234" -
lastAction = "login"
close(fig)
Copyright 2025 The MathWorks, Inc.
+
lastAction = "login"
close(fig)
Copyright 2025 The MathWorks, Inc.