diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..60da6807 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,351 @@ +# Widgets Toolbox Repository Instructions + +## Project Purpose + +Widgets Toolbox provides reusable MATLAB UI components, application infrastructure, and development utilities. + +Prioritize: + +1. Reusable framework capabilities +2. API consistency +3. Backward compatibility +4. App Designer compatibility +5. Testability +6. Documentation quality + +Avoid widget-specific solutions when a reusable framework enhancement is possible. + +## Repository Philosophy + +Widgets Toolbox is primarily an application framework and UI infrastructure toolbox rather than a collection of isolated widgets. + +When evaluating a change, prefer solutions that: + +- Benefit multiple components. +- Reduce duplicate implementations. +- Improve extensibility. +- Improve maintainability. +- Improve consistency across the toolbox. + +Prefer framework-level solutions over widget-level solutions whenever practical. + +Before implementing a feature, consider whether the capability belongs: + +1. In a reusable utility. +2. In shared infrastructure. +3. In a mixin or base class. +4. In an individual widget. + +Choose the highest reusable layer that remains reasonable. + +Repository conventions are generally preferred over generic MATLAB recommendations. + +When a repository convention conflicts with a generic recommendation, follow the repository convention unless there is a compelling reason not to. + +## Preferred MATLAB Agentic Toolkit Skills + +When solving problems in this repository, prioritize guidance from: + +- matlab-programming +- matlab-software-development +- matlab-review-code +- matlab-build-app +- matlab-create-live-script + +Use domain-specific skills only when directly relevant. + +Avoid introducing solutions derived from unrelated domains unless explicitly requested. + +## Repository Structure + +- `widgets/+wt` contains toolbox source code. +- `widgets/examples` contains user-facing examples. +- `widgets/doc` contains user-facing documentation. +- `widgets/resources` contains Component Library metadata. +- `widgets/icons` contains static icon assets. +- `widgets/templates` contains application templates. +- `test` contains automated tests. +- `deploy` contains release packaging utilities. +- `release` contains released toolbox artifacts. + +Use existing patterns from neighboring files before introducing new conventions. + +## Copyright Notices + +Copyright notices are required in all source files, tests, examples, utilities, and documentation files. + +For MathWorks-owned files: + +- Ensure a copyright statement is present. +- Update the ending year whenever making code changes. +- Preserve existing copyright notices. +- Follow the format used by neighboring files. + +Example: + +```matlab +% Copyright 2019-2026 The MathWorks, Inc. +``` + +When modifying a MathWorks-owned file: + +- Verify that the copyright year range includes the current year. +- Update the ending year if necessary. +- Do not remove existing copyright notices. + +When creating a new MathWorks-owned file: + +- Add the standard MathWorks copyright notice. +- Match the format used by similar files in the repository. + +Exception: + +- If a file contains a third-party copyright notice that is not owned by MathWorks, preserve the existing copyright and licensing information. +- Do not replace, modify, remove, or augment third-party copyright statements unless explicitly requested. + +## Review Before Implementation + +Before implementing a solution: + +1. Search for similar functionality already present in the repository. +2. Search for reusable infrastructure. +3. Search for existing base classes, mixins, utilities, and services. +4. Consider whether multiple widgets could benefit from the change. +5. Minimize API surface area. + +Favor extending existing patterns over introducing new architectural approaches. + +Document significant architectural tradeoffs when they are not obvious. + +## Architecture Guidelines + +When adding functionality: + +- Search for existing widgets, base classes, mixins, utilities, and infrastructure before adding new code. +- Prefer extending reusable framework components over creating special-case implementations. +- Prefer composition over inheritance when either solution is reasonable. +- Reuse existing event, listener, and lifecycle patterns. +- Keep UI concerns separate from business logic when practical. +- Minimize coupling between components. +- Preserve public APIs unless a breaking change is explicitly requested. + +Before creating a new base class, verify that multiple components will benefit from it. + +Do not introduce framework abstractions that only serve a single use case. + +## Release Compatibility + +Widgets Toolbox supports multiple MATLAB releases. + +When introducing new language features: + +- Verify compatibility with supported releases. +- Do not assume the latest MATLAB release is available. +- Avoid unnecessary compatibility breaks. +- Flag compatibility risks explicitly. + +Prefer solutions that maintain behavior across supported MATLAB releases. + +## MATLAB Development Guidelines + +Follow existing repository conventions. + +When no convention exists: + +- Use PascalCase for class names. +- Use camelCase for methods, properties, variables, and events. +- Prefer clear and maintainable code over overly compact code. +- Use MATLAB Code Analyzer recommendations. +- Vectorize code when it improves performance without reducing readability. + +Prefer: + +- `arguments` blocks over `nargin` and `inputParser` when release compatibility permits. +- String arrays for new APIs when appropriate. +- Modern MATLAB language features when compatible with supported releases. + +Avoid introducing toolbox dependencies unless clearly justified. + +Before implementing custom functionality, verify that an equivalent MATLAB capability does not already exist. + +## Dependencies + +Favor base MATLAB implementations whenever practical. + +Before introducing: + +- Toolbox dependencies +- Java dependencies +- Third-party dependencies +- External services + +verify that existing toolbox infrastructure cannot provide an equivalent solution. + +New dependencies require strong justification. + +## Widget Development + +New widgets should normally include: + +- implementation +- automated tests +- help text +- example code +- Component Library metadata when applicable + +Maintain: + +- App Designer compatibility +- MATLAB Online compatibility when practical +- Web App compatibility when practical +- Existing widget behavior unless explicitly changing it + +Avoid undocumented graphics APIs unless they are already established within the repository. + +## App Designer Requirements + +Changes should preserve: + +- Component Library integration +- Design-time behavior +- Runtime behavior +- Property inspection support +- Existing App Designer workflows + +Do not introduce solutions that require users to bypass App Designer workflows unless unavoidable. + +## Performance + +Prioritize: + +1. UI responsiveness +2. Startup performance +3. Memory efficiency +4. Rendering performance + +Avoid premature optimization. + +Optimize when: + +- Performance issues are known. +- Measurements justify the change. +- The resulting implementation remains maintainable. + +## Testing + +Testing is part of implementation. + +When modifying functionality: + +- Add or update automated tests. +- Preserve existing test coverage. +- Verify new behavior with focused tests. +- Add regression tests when fixing bugs. + +Test file naming should mirror implementation structure. + +Example implementation: + +`widgets/+wt/FileSelector.m` + +Example test: + +`test/+wt/+test/FileSelector.m` + +When fixing a reported defect: + +1. Reproduce the issue with a test when practical. +2. Fix the issue. +3. Verify the new test passes. + +## Code Reviews + +When reviewing code, prioritize: + +1. Correctness defects +2. Behavioral regressions +3. Backward compatibility risks +4. Missing tests +5. API consistency +6. Reusability opportunities +7. Style and formatting concerns + +Additionally verify: + +- Copyright notices are present and current. +- New files include the appropriate copyright statement. +- Modified MathWorks-owned files have updated copyright years when required. + +Distinguish: + +- bug fixes +- compatibility fixes +- architectural improvements +- modernization opportunities + +Do not recommend unrelated modernization as part of another change. + +## Modernization + +Modernization is encouraged when it provides clear value. + +Do not: + +- Rewrite working code solely for style reasons. +- Replace established patterns without justification. +- Introduce large-scale refactoring during feature work. + +Prefer incremental improvements over architectural churn. + +## Documentation + +When public behavior changes: + +- Update help text. +- Update examples. +- Update documentation as needed. + +Document architectural decisions when they may not be obvious to future maintainers. + +Avoid comments that merely restate implementation details. + +Use comments primarily for: + +- intent +- assumptions +- constraints +- non-obvious design decisions + +## Release Expectations + +Before proposing a release: + +- Run relevant automated tests. +- Verify toolbox packaging succeeds. +- Update documentation if required. +- Update examples if required. + +Release packaging is performed using: + +`deploy/wtPackageRelease.m` + +## Source Control + +Keep changes focused on the requested task. + +Do not: + +- Modify unrelated files. +- Remove backward-compatible behavior without reason. +- Introduce large-scale refactoring during feature work. +- Replace existing patterns without understanding why they exist. + +Preserve user modifications and ongoing work. + +## Open Source Status + +This repository is public open-source software. + +Repository contents may be analyzed, summarized, reviewed, and quoted when appropriate. + +Do not introduce confidential customer information, internal project details, credentials, or proprietary code not already contained within the repository. \ No newline at end of file diff --git a/README.md b/README.md index 8d1d7b5b..2a63a440 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,8 @@ Additional object-oriented code components provide enable you to work more effic This version of Widgets Toolbox is intended for NEW development of uifigure or App Designer apps starting from R2021a or newer releases. +Maintainers build and package releases in the latest MATLAB release using MATLAB Build Tool via `buildtool archive`. That maintainer workflow is separate from the packaged toolbox support range above. + If you have an existing MATLAB app using Widgets Toolbox content developed prior to R2020b, you may also need "Widgets Toolbox (Compatibility Support)". https://www.mathworks.com/matlabcentral/fileexchange/66235-widgets-toolbox-compatibility-support https://github.com/mathworks/widgets-toolbox-legacy diff --git a/buildfile.m b/buildfile.m index a4bf63bf..12347dc0 100644 --- a/buildfile.m +++ b/buildfile.m @@ -1,12 +1,13 @@ function plan = buildfile -% Perform build, test, and package actions +% Define maintainer build, test, and release tasks. -% Copyright 2025 The MathWorks, Inc. +% Copyright 2025-2026 The MathWorks, Inc. -% These tasks require R2023b or later +% Maintainers run the build in the latest MATLAB release. The packaged +% toolbox output still declares support for MATLAB R2021a and later. if isMATLABReleaseOlderThan("R2023b") - error("R2023b or later is required for build tools.") + error("R2023b or later is required to run Build Tool tasks.") end @@ -21,40 +22,143 @@ plan("check").SourceFiles = fullfile(plan.RootFolder,"widgets"); plan("check").WarningThreshold = 0; -% Test task -plan("test") = matlab.buildtool.tasks.TestTask; -plan("test").SourceFiles = fullfile(plan.RootFolder,"test"); +% Release workflow +plan("prepareRelease").Dependencies = "test"; +plan("publishDocHtml").Dependencies = "prepareRelease"; +plan("publishExampleHtml").Dependencies = "prepareRelease"; +plan("publishGettingStarted").Dependencies = "prepareRelease"; +plan("buildDocSearchDb").Dependencies = ... + ["publishDocHtml","publishExampleHtml","publishGettingStarted"]; +plan("package").Dependencies = ["check","buildDocSearchDb"]; +plan("finalizeRelease").Dependencies = "package"; -% Package task -plan("archive").Dependencies = ["check","test"]; +% Top-level release aggregation task +plan("archive").Dependencies = "finalizeRelease"; % Set default tasks plan.DefaultTasks = ["check","test"]; -end %function +end + + +function testTask(context) +% Run the release test gate. + +rootFolder = string(context.Plan.RootFolder); +ensureProjectLoaded(rootFolder); +wt.deploy.runReleaseTests(); +end -function archiveTask(context) -% Package the mltbx file -% Get root -rootFolder = context.Plan.RootFolder; +function prepareReleaseTask(context) +% Perform release preparation ahead of documentation generation. -% Increment the last part of the version number in wtDeployVersion.txt -% (Other changes require commenting this out and making manual edits) +rootFolder = string(context.Plan.RootFolder); +ensureProjectLoaded(rootFolder); wt.deploy.incrementVersionNumber(); -% Increment the last part of the version number in version.txt -toolboxVersion = wt.deploy.readVersionNumber(); +end -% Read in the package options -opts = wt.deploy.getPackageOptions(rootFolder, toolboxVersion); -% Perform the packaging -matlab.addons.toolbox.packageToolbox(opts); +function publishDocHtmlTask(context) +% Publish widgets/doc live scripts to HTML. + +rootFolder = string(context.Plan.RootFolder); +ensureProjectLoaded(rootFolder); +wt.deploy.publishReleaseDocumentation(rootFolder); + +end + + +function publishExampleHtmlTask(context) +% Publish widgets/examples live scripts to HTML. + +rootFolder = string(context.Plan.RootFolder); +ensureProjectLoaded(rootFolder); +wt.deploy.publishReleaseExamples(rootFolder); + +end + + +function publishGettingStartedTask(context) +% Publish GettingStarted.mlx to HTML. + +rootFolder = string(context.Plan.RootFolder); +ensureProjectLoaded(rootFolder); +wt.deploy.publishGettingStartedHtml(rootFolder); + +end + + +function buildDocSearchDbTask(context) +% Rebuild the documentation search database. + +rootFolder = string(context.Plan.RootFolder); +ensureProjectLoaded(rootFolder); +wt.deploy.buildDocumentationSearchDb(rootFolder); + +end + + +function packageTask(context) +% Package the toolbox release. + +rootFolder = string(context.Plan.RootFolder); +ensureProjectLoaded(rootFolder); +wt.deploy.packageRelease(rootFolder); -% Open the release folder -releaseFolder = fullfile(rootFolder,"release"); -winopen(releaseFolder); +end + + +function finalizeReleaseTask(context) +% Perform legacy release finalization. + +rootFolder = string(context.Plan.RootFolder); +ensureProjectLoaded(rootFolder); +outputFile = getExpectedReleaseOutputFile(rootFolder); +wt.deploy.finalizeRelease(rootFolder, outputFile); + +end + + +function archiveTask(~) +% Aggregate the full release workflow. + +end + + +function ensureProjectLoaded(rootFolder) +% Open the MATLAB project so deploy helpers and resources are available. + +arguments + rootFolder (1,1) string +end + +projectFile = fullfile(rootFolder, "WidgetsToolbox.prj"); -end %function \ No newline at end of file +try + project = currentProject; +catch + project = []; +end + +if isempty(project) || string(project.RootFolder) ~= rootFolder + openProject(projectFile); +end + +end + + +function outputFile = getExpectedReleaseOutputFile(rootFolder) +% Return the expected package output file for the current deploy version. + +arguments + rootFolder (1,1) string +end + +toolboxVersion = wt.deploy.readVersionNumber(); +opts = wt.deploy.getPackageOptions(rootFolder, toolboxVersion); +outputFile = string(opts.OutputFile); + +end diff --git a/deploy/+wt/+deploy/buildDocumentationSearchDb.m b/deploy/+wt/+deploy/buildDocumentationSearchDb.m new file mode 100644 index 00000000..0976ac75 --- /dev/null +++ b/deploy/+wt/+deploy/buildDocumentationSearchDb.m @@ -0,0 +1,15 @@ +function buildDocumentationSearchDb(projectRoot) +% Build the documentation search database for the release docs. +% +% Copyright 2026 The MathWorks, Inc. + +arguments + projectRoot (1,1) string {mustBeFolder} +end + + +% The search database is built in the maintainer MATLAB release. +releaseContext = wt.deploy.getReleaseContext(projectRoot); +builddocsearchdb(releaseContext.DocOutputPath); + +end diff --git a/deploy/+wt/+deploy/finalizeRelease.m b/deploy/+wt/+deploy/finalizeRelease.m new file mode 100644 index 00000000..82a3c0e6 --- /dev/null +++ b/deploy/+wt/+deploy/finalizeRelease.m @@ -0,0 +1,28 @@ +function finalizeRelease(projectRoot, outputFile) +% Add the packaged toolbox to the project and open the release folder. +% +% Copyright 2026 The MathWorks, Inc. + +arguments + projectRoot (1,1) string {mustBeFolder} + outputFile (1,1) string {mustBeFile} +end + + +% Resolve the release context for project and folder side effects. +releaseContext = wt.deploy.getReleaseContext(projectRoot); + + +% Keep the legacy project update behavior for generated installers. +project = releaseContext.Project; +if isempty(project) + error("wt:deploy:ProjectRequired", ... + "Open WidgetsToolbox.prj before finalizing the release."); +end +project.addFile(outputFile); + + +% Preserve the legacy UI behavior after packaging completes. +winopen(releaseContext.ReleaseFolder); + +end diff --git a/deploy/+wt/+deploy/getPackageOptions.m b/deploy/+wt/+deploy/getPackageOptions.m index 4f24b3a3..d355ef2b 100644 --- a/deploy/+wt/+deploy/getPackageOptions.m +++ b/deploy/+wt/+deploy/getPackageOptions.m @@ -1,7 +1,7 @@ function opts = getPackageOptions(projectRoot, toolboxVersion) % Returns the toolbox packaging options -% Copyright 2025 The MathWorks, Inc. +% Copyright 2025-2026 The MathWorks, Inc. arguments %(Input) projectRoot (1,1) string {mustBeFolder} @@ -31,13 +31,15 @@ %% Requirements +% Maintainers build the toolbox in the latest MATLAB release, but the +% packaged toolbox output continues to support MATLAB R2021a and later. opts.MinimumMatlabRelease = "R2021a"; opts.MaximumMatlabRelease = ""; opts.SupportedPlatforms.Win64 = true; opts.SupportedPlatforms.Glnxa64 = true; -opts.SupportedPlatforms.Maci64 = true; +opts.SupportedPlatforms.Mac = true; opts.SupportedPlatforms.MatlabOnline = true; % opts.RequiredAddons @@ -77,9 +79,8 @@ % opts.ToolboxJavaPath % Files to add to the app gallery -opts.AppGalleryFiles = [ - fullfile(toolboxFolder, "examples","WidgetsExampleApp.mlapp") - ]; +opts.AppGalleryFiles = ... + fullfile(toolboxFolder, "examples", "WidgetsExampleApp.mlapp"); % Path to the getting started guide opts.ToolboxGettingStartedGuide = fullfile(toolboxFolder,... @@ -145,4 +146,4 @@ opts.AuthorCompany = "MathWorks Consulting"; -opts.ToolboxImageFile = fullfile(projectRoot,"deploy","wtLogo.png"); \ No newline at end of file +opts.ToolboxImageFile = fullfile(projectRoot,"deploy","wtLogo.png"); diff --git a/deploy/+wt/+deploy/getReleaseContext.m b/deploy/+wt/+deploy/getReleaseContext.m new file mode 100644 index 00000000..9fe751d0 --- /dev/null +++ b/deploy/+wt/+deploy/getReleaseContext.m @@ -0,0 +1,40 @@ +function releaseContext = getReleaseContext(projectRoot) +% Return shared paths and project data for release tasks. +% +% Copyright 2026 The MathWorks, Inc. + +arguments + projectRoot (1,1) string = "" +end + + +% Resolve the active project when the caller does not provide a root. +project = []; +try + project = currentProject; +catch +end + +if strlength(projectRoot) == 0 + if isempty(project) + error("wt:deploy:ProjectRequired", ... + "Open WidgetsToolbox.prj before running release tasks."); + end + projectRoot = string(project.RootFolder); +end + + +% Capture the shared paths used by the release workflow. +releaseContext = struct( ... + "Project", project, ... + "ProjectRoot", projectRoot, ... + "ProjectFile", fullfile(projectRoot, "WidgetsToolbox.prj"), ... + "DocInputPath", fullfile(projectRoot, "widgets", "doc"), ... + "DocOutputPath", fullfile(projectRoot, "widgets", "doc"), ... + "ExamplesInputPath", fullfile(projectRoot, "widgets", "examples"), ... + "ExamplesOutputPath", fullfile(projectRoot, "widgets", "doc"), ... + "GettingStartedInputPath", fullfile(projectRoot, "widgets", "doc", "GettingStarted.mlx"), ... + "GettingStartedOutputPath", fullfile(projectRoot, "widgets", "doc"), ... + "ReleaseFolder", fullfile(projectRoot, "release")); + +end diff --git a/deploy/+wt/+deploy/packageRelease.m b/deploy/+wt/+deploy/packageRelease.m new file mode 100644 index 00000000..5d53210f --- /dev/null +++ b/deploy/+wt/+deploy/packageRelease.m @@ -0,0 +1,24 @@ +function outputFile = packageRelease(projectRoot) +% Package the toolbox release and return the output file path. +% +% Copyright 2026 The MathWorks, Inc. + +arguments + projectRoot (1,1) string {mustBeFolder} +end + + +% Build the .mltbx using the current deploy version number. +toolboxVersion = wt.deploy.readVersionNumber(); +opts = wt.deploy.getPackageOptions(projectRoot, toolboxVersion); +outputFile = opts.OutputFile; +matlab.addons.toolbox.packageToolbox(opts); + + +% Fail immediately if packaging did not create the expected artifact. +if ~isfile(outputFile) + error("wt:deploy:PackageNotCreated", ... + "Toolbox packaging completed without creating %s.", outputFile); +end + +end diff --git a/deploy/+wt/+deploy/publishGettingStartedHtml.m b/deploy/+wt/+deploy/publishGettingStartedHtml.m new file mode 100644 index 00000000..9212ea30 --- /dev/null +++ b/deploy/+wt/+deploy/publishGettingStartedHtml.m @@ -0,0 +1,17 @@ +function publishGettingStartedHtml(projectRoot) +% Publish the getting started live script to HTML. +% +% Copyright 2026 The MathWorks, Inc. + +arguments + projectRoot (1,1) string {mustBeFolder} +end + + +% Keep the standalone getting started publish step explicit. +releaseContext = wt.deploy.getReleaseContext(projectRoot); +wt.deploy.publishLiveScriptToHtml( ... + releaseContext.GettingStartedInputPath, ... + releaseContext.GettingStartedOutputPath); + +end diff --git a/deploy/+wt/+deploy/publishReleaseDocumentation.m b/deploy/+wt/+deploy/publishReleaseDocumentation.m new file mode 100644 index 00000000..9a9706ad --- /dev/null +++ b/deploy/+wt/+deploy/publishReleaseDocumentation.m @@ -0,0 +1,18 @@ +function publishReleaseDocumentation(projectRoot) +% Publish widget documentation live scripts to HTML. +% +% Copyright 2026 The MathWorks, Inc. + +arguments + projectRoot (1,1) string {mustBeFolder} +end + + +% Publish all live scripts from widgets/doc back into widgets/doc. +releaseContext = wt.deploy.getReleaseContext(projectRoot); +docInputInfo = what(releaseContext.DocInputPath); +docInputFiles = reshape(string(docInputInfo.mlx), [], 1); +docInputFiles = fullfile(releaseContext.DocInputPath, docInputFiles); +wt.deploy.publishLiveScriptToHtml(docInputFiles, releaseContext.DocOutputPath); + +end diff --git a/deploy/+wt/+deploy/publishReleaseExamples.m b/deploy/+wt/+deploy/publishReleaseExamples.m new file mode 100644 index 00000000..44532181 --- /dev/null +++ b/deploy/+wt/+deploy/publishReleaseExamples.m @@ -0,0 +1,18 @@ +function publishReleaseExamples(projectRoot) +% Publish example live scripts to the toolbox documentation folder. +% +% Copyright 2026 The MathWorks, Inc. + +arguments + projectRoot (1,1) string {mustBeFolder} +end + + +% Publish all live scripts from widgets/examples into widgets/doc. +releaseContext = wt.deploy.getReleaseContext(projectRoot); +examplesInputInfo = what(releaseContext.ExamplesInputPath); +examplesInputFiles = reshape(string(examplesInputInfo.mlx), [], 1); +examplesInputFiles = fullfile(releaseContext.ExamplesInputPath, examplesInputFiles); +wt.deploy.publishLiveScriptToHtml(examplesInputFiles, releaseContext.ExamplesOutputPath); + +end diff --git a/deploy/+wt/+deploy/runReleaseTests.m b/deploy/+wt/+deploy/runReleaseTests.m new file mode 100644 index 00000000..0e058c86 --- /dev/null +++ b/deploy/+wt/+deploy/runReleaseTests.m @@ -0,0 +1,17 @@ +function [testSuite, testResult] = runReleaseTests() +% Run the release test gate using the legacy test suite entry point. +% +% Copyright 2026 The MathWorks, Inc. + + +% Keep the legacy release test selection and reporting behavior. +[testSuite, testResult] = runTestSuite(); + + +% Abort the release if any test did not pass. +if ~all([testResult.Passed]) + error("wt:deploy:ReleaseTestsFailed", ... + "Unit tests failed. Aborting package release."); +end + +end diff --git a/deploy/wtPackageRelease.m b/deploy/wtPackageRelease.m index ee93ef27..c49d84e1 100644 --- a/deploy/wtPackageRelease.m +++ b/deploy/wtPackageRelease.m @@ -1,89 +1,12 @@ % Package a Toolbox Release +% +% Preferred maintainer entry point: buildtool archive -% Copyright 2025 The MathWorks, Inc. +% Copyright 2025-2026 The MathWorks, Inc. -%% Increment Version Number? +%% Open project and run the Build Tool release workflow -% Increment the last part of the version number in wtDeployVersion.txt for -% the next release -wt.deploy.incrementVersionNumber(); - - -%% Get paths - -% Project root -proj = currentProject; -projectRoot = proj.RootFolder; - -docInputPath = fullfile(projectRoot,"widgets","doc"); -docOutputPath = fullfile(projectRoot,"widgets","doc"); - -examplesInputPath = fullfile(projectRoot,"widgets","examples"); -examplesOutputPath = fullfile(projectRoot,"widgets","doc"); - -gettingStartedInputPath = fullfile(projectRoot,"widgets","doc","GettingStarted.mlx"); -gettingStartedOutputPath = fullfile(projectRoot,"widgets","doc"); - - -%% Run unit tests -[testSuite, testResult] = runTestSuite; -if ~all([testResult.Passed]) - error("Unit tests failed. Aborting package release."); -end - - -%% Publish doc and examples mlx as html - -% Find the doc input ".mlx" files -docInputInfo = what(docInputPath); -docInputFiles = docInputInfo.mlx; -docInputFiles = fullfile(docInputPath, docInputFiles); - -% Publish these to HTML output -wt.deploy.publishLiveScriptToHtml(docInputFiles, docOutputPath) - - -%% Publish examples as html - -% Find the examples ".mlx" files -examplesInputInfo = what(examplesInputPath); -examplesInputFiles = examplesInputInfo.mlx; -examplesInputFiles = fullfile(examplesInputPath, examplesInputFiles); - -% Publish these to HTML output -wt.deploy.publishLiveScriptToHtml(examplesInputFiles, examplesOutputPath) - - -%% Publish GettingStarted.mlx as html - -% Publish to HTML output -wt.deploy.publishLiveScriptToHtml(gettingStartedInputPath, gettingStartedOutputPath) - - -%% Build search database -% Create v3 for/from R2021a - R2021b -% Create v4 for/from R2022a and later -builddocsearchdb(docOutputPath) - - -%% Package the Release - -% Increment the last part of the version number in version.txt -toolboxVersion = wt.deploy.readVersionNumber(); - -% Read in the package options -opts = wt.deploy.getPackageOptions(projectRoot, toolboxVersion); -outputFile = opts.OutputFile; - -% Perform the packaging -matlab.addons.toolbox.packageToolbox(opts); - -% Add the installer to the project -proj.addFile(outputFile) - - -%% Open the release folder - -releaseFolder = fullfile(projectRoot,"release"); -winopen(releaseFolder); +projectRoot = fileparts(fileparts(mfilename("fullpath"))); +openProject(fullfile(projectRoot,"WidgetsToolbox.prj")); +buildtool("archive"); diff --git a/docs/development/build-tool-migration-plan.md b/docs/development/build-tool-migration-plan.md new file mode 100644 index 00000000..8a8aae0c --- /dev/null +++ b/docs/development/build-tool-migration-plan.md @@ -0,0 +1,409 @@ +# Build Tool Migration Plan + + + +## Goal + +Integrate the existing release workflow from `deploy/wtPackageRelease.m` into MATLAB Build Tool so that: + +- `buildfile.m` is the primary entry point +- `deploy/wtPackageRelease.m` becomes a thin compatibility wrapper +- existing release behavior is preserved +- release outputs do not change +- code churn stays low + +## Version Policy + +The repository should document two distinct version concepts: + +- maintainer build and release environment: latest MATLAB release +- shipped toolbox runtime support: `R2021a` or later + +These are intentionally different: + +- maintainers use the latest MATLAB release to run Build Tool and perform packaging +- toolbox consumers install and use the packaged release on supported MATLAB releases back to `R2021a` + +This distinction should be stated consistently in: + +- `buildfile.m` comments +- contributor-facing documentation such as `README.md` +- packaging code comments near `opts.MinimumMatlabRelease = "R2021a"` +- migration notes for the Build Tool rollout + +## Current State + +`deploy/wtPackageRelease.m` currently performs the full release pipeline: + +1. Increment deploy version via `wt.deploy.incrementVersionNumber` +2. Resolve project root from `currentProject` +3. Run unit tests via `runTestSuite` +4. Publish `widgets/doc/*.mlx` to HTML in `widgets/doc` +5. Publish `widgets/examples/*.mlx` to HTML in `widgets/doc` +6. Publish `widgets/doc/GettingStarted.mlx` to HTML +7. Rebuild the documentation search database in `widgets/doc` +8. Build the toolbox package via `wt.deploy.getPackageOptions` and `matlab.addons.toolbox.packageToolbox` +9. Add the generated `.mltbx` to the project +10. Open the `release` folder + +`buildfile.m` currently provides: + +- `check` via `CodeIssuesTask` +- `test` via `TestTask` +- `archive` as a function task depending on `check` and `test` + +It does not yet cover documentation publishing, search database generation, adding the installer to the project, or preserving the exact legacy test entry point. + +The current repository also leaves the version policy implicit: + +- `buildfile.m` currently states a Build Tool requirement of `R2023b+` +- package metadata declares a minimum supported MATLAB release of `R2021a` + +The migration should replace that ambiguity with an explicit maintainer-versus-runtime support statement. + +## Proposed Build Task Graph + +Use `buildfile.m` as the orchestration layer and keep task names close to the current workflow. + +```text +check +test +prepareRelease +publishDocHtml +publishExampleHtml +publishGettingStarted +buildDocSearchDb +package +finalizeRelease +archive +``` + +Recommended dependencies: + +- `check`: no dependencies +- `test`: no dependencies initially +- `prepareRelease`: depends on `test` +- `publishDocHtml`: depends on `prepareRelease` +- `publishExampleHtml`: depends on `prepareRelease` +- `publishGettingStarted`: depends on `prepareRelease` +- `buildDocSearchDb`: depends on `publishDocHtml`, `publishExampleHtml`, `publishGettingStarted` +- `package`: depends on `check`, `buildDocSearchDb` +- `finalizeRelease`: depends on `package` +- `archive`: depends on `finalizeRelease` + +Recommended default tasks: + +- keep `DefaultTasks = ["check","test"]` + +Recommended release invocation: + +- `buildtool archive` + +Rationale: + +- preserves the current default lightweight developer flow +- makes the full release flow explicit +- isolates side-effecting steps into small task functions +- avoids changing packaged artifacts or release folder contents + +## Functions To Extract + +Move the release implementation into reusable helpers under `deploy/+wt/+deploy/` and let `buildfile.m` own the orchestration. + +Recommended extracted functions: + +### `deploy/+wt/+deploy/getReleaseContext.m` + +Purpose: + +- centralize `currentProject` lookup +- compute `projectRoot` +- compute doc/examples/getting-started input and output paths + +Why: + +- these paths are currently assembled inline in `wtPackageRelease.m` +- `buildfile.m` and the wrapper script will need identical path resolution + +### `deploy/+wt/+deploy/runReleaseTests.m` + +Purpose: + +- wrap the current `runTestSuite` behavior +- preserve pass/fail semantics from `wtPackageRelease.m` + +Why: + +- `runTestSuite` is the current release gate +- using a wrapper avoids changing release behavior while allowing Build Tool to call the same implementation + +### `deploy/+wt/+deploy/publishReleaseDocumentation.m` + +Purpose: + +- publish `widgets/doc/*.mlx` + +Why: + +- isolates one existing workflow block with no behavior change + +### `deploy/+wt/+deploy/publishReleaseExamples.m` + +Purpose: + +- publish `widgets/examples/*.mlx` to `widgets/doc` + +Why: + +- preserves the current output location and side effects + +### `deploy/+wt/+deploy/publishGettingStartedHtml.m` + +Purpose: + +- publish `widgets/doc/GettingStarted.mlx` + +Why: + +- keeps the special-case behavior explicit + +### `deploy/+wt/+deploy/buildDocumentationSearchDb.m` + +Purpose: + +- wrap `builddocsearchdb(docOutputPath)` + +Why: + +- separates a release-specific side effect into a dedicated build step + +### `deploy/+wt/+deploy/packageRelease.m` + +Purpose: + +- read version +- build options +- call `matlab.addons.toolbox.packageToolbox` +- return output file path + +Why: + +- this is the core packaging step already split across `wtPackageRelease.m` and `getPackageOptions.m` + +### `deploy/+wt/+deploy/finalizeRelease.m` + +Purpose: + +- add installer to project +- open release folder + +Why: + +- preserves legacy release behavior +- keeps UI/project side effects out of the package step + +## New File Structure + +Proposed minimal structure: + +```text +buildfile.m +deploy/ + wtPackageRelease.m + +wt/ + +deploy/ + buildDocumentationSearchDb.m + finalizeRelease.m + getPackageOptions.m + getReleaseContext.m + incrementVersionNumber.m + packageRelease.m + publishGettingStartedHtml.m + publishLiveScriptToHtml.m + publishReleaseDocumentation.m + publishReleaseExamples.m + runReleaseTests.m +``` + +Notes: + +- keep `runTestSuite.m` in place for backward compatibility +- `buildfile.m` should call the extracted functions directly and own task sequencing +- `wtPackageRelease.m` should become a thin wrapper for backward compatibility only +- no release logic should be duplicated between `buildfile.m` and `wtPackageRelease.m` + +## Proposed Responsibility Split + +### `buildfile.m` + +Owns: + +- task definitions +- task dependencies +- release entry point selection +- Build Tool version gating +- sequencing of the release workflow +- calling the extracted `wt.deploy.*` helpers + +Does not own: + +- path assembly details +- documentation publishing logic +- packaging logic +- project side effects + +### `deploy/wtPackageRelease.m` + +Owns: + +- legacy script entry point only +- either forwarding to `buildtool archive` or invoking a single shared release entry helper if direct Build Tool invocation is not appropriate + +Does not own: + +- task graph definition +- inline implementation of release steps + +### `deploy/+wt/+deploy/*` + +Owns: + +- implementation of each release step +- shared behavior used by both script and Build Tool + +## Migration Plan + +### Phase 1: Extract Without Behavioral Change + +1. Add `getReleaseContext` +2. Add `runReleaseTests` +3. Add documentation publishing wrappers +4. Add `buildDocumentationSearchDb` +5. Add `packageRelease` +6. Add `finalizeRelease` +7. Rewrite `deploy/wtPackageRelease.m` to become a thin wrapper over the shared release implementation + +Success criteria: + +- running `deploy/wtPackageRelease.m` yields the same `.mltbx`, HTML outputs, search database updates, project update, and release-folder open behavior as before + +### Phase 2: Wire Build Tool To The Same Implementation + +1. Update `buildfile.m` to define release tasks +2. Make each release task call the extracted `wt.deploy.*` helper +3. Keep `archive` as the top-level release task +4. Preserve current default tasks unless there is an explicit decision to change developer workflow +5. Make `buildtool archive` the documented primary release command +6. Update maintainer-facing comments and docs so they explicitly state that builds run in the latest MATLAB release while packaged toolbox support remains `R2021a+` + +Success criteria: + +- `buildtool archive` performs the same release workflow as `deploy/wtPackageRelease.m` + +### Phase 3: Compatibility Validation + +1. Compare generated release artifact name and location +2. Compare generated HTML files and search DB side effects +3. Confirm version increment behavior is unchanged +4. Confirm project update still occurs +5. Confirm release-folder open behavior still occurs + +Success criteria: + +- no external release output changes + +### Phase 4: Deprecation Posture + +1. Retain `deploy/wtPackageRelease.m` as a supported wrapper +2. Add a short header comment indicating `buildtool archive` is now the preferred entry point +3. Keep the wrapper minimal enough that future release changes are made only in `buildfile.m` and `deploy/+wt/+deploy/` +4. Do not remove the wrapper in the same change set + +Success criteria: + +- existing users of the script are not broken + +## Risks And Compatibility Concerns + +### Release Behavior Drift + +Risk: + +- switching from `runTestSuite` to pure `TestTask` may alter selected tests, reporting, or failure semantics + +Mitigation: + +- keep `runReleaseTests` backed by `runTestSuite` initially +- treat `TestTask` as a developer-facing test entry point, not the authoritative release gate until equivalence is proven + +### Build Tool Release Gating + +Risk: + +- maintainers may interpret the Build Tool MATLAB requirement as the supported runtime floor for toolbox users + +Mitigation: + +- document explicitly that releases are built in the latest MATLAB release, while toolbox support for consumers remains `R2021a+` +- update `buildfile.m`, `README.md`, and packaging comments to reflect that split consistently +- keep `wtPackageRelease.m` available as a compatibility wrapper during migration + +### Project Context Assumptions + +Risk: + +- `wtPackageRelease.m` currently assumes `currentProject` is loaded + +Mitigation: + +- make `getReleaseContext` validate project availability and fail with a targeted message +- do not silently change path behavior + +### Duplicate Side Effects + +Risk: + +- publishing documentation or incrementing the version in multiple tasks can create inconsistent state if tasks are invoked independently + +Mitigation: + +- confine version increment to `prepareRelease` +- document that `archive` is the supported full-release entry point +- keep intermediate tasks available for diagnostics but not as the normal release command +- keep `wtPackageRelease.m` thin so it cannot diverge from `buildfile.m` + +### Output Drift + +Risk: + +- reorganizing helpers could accidentally change output locations, timestamps, or package metadata + +Mitigation: + +- keep `getPackageOptions` unchanged initially +- keep doc and example output paths unchanged +- use wrapper functions around existing calls instead of rewriting logic + +### UI Side Effects In Automation + +Risk: + +- `winopen(releaseFolder)` and `proj.addFile(outputFile)` may be undesirable in CI or headless use + +Mitigation: + +- preserve them initially to meet the no-output-change requirement +- defer any optional headless mode until after migration parity is established + +## Recommended First Implementation Slice + +The lowest-risk first change is: + +1. Extract `getReleaseContext` +2. Extract `runReleaseTests` +3. Extract `packageRelease` +4. Add the remaining doc publishing and search DB helpers +5. Update `buildfile.m` so `archive` owns the full release workflow through those helpers +6. Convert `wtPackageRelease.m` into a thin wrapper + +This sequence keeps the release flow centered in `buildfile.m`, keeps code churn local to `deploy/+wt/+deploy/`, and avoids changing package outputs while the migration is underway. diff --git a/docs/reviews/2026-07-maintainer-issue-drafts.md b/docs/reviews/2026-07-maintainer-issue-drafts.md new file mode 100644 index 00000000..9a48b697 --- /dev/null +++ b/docs/reviews/2026-07-maintainer-issue-drafts.md @@ -0,0 +1,280 @@ +# 2026-07 Maintainer Issue Drafts + +These issue drafts are derived from [2026-07-maintainer-review.md](/c:/AI/widgets-toolbox/docs/reviews/2026-07-maintainer-review.md). + +GitHub issue creation was not completed from this session because `gh auth status` reports an invalid token for the configured account. The drafts below are ready to file manually. + +## 1. Clarify supported MATLAB versions for runtime, build, and CI + +**Title**: Clarify and document the supported MATLAB version matrix + +**Summary** + +The repository currently presents conflicting version expectations. `README.md` says the toolbox is intended for new development from `R2021a` onward, while `buildfile.m` requires `R2023b` or later for build tasks. That difference may be intentional, but it is not documented clearly enough for maintainers or contributors. + +**Affected files** + +- `README.md` +- `buildfile.m` +- `.github/workflows/git-ci.yml` + +**Why this matters** + +- Contributors cannot tell whether CI/build requirements are stricter than runtime requirements by design. +- Release support statements are part of the public contract. +- Version confusion increases support churn and complicates triage for compatibility bugs. + +**Acceptance criteria** + +- The repository documents runtime support separately from build/test support. +- `README.md` states the supported release range unambiguously. +- `buildfile.m` comments explain why `R2023b+` is required for build tasks, if that remains true. +- CI configuration is consistent with the documented support policy. + +## 2. Add direct unit coverage for `wt.model.BaseModel` + +**Title**: Add direct regression tests for `wt.model.BaseModel` + +**Summary** + +`widgets/+wt/+model/BaseModel.m` is a core framework class with no direct same-name test in `test/+wt/+test`. It handles observable properties, listener setup, recursive aggregated-model notifications, copy behavior, and load-related repair logic. + +**Affected files** + +- `widgets/+wt/+model/BaseModel.m` +- `test/+wt/+test` + +**Why this matters** + +- `BaseModel` is central infrastructure and a single regression can break multiple apps and widgets. +- Current coverage appears indirect rather than explicit. +- Event ordering and listener cleanup are high-risk areas that are difficult to debug after the fact. + +**Acceptance criteria** + +- A new class-based unit test directly targets `wt.model.BaseModel` behavior. +- Tests cover `PropertyChanged` notifications for observable properties. +- Tests cover aggregated or nested model change propagation. +- Tests cover copy or lifecycle behavior that could leave stale listeners behind. +- Tests follow repository conventions and pass in the full suite. + +## 3. Add direct tests for dialog base classes + +**Title**: Add direct tests for `BaseInternalDialog` and `BaseExternalDialog` + +**Summary** + +Dialog behavior is exercised indirectly through `test/+wt/+test/ListSelectionDialog.m`, but the framework base classes themselves do not have direct same-name tests. These classes are large, duplicated, and marked as prototype components. + +**Affected files** + +- `widgets/+wt/+abstract/BaseInternalDialog.m` +- `widgets/+wt/+abstract/BaseExternalDialog.m` +- `test/+wt/+test` + +**Why this matters** + +- Base dialog regressions can affect every dialog subclass. +- Modal behavior, button state, cleanup, and close actions should be protected directly. +- Shared behaviors are harder to refactor safely without targeted tests. + +**Acceptance criteria** + +- New tests directly exercise each base dialog path. +- Tests cover button configuration and `DialogButtonPushed` behavior. +- Tests cover modal and non-modal behavior. +- Tests cover close/delete lifecycle and cleanup. +- Tests verify no warnings on basic creation and teardown paths. + +## 4. Expand `BaseTimeAlignedChart` regression coverage + +**Title**: Expand test coverage for `BaseTimeAlignedChart` + +**Summary** + +`widgets/+wt/+abstract/BaseTimeAlignedChart.m` appears to be covered only indirectly by the smoke-style test in `test/+wt/+test/BaseViewChart.m`. The class manages axes count, selection, legends, grid state, colors, labels, and dependent limit properties. + +**Affected files** + +- `widgets/+wt/+abstract/BaseTimeAlignedChart.m` +- `test/+wt/+test/BaseViewChart.m` +- `test/+wt/+test` + +**Why this matters** + +- Chart layout and axes state are typically sensitive to regressions. +- The class is a reusable framework piece rather than a one-off example. +- Smoke coverage is not enough for a major release boundary. + +**Acceptance criteria** + +- Tests cover `NumAxes` changes and resulting axes layout updates. +- Tests cover `ShowGrid`, `ShowLegend`, and selected-axes behavior. +- Tests cover dependent properties such as `YLim` and `YLimMode`. +- Tests verify representative label and color propagation behavior. + +## 5. Isolate version and theme compatibility logic + +**Title**: Introduce a compatibility layer for MATLAB release gates and theme access + +**Summary** + +Version gates and internal theme API usage are scattered across framework classes. Centralizing that logic would reduce release churn and lower the risk of inconsistencies across widgets. + +**Affected files** + +- `widgets/+wt/+apps/BaseApp.m` +- `widgets/+wt/+abstract/BaseWidget.m` +- `widgets/+wt/+abstract/BaseViewController.m` +- `widgets/+wt/DropDownListManager.m` +- `widgets/+wt/SearchDropDown.m` +- `widgets/+wt/ListSelector.m` +- `widgets/+wt/ContextualView.m` + +**Why this matters** + +- `isMATLABReleaseOlderThan(...)` branches are distributed across unrelated components. +- `matlab.graphics.internal.themes.getAttributeValue(...)` introduces dependence on an internal MATLAB API. +- Compatibility work should be isolated rather than reimplemented ad hoc. + +**Acceptance criteria** + +- A dedicated internal compatibility utility or package is introduced. +- Existing release gates are reduced or routed through the shared utility. +- Theme access is wrapped behind a single abstraction with clear fallback behavior. +- Existing tests continue to pass after the compatibility logic is centralized. + +## 6. Extract shared dialog core from internal and external dialog bases + +**Title**: Refactor dialog framework to share a common dialog core + +**Summary** + +`BaseInternalDialog` and `BaseExternalDialog` implement similar concepts with duplicated logic. The duplicated surface makes future fixes and enhancements expensive and error-prone. + +**Affected files** + +- `widgets/+wt/+abstract/BaseInternalDialog.m` +- `widgets/+wt/+abstract/BaseExternalDialog.m` +- `widgets/+wt/+dialog/ListSelection.m` + +**Why this matters** + +- Bug fixes can land in one dialog mode and not the other. +- The current duplication makes behavior drift more likely over time. +- This is a natural follow-on after targeted test coverage exists. + +**Acceptance criteria** + +- Shared dialog state and button-management logic are extracted into one internal layer. +- Host-specific behavior remains isolated to figure-specific versus panel-specific concerns. +- Existing dialog subclasses continue to behave the same. +- Direct dialog tests cover both hosting modes before and after refactoring. + +## 7. Split `BaseApp` responsibilities into narrower framework services + +**Title**: Reduce `BaseApp` scope by extracting theme, preferences, and dialog responsibilities + +**Summary** + +`widgets/+wt/+apps/BaseApp.m` currently mixes too many responsibilities for a framework superclass. Its size and breadth make it a high-risk class for future changes. + +**Affected files** + +- `widgets/+wt/+apps/BaseApp.m` +- `widgets/+wt/+apps/AbstractSessionApp.m` +- `widgets/+wt/+apps/BaseSingleSessionApp.m` +- `widgets/+wt/+apps/BaseMultiSessionApp.m` + +**Why this matters** + +- App subclasses inherit a wide API surface regardless of what they need. +- Theme handling, preference persistence, and dialog orchestration change for different reasons and should not be tightly coupled. +- This class is a likely source of breaking changes as the framework evolves. + +**Acceptance criteria** + +- `BaseApp` responsibilities are mapped and split into smaller internal services, collaborators, or mixins. +- Public subclass behavior is preserved or intentionally deprecated with migration notes. +- Session-app subclasses continue to pass existing tests. +- The resulting `BaseApp` has fewer mixed concerns and clearer extension points. + +## 8. Align shared selection logic between `ListSelector` variants + +**Title**: Extract shared selection/index translation logic from `ListSelector` widgets + +**Summary** + +`widgets/+wt/ListSelector.m` and `widgets/+wt/ListSelectorTwoPane.m` expose related selection workflows but maintain separate implementations. The overlap suggests a shared internal selection core should exist. + +**Affected files** + +- `widgets/+wt/ListSelector.m` +- `widgets/+wt/ListSelectorTwoPane.m` +- `test/+wt/+test/ListSelector.m` +- `test/+wt/+test/ListSelectorTwoPane.m` + +**Why this matters** + +- Similar widgets can drift semantically when bug fixes are applied independently. +- Shared item/value/index handling logic is difficult to keep consistent across separate implementations. +- This is a maintainability issue first, but it also creates user-facing inconsistency risk. + +**Acceptance criteria** + +- Common item, value, highlight, and index translation logic is extracted into a shared helper or internal selector core. +- Both selector widgets preserve current observable behavior. +- Existing selector tests pass, with new regression coverage added for shared edge cases. + +## 9. Decide support status and stabilization plan for `SearchDropDown` + +**Title**: Define whether `SearchDropDown` is experimental or supported and align code/tests accordingly + +**Summary** + +`widgets/+wt/SearchDropDown.m` is explicitly labeled as a prototype widget, but it is also part of the shipped widget set and already has dedicated tests. The current position is ambiguous from a support standpoint. + +**Affected files** + +- `widgets/+wt/SearchDropDown.m` +- `test/+wt/+test/SearchDropDown.m` +- `README.md` + +**Why this matters** + +- Prototype labeling conflicts with the expectations users will have for a public widget. +- The class is large and interaction-heavy, which raises support burden if treated as stable. +- Major releases should avoid ambiguous support boundaries. + +**Acceptance criteria** + +- A maintainer decision is made to either stabilize or explicitly constrain support for `SearchDropDown`. +- Documentation is updated to reflect that decision. +- If stabilized, additional tests cover keyboard interaction, focus behavior, and filtered-list synchronization. +- If not stabilized, the prototype status is documented clearly and surfaced consistently. + +## 10. Expand lifecycle and reuse tests for `ContextualView` + +**Title**: Add deeper lifecycle and reuse tests for `ContextualView` + +**Summary** + +`widgets/+wt/ContextualView.m` has useful direct coverage now, but it still owns a complex lifecycle: dynamic class loading, model reassignment, reuse of loaded views, active-view switching, and deletion-aware cleanup. + +**Affected files** + +- `widgets/+wt/ContextualView.m` +- `test/+wt/+test/ContextualView.m` + +**Why this matters** + +- This class uses string-based dynamic loading and cached view instances. +- Runtime-only errors are more likely here than in static widgets. +- Coverage should protect reuse semantics before any refactoring or API hardening. + +**Acceptance criteria** + +- Tests cover relaunching the same view class with an existing loaded instance. +- Tests cover behavior when a previously loaded view has been deleted. +- Tests cover model reassignment semantics explicitly. +- Tests verify cleanup and active-view state after transitions. diff --git a/docs/reviews/2026-07-maintainer-review.md b/docs/reviews/2026-07-maintainer-review.md new file mode 100644 index 00000000..3fdcd20d --- /dev/null +++ b/docs/reviews/2026-07-maintainer-review.md @@ -0,0 +1,232 @@ +# 2026-07 Maintainer Review + +Scope: `widgets/+wt`, `test/+wt/+test`, `README.md`, `buildfile.m`, and the MATLAB project entry point `WidgetsToolbox.prj`. + +This review is aimed at release readiness. MATLAB Code Analyzer was quiet on the main hotspot files, so the concerns below are primarily architectural, compatibility, and supportability risks rather than syntax-level defects. + +## Architectural Concerns + +### 1. Overloaded application base class + +- `widgets/+wt/+apps/BaseApp.m` + - `BaseApp` mixes window lifecycle, preference persistence, theme handling, dialog orchestration, custom display behavior, and debugging support into one superclass. + - This creates a high fan-in dependency: app subclasses inherit a large behavioral surface even when they only need a small subset. + - Any change to figure management, theme logic, or preferences risks regressions across the app framework. + +### 2. Duplicated dialog framework split by hosting mode + +- `widgets/+wt/+abstract/BaseInternalDialog.m` +- `widgets/+wt/+abstract/BaseExternalDialog.m` + - These classes implement parallel dialog concepts with overlapping lifecycle and button-management behavior, but diverge because one targets embedded panels and the other separate figures. + - The duplication increases maintenance cost and makes fixes likely to land in only one path. + - Both files are also marked as prototype components, which weakens the contract for downstream users late in a release cycle. + +### 3. Centralized and fragile model/event backbone + +- `widgets/+wt/+model/BaseModel.m` + - `BaseModel` owns property listeners, recursive aggregated-model notifications, copy behavior, display behavior, and load-time repair concerns. + - That concentration makes the class a single point of failure for model synchronization and persistence. + - Recursive event propagation is especially risky because small changes can alter notification order or create hard-to-debug listener cascades. + +### 4. Dynamic view loading couples navigation, construction, and ownership + +- `widgets/+wt/ContextualView.m` + - `ContextualView` handles dynamic class loading, model injection, caching/reuse of loaded views, active-view switching, and cleanup. + - That is a powerful API, but it also means navigation behavior depends on runtime strings, object validity state, and implicit conventions for hosted views. + - The result is a flexible but difficult-to-reason-about framework seam. + +## Technical Debt + +### 1. Release compatibility policy is inconsistent + +- `README.md` +- `buildfile.m` + - The README says the toolbox is for new development starting with `R2021a`. + - `buildfile.m` requires `R2023b` or later for build tasks. + - That split may be acceptable if runtime and build-time support differ intentionally, but it is not explained clearly enough for maintainers or contributors. + +### 2. Internal MATLAB APIs are part of the theme path + +- `widgets/+wt/+apps/BaseApp.m` +- `widgets/+wt/+abstract/BaseWidget.m` +- `widgets/+wt/+abstract/BaseViewController.m` + - These files call `matlab.graphics.internal.themes.getAttributeValue(...)`. + - Internal APIs are more likely to change without compatibility guarantees, so this creates avoidable release fragility. + - The risk is amplified because these classes sit near the center of the framework. + +### 3. Version gates are scattered across the codebase + +- `widgets/+wt/+apps/BaseApp.m` +- `widgets/+wt/+abstract/BaseInternalDialog.m` +- `widgets/+wt/+abstract/BaseExternalDialog.m` +- `widgets/+wt/DropDownListManager.m` +- `widgets/+wt/SearchDropDown.m` +- `widgets/+wt/ListSelector.m` +- `widgets/+wt/ContextualView.m` + - Repeated `isMATLABReleaseOlderThan(...)` branches indicate the compatibility strategy is distributed instead of isolated. + - That makes future release work harder because maintainers must rediscover version-specific behavior in many unrelated classes. + +### 4. Prototype and deprecated surfaces are still in the active framework + +- `widgets/+wt/SearchDropDown.m` +- `widgets/+wt/+abstract/BaseInternalDialog.m` +- `widgets/+wt/+abstract/BaseExternalDialog.m` +- `widgets/+wt/+abstract/BaseDialog.m` + - `SearchDropDown`, `BaseInternalDialog`, and `BaseExternalDialog` are explicitly labeled prototype. + - `BaseDialog` is marked deprecated. + - Shipping a major release with prototype and deprecated framework layers still visible to users increases support ambiguity. + +### 5. Several framework classes are already large enough to resist safe change + +- `widgets/+wt/SearchDropDown.m` at about 620 lines +- `widgets/+wt/+abstract/BaseInternalDialog.m` at about 596 lines +- `widgets/+wt/+abstract/BaseExternalDialog.m` at about 460 lines +- `widgets/+wt/+apps/BaseApp.m` at about 438 lines +- `widgets/+wt/ListSelectorTwoPane.m` at about 436 lines +- `widgets/+wt/ListSelector.m` at about 419 lines +- `widgets/+wt/+model/BaseModel.m` at about 382 lines +- `widgets/+wt/DropDownListManager.m` at about 381 lines +- `widgets/+wt/FileSelector.m` at about 379 lines + - These are not automatically problematic, but in this repository the size correlates with mixed responsibilities and compatibility branching. + +## Missing Tests + +### 1. Public widgets are tested more consistently than the internal framework + +- `test/+wt/+test` + - Top-level widgets generally have same-name tests. + - Internal framework packages are much thinner: + - `widgets/+wt/+abstract`: 8 source classes, 2 same-name tests + - `widgets/+wt/+apps`: 4 source classes, 1 same-name test + - `widgets/+wt/+dialog`: 2 source classes, 0 same-name tests + - `widgets/+wt/+mixin`: 16 source classes, 1 same-name test + - `widgets/+wt/+model`: 3 source classes, 0 same-name tests + - `widgets/+wt/+toolbar`: 3 source classes, 0 same-name tests + - `widgets/+wt/+utility`: 12 source classes, 0 same-name tests + - `widgets/+wt/+validators`: 3 source classes, 0 same-name tests + +### 2. Core framework classes lack direct regression protection + +- `widgets/+wt/+model/BaseModel.m` + - No direct same-name test exists in `test/+wt/+test`. + - Given that it underpins model observation and propagation, this is a major gap. + +- `widgets/+wt/+abstract/BaseInternalDialog.m` +- `widgets/+wt/+abstract/BaseExternalDialog.m` + - Dialog behavior is exercised indirectly through `test/+wt/+test/ListSelectionDialog.m`, but the base classes themselves do not have direct suites. + - That leaves modal behavior, teardown paths, and shared lifecycle behavior vulnerable to regressions. + +- `widgets/+wt/+abstract/BaseTimeAlignedChart.m` + - Coverage appears to be limited to a smoke-style test in `test/+wt/+test/BaseViewChart.m`. + - There is little evidence of deeper tests around axes count changes, legend/grid toggles, limit propagation, or selection behavior. + +- `widgets/+wt/ContextualView.m` + - `test/+wt/+test/ContextualView.m` now covers key launches and invalid view classes, but dynamic reuse, cleanup after deletion, and model reassignment semantics still carry higher risk than the current suite suggests. + +- `widgets/+wt/SearchDropDown.m` + - `test/+wt/+test/SearchDropDown.m` improved coverage, but the widget remains complex enough that keyboard interaction, focus state, filtered-list synchronization, and release-gated behavior deserve broader regression tests. + +### 3. Supporting infrastructure packages are effectively untested as units + +- `widgets/+wt/+eventdata` +- `widgets/+wt/+utility` +- `widgets/+wt/+validators` +- `widgets/+wt/+toolbar` + - These packages may be exercised transitively, but they do not appear to have targeted unit tests. + - That makes it harder to refactor internals safely because failures surface only through higher-level UI tests. + +## API Concerns + +### 1. Prototype APIs are difficult to support long term + +- `widgets/+wt/SearchDropDown.m` +- `widgets/+wt/+abstract/BaseInternalDialog.m` +- `widgets/+wt/+abstract/BaseExternalDialog.m` + - If these classes remain public, maintainers will be asked to preserve behavior despite their prototype disclaimers. + - If they are not intended as stable APIs, that boundary should be made much clearer before release. + +### 2. Similar selector widgets expose overlapping but different semantics + +- `widgets/+wt/ListSelector.m` +- `widgets/+wt/ListSelectorTwoPane.m` + - Both solve list selection problems, but they differ in property model and interaction semantics. + - Supporting both as separate public abstractions will become harder if bug fixes or enhancements need to stay behaviorally aligned. + +### 3. String-based dynamic view launching is flexible but brittle + +- `widgets/+wt/ContextualView.m` + - Requiring callers to pass class names as strings increases runtime-only failure modes. + - This is workable, but the API is harder to validate statically and harder to evolve without breaking calling code. + +### 4. App framework behavior likely exposes too much inherited surface area + +- `widgets/+wt/+apps/BaseApp.m` +- `widgets/+wt/+apps/AbstractSessionApp.m` +- `widgets/+wt/+apps/BaseSingleSessionApp.m` +- `widgets/+wt/+apps/BaseMultiSessionApp.m` + - The application framework appears convenient for authors, but it likely exposes a large inherited API that will be difficult to change once external apps depend on it. + - Preference persistence, session management, theme behavior, and figure lifecycle are all areas where small interface changes could become breaking changes. + +## Recommended Refactoring + +### 1. Split `BaseApp` into narrower services + +- `widgets/+wt/+apps/BaseApp.m` + - Extract theme handling, preference persistence, and dialog coordination behind separate internal collaborators or mixins. + - The goal is to reduce the amount of framework behavior every app subclass inherits by default. + +### 2. Create a shared dialog core + +- `widgets/+wt/+abstract/BaseInternalDialog.m` +- `widgets/+wt/+abstract/BaseExternalDialog.m` + - Consolidate shared state, button configuration, and lifecycle rules into a common internal layer. + - Keep only host-specific figure-versus-panel behavior in the concrete dialog bases. + +### 3. Isolate compatibility logic behind one facade + +- `widgets/+wt/+apps/BaseApp.m` +- `widgets/+wt/+abstract/BaseWidget.m` +- `widgets/+wt/+abstract/BaseViewController.m` +- `widgets/+wt/DropDownListManager.m` + - Move version checks and internal theme access behind a dedicated compatibility utility. + - This reduces scattered release branching and makes future MATLAB upgrade work more predictable. + +### 4. Reduce selector duplication + +- `widgets/+wt/ListSelector.m` +- `widgets/+wt/ListSelectorTwoPane.m` + - Extract common item/value/index translation and ordering logic into a shared internal helper or selector core. + - That should lower the risk of semantic drift between the two widgets. + +### 5. Simplify `BaseModel` event plumbing + +- `widgets/+wt/+model/BaseModel.m` + - Separate listener registration, aggregated-model tracking, and persistence/copy repair into smaller internal methods or collaborators. + - This is a good candidate for refactoring before adding more model subclasses. + +### 6. Decide whether `SearchDropDown` is experimental or supported + +- `widgets/+wt/SearchDropDown.m` + - Either reduce scope and harden it as a supported widget, or keep it clearly experimental and exclude it from stable API promises. + - The current state is in between, which is the hardest position to support. + +## Open Questions + +1. What is the actual supported MATLAB range for this release? + - `README.md` implies runtime support from `R2021a`, while `buildfile.m` requires `R2023b` for build tasks. + +2. Which classes are intended to be stable public APIs versus internal framework details? + - This is especially important for `BaseApp`, `BaseModel`, `ContextualView`, and the dialog base classes. + +3. Are prototype-tagged components expected to carry compatibility guarantees for external users? + - If yes, the prototype labels should likely be removed. + - If no, their public exposure should be reduced or documented more sharply. + +4. Is reliance on `matlab.graphics.internal.themes.getAttributeValue(...)` acceptable for the release target? + - If not, theme behavior needs a public-API fallback path. + +5. How much direct unit coverage is expected for internal packages? + - A major release would be more defensible with explicit tests for `BaseModel`, dialog bases, compatibility helpers, and time-aligned chart behavior. + +6. Are `ListSelector` and `ListSelectorTwoPane` meant to converge or remain separate long-term products? + - That decision affects whether the current duplication is temporary or structural debt. diff --git a/resources/project/LwJcy9ZaEyJvuNK-ApM3PZoS5oo/DPYBCD8atDeytBNwnG9M7ztrsX8d.xml b/resources/project/LwJcy9ZaEyJvuNK-ApM3PZoS5oo/DPYBCD8atDeytBNwnG9M7ztrsX8d.xml new file mode 100644 index 00000000..99772b42 --- /dev/null +++ b/resources/project/LwJcy9ZaEyJvuNK-ApM3PZoS5oo/DPYBCD8atDeytBNwnG9M7ztrsX8d.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/resources/project/LwJcy9ZaEyJvuNK-ApM3PZoS5oo/DPYBCD8atDeytBNwnG9M7ztrsX8p.xml b/resources/project/LwJcy9ZaEyJvuNK-ApM3PZoS5oo/DPYBCD8atDeytBNwnG9M7ztrsX8p.xml new file mode 100644 index 00000000..084220c9 --- /dev/null +++ b/resources/project/LwJcy9ZaEyJvuNK-ApM3PZoS5oo/DPYBCD8atDeytBNwnG9M7ztrsX8p.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/LwJcy9ZaEyJvuNK-ApM3PZoS5oo/bSAB07p-zJclQjVXFVggUaaEfJYd.xml b/resources/project/LwJcy9ZaEyJvuNK-ApM3PZoS5oo/bSAB07p-zJclQjVXFVggUaaEfJYd.xml new file mode 100644 index 00000000..99772b42 --- /dev/null +++ b/resources/project/LwJcy9ZaEyJvuNK-ApM3PZoS5oo/bSAB07p-zJclQjVXFVggUaaEfJYd.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/resources/project/LwJcy9ZaEyJvuNK-ApM3PZoS5oo/bSAB07p-zJclQjVXFVggUaaEfJYp.xml b/resources/project/LwJcy9ZaEyJvuNK-ApM3PZoS5oo/bSAB07p-zJclQjVXFVggUaaEfJYp.xml new file mode 100644 index 00000000..a64295ca --- /dev/null +++ b/resources/project/LwJcy9ZaEyJvuNK-ApM3PZoS5oo/bSAB07p-zJclQjVXFVggUaaEfJYp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/EaRtJshbdr5mFOrBv4pRLm7AO2Yd.xml b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/EaRtJshbdr5mFOrBv4pRLm7AO2Yd.xml new file mode 100644 index 00000000..99772b42 --- /dev/null +++ b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/EaRtJshbdr5mFOrBv4pRLm7AO2Yd.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/EaRtJshbdr5mFOrBv4pRLm7AO2Yp.xml b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/EaRtJshbdr5mFOrBv4pRLm7AO2Yp.xml new file mode 100644 index 00000000..c9bc329e --- /dev/null +++ b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/EaRtJshbdr5mFOrBv4pRLm7AO2Yp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/GF_aNYg1lkeOZDobLmnQ0ivnbcUd.xml b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/GF_aNYg1lkeOZDobLmnQ0ivnbcUd.xml new file mode 100644 index 00000000..99772b42 --- /dev/null +++ b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/GF_aNYg1lkeOZDobLmnQ0ivnbcUd.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/GF_aNYg1lkeOZDobLmnQ0ivnbcUp.xml b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/GF_aNYg1lkeOZDobLmnQ0ivnbcUp.xml new file mode 100644 index 00000000..5c825e13 --- /dev/null +++ b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/GF_aNYg1lkeOZDobLmnQ0ivnbcUp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/TukO3Gy55UeBLyc94Q0C-jPPHhcd.xml b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/TukO3Gy55UeBLyc94Q0C-jPPHhcd.xml new file mode 100644 index 00000000..99772b42 --- /dev/null +++ b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/TukO3Gy55UeBLyc94Q0C-jPPHhcd.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/TukO3Gy55UeBLyc94Q0C-jPPHhcp.xml b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/TukO3Gy55UeBLyc94Q0C-jPPHhcp.xml new file mode 100644 index 00000000..91d4202a --- /dev/null +++ b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/TukO3Gy55UeBLyc94Q0C-jPPHhcp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/U_h2Euzv4_b3xiVzTOFq7Td4c08d.xml b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/U_h2Euzv4_b3xiVzTOFq7Td4c08d.xml new file mode 100644 index 00000000..378b6137 --- /dev/null +++ b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/U_h2Euzv4_b3xiVzTOFq7Td4c08d.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/U_h2Euzv4_b3xiVzTOFq7Td4c08p.xml b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/U_h2Euzv4_b3xiVzTOFq7Td4c08p.xml new file mode 100644 index 00000000..ead62fb6 --- /dev/null +++ b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/U_h2Euzv4_b3xiVzTOFq7Td4c08p.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/ptfMWSwMtytO6QuGpd5JUonwq_od.xml b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/ptfMWSwMtytO6QuGpd5JUonwq_od.xml new file mode 100644 index 00000000..378b6137 --- /dev/null +++ b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/ptfMWSwMtytO6QuGpd5JUonwq_od.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/ptfMWSwMtytO6QuGpd5JUonwq_op.xml b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/ptfMWSwMtytO6QuGpd5JUonwq_op.xml new file mode 100644 index 00000000..902e329b --- /dev/null +++ b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/ptfMWSwMtytO6QuGpd5JUonwq_op.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/k9GapF57CilzlMtyGDnroLmL7CI/LmowpMG13smXzdCg680Y1g42aV0d.xml b/resources/project/k9GapF57CilzlMtyGDnroLmL7CI/LmowpMG13smXzdCg680Y1g42aV0d.xml new file mode 100644 index 00000000..4356a6ae --- /dev/null +++ b/resources/project/k9GapF57CilzlMtyGDnroLmL7CI/LmowpMG13smXzdCg680Y1g42aV0d.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/k9GapF57CilzlMtyGDnroLmL7CI/LmowpMG13smXzdCg680Y1g42aV0p.xml b/resources/project/k9GapF57CilzlMtyGDnroLmL7CI/LmowpMG13smXzdCg680Y1g42aV0p.xml new file mode 100644 index 00000000..04e42dd5 --- /dev/null +++ b/resources/project/k9GapF57CilzlMtyGDnroLmL7CI/LmowpMG13smXzdCg680Y1g42aV0p.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/k9GapF57CilzlMtyGDnroLmL7CI/kZk2_bHyqjbh5W64jRtodECnu5gd.xml b/resources/project/k9GapF57CilzlMtyGDnroLmL7CI/kZk2_bHyqjbh5W64jRtodECnu5gd.xml new file mode 100644 index 00000000..4356a6ae --- /dev/null +++ b/resources/project/k9GapF57CilzlMtyGDnroLmL7CI/kZk2_bHyqjbh5W64jRtodECnu5gd.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/k9GapF57CilzlMtyGDnroLmL7CI/kZk2_bHyqjbh5W64jRtodECnu5gp.xml b/resources/project/k9GapF57CilzlMtyGDnroLmL7CI/kZk2_bHyqjbh5W64jRtodECnu5gp.xml new file mode 100644 index 00000000..01cb34e6 --- /dev/null +++ b/resources/project/k9GapF57CilzlMtyGDnroLmL7CI/kZk2_bHyqjbh5W64jRtodECnu5gp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/p_kgrnFG7r36zi6RwFvI4QX5KXs/EI78aoU6iRoM1m1qF1qsjxU3JtAd.xml b/resources/project/p_kgrnFG7r36zi6RwFvI4QX5KXs/EI78aoU6iRoM1m1qF1qsjxU3JtAd.xml new file mode 100644 index 00000000..4356a6ae --- /dev/null +++ b/resources/project/p_kgrnFG7r36zi6RwFvI4QX5KXs/EI78aoU6iRoM1m1qF1qsjxU3JtAd.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/p_kgrnFG7r36zi6RwFvI4QX5KXs/EI78aoU6iRoM1m1qF1qsjxU3JtAp.xml b/resources/project/p_kgrnFG7r36zi6RwFvI4QX5KXs/EI78aoU6iRoM1m1qF1qsjxU3JtAp.xml new file mode 100644 index 00000000..2c6d013d --- /dev/null +++ b/resources/project/p_kgrnFG7r36zi6RwFvI4QX5KXs/EI78aoU6iRoM1m1qF1qsjxU3JtAp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/p_kgrnFG7r36zi6RwFvI4QX5KXs/Tg2wLQFW8obvtoXFsjEIjprlFPAd.xml b/resources/project/p_kgrnFG7r36zi6RwFvI4QX5KXs/Tg2wLQFW8obvtoXFsjEIjprlFPAd.xml new file mode 100644 index 00000000..4356a6ae --- /dev/null +++ b/resources/project/p_kgrnFG7r36zi6RwFvI4QX5KXs/Tg2wLQFW8obvtoXFsjEIjprlFPAd.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/p_kgrnFG7r36zi6RwFvI4QX5KXs/Tg2wLQFW8obvtoXFsjEIjprlFPAp.xml b/resources/project/p_kgrnFG7r36zi6RwFvI4QX5KXs/Tg2wLQFW8obvtoXFsjEIjprlFPAp.xml new file mode 100644 index 00000000..64d5d495 --- /dev/null +++ b/resources/project/p_kgrnFG7r36zi6RwFvI4QX5KXs/Tg2wLQFW8obvtoXFsjEIjprlFPAp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/p_kgrnFG7r36zi6RwFvI4QX5KXs/srfQ-d3mab8B8B5FP7S0gQd2OVkd.xml b/resources/project/p_kgrnFG7r36zi6RwFvI4QX5KXs/srfQ-d3mab8B8B5FP7S0gQd2OVkd.xml new file mode 100644 index 00000000..4356a6ae --- /dev/null +++ b/resources/project/p_kgrnFG7r36zi6RwFvI4QX5KXs/srfQ-d3mab8B8B5FP7S0gQd2OVkd.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/p_kgrnFG7r36zi6RwFvI4QX5KXs/srfQ-d3mab8B8B5FP7S0gQd2OVkp.xml b/resources/project/p_kgrnFG7r36zi6RwFvI4QX5KXs/srfQ-d3mab8B8B5FP7S0gQd2OVkp.xml new file mode 100644 index 00000000..01cb34e6 --- /dev/null +++ b/resources/project/p_kgrnFG7r36zi6RwFvI4QX5KXs/srfQ-d3mab8B8B5FP7S0gQd2OVkp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/lLewCy-_C-WfJRPYVvwveM3_P0od.xml b/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/lLewCy-_C-WfJRPYVvwveM3_P0od.xml new file mode 100644 index 00000000..4356a6ae --- /dev/null +++ b/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/lLewCy-_C-WfJRPYVvwveM3_P0od.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/lLewCy-_C-WfJRPYVvwveM3_P0op.xml b/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/lLewCy-_C-WfJRPYVvwveM3_P0op.xml new file mode 100644 index 00000000..6f7a5e37 --- /dev/null +++ b/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/lLewCy-_C-WfJRPYVvwveM3_P0op.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/rW25SbP5jRWeIzL7_iMpZrrygYkd.xml b/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/rW25SbP5jRWeIzL7_iMpZrrygYkd.xml new file mode 100644 index 00000000..4356a6ae --- /dev/null +++ b/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/rW25SbP5jRWeIzL7_iMpZrrygYkd.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/rW25SbP5jRWeIzL7_iMpZrrygYkp.xml b/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/rW25SbP5jRWeIzL7_iMpZrrygYkp.xml new file mode 100644 index 00000000..9947c605 --- /dev/null +++ b/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/rW25SbP5jRWeIzL7_iMpZrrygYkp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/rW25SbP5jRWeIzL7_iMpZrrygYk/k9GapF57CilzlMtyGDnroLmL7CId.xml b/resources/project/rW25SbP5jRWeIzL7_iMpZrrygYk/k9GapF57CilzlMtyGDnroLmL7CId.xml new file mode 100644 index 00000000..4356a6ae --- /dev/null +++ b/resources/project/rW25SbP5jRWeIzL7_iMpZrrygYk/k9GapF57CilzlMtyGDnroLmL7CId.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/rW25SbP5jRWeIzL7_iMpZrrygYk/k9GapF57CilzlMtyGDnroLmL7CIp.xml b/resources/project/rW25SbP5jRWeIzL7_iMpZrrygYk/k9GapF57CilzlMtyGDnroLmL7CIp.xml new file mode 100644 index 00000000..91ea5028 --- /dev/null +++ b/resources/project/rW25SbP5jRWeIzL7_iMpZrrygYk/k9GapF57CilzlMtyGDnroLmL7CIp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/rW25SbP5jRWeIzL7_iMpZrrygYk/mT-GHnZui1JHuHyjd1ddvO5o-Tsd.xml b/resources/project/rW25SbP5jRWeIzL7_iMpZrrygYk/mT-GHnZui1JHuHyjd1ddvO5o-Tsd.xml new file mode 100644 index 00000000..4356a6ae --- /dev/null +++ b/resources/project/rW25SbP5jRWeIzL7_iMpZrrygYk/mT-GHnZui1JHuHyjd1ddvO5o-Tsd.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/rW25SbP5jRWeIzL7_iMpZrrygYk/mT-GHnZui1JHuHyjd1ddvO5o-Tsp.xml b/resources/project/rW25SbP5jRWeIzL7_iMpZrrygYk/mT-GHnZui1JHuHyjd1ddvO5o-Tsp.xml new file mode 100644 index 00000000..01cb34e6 --- /dev/null +++ b/resources/project/rW25SbP5jRWeIzL7_iMpZrrygYk/mT-GHnZui1JHuHyjd1ddvO5o-Tsp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/rW25SbP5jRWeIzL7_iMpZrrygYk/p_kgrnFG7r36zi6RwFvI4QX5KXsd.xml b/resources/project/rW25SbP5jRWeIzL7_iMpZrrygYk/p_kgrnFG7r36zi6RwFvI4QX5KXsd.xml new file mode 100644 index 00000000..4356a6ae --- /dev/null +++ b/resources/project/rW25SbP5jRWeIzL7_iMpZrrygYk/p_kgrnFG7r36zi6RwFvI4QX5KXsd.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/rW25SbP5jRWeIzL7_iMpZrrygYk/p_kgrnFG7r36zi6RwFvI4QX5KXsp.xml b/resources/project/rW25SbP5jRWeIzL7_iMpZrrygYk/p_kgrnFG7r36zi6RwFvI4QX5KXsp.xml new file mode 100644 index 00000000..75835c19 --- /dev/null +++ b/resources/project/rW25SbP5jRWeIzL7_iMpZrrygYk/p_kgrnFG7r36zi6RwFvI4QX5KXsp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/rgXXnynq22bfGDhCOgy-Zy3ZN2w/AE3v8Y8ulLrqSob4UaR275UgVNcd.xml b/resources/project/rgXXnynq22bfGDhCOgy-Zy3ZN2w/AE3v8Y8ulLrqSob4UaR275UgVNcd.xml new file mode 100644 index 00000000..99772b42 --- /dev/null +++ b/resources/project/rgXXnynq22bfGDhCOgy-Zy3ZN2w/AE3v8Y8ulLrqSob4UaR275UgVNcd.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/resources/project/rgXXnynq22bfGDhCOgy-Zy3ZN2w/AE3v8Y8ulLrqSob4UaR275UgVNcp.xml b/resources/project/rgXXnynq22bfGDhCOgy-Zy3ZN2w/AE3v8Y8ulLrqSob4UaR275UgVNcp.xml new file mode 100644 index 00000000..0dcf72a9 --- /dev/null +++ b/resources/project/rgXXnynq22bfGDhCOgy-Zy3ZN2w/AE3v8Y8ulLrqSob4UaR275UgVNcp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/rgXXnynq22bfGDhCOgy-Zy3ZN2w/GthwoYEEQ99IYSz6W_KF1fW1QEcd.xml b/resources/project/rgXXnynq22bfGDhCOgy-Zy3ZN2w/GthwoYEEQ99IYSz6W_KF1fW1QEcd.xml new file mode 100644 index 00000000..99772b42 --- /dev/null +++ b/resources/project/rgXXnynq22bfGDhCOgy-Zy3ZN2w/GthwoYEEQ99IYSz6W_KF1fW1QEcd.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/resources/project/rgXXnynq22bfGDhCOgy-Zy3ZN2w/GthwoYEEQ99IYSz6W_KF1fW1QEcp.xml b/resources/project/rgXXnynq22bfGDhCOgy-Zy3ZN2w/GthwoYEEQ99IYSz6W_KF1fW1QEcp.xml new file mode 100644 index 00000000..9a2a19a3 --- /dev/null +++ b/resources/project/rgXXnynq22bfGDhCOgy-Zy3ZN2w/GthwoYEEQ99IYSz6W_KF1fW1QEcp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/12QbYibBEvoQXdo7tRmsUVN2HoId.xml b/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/12QbYibBEvoQXdo7tRmsUVN2HoId.xml new file mode 100644 index 00000000..99772b42 --- /dev/null +++ b/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/12QbYibBEvoQXdo7tRmsUVN2HoId.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/12QbYibBEvoQXdo7tRmsUVN2HoIp.xml b/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/12QbYibBEvoQXdo7tRmsUVN2HoIp.xml new file mode 100644 index 00000000..bbb9bad2 --- /dev/null +++ b/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/12QbYibBEvoQXdo7tRmsUVN2HoIp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/1olLKziVPq9z4Kozh6TvJl3SK1Yd.xml b/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/1olLKziVPq9z4Kozh6TvJl3SK1Yd.xml new file mode 100644 index 00000000..99772b42 --- /dev/null +++ b/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/1olLKziVPq9z4Kozh6TvJl3SK1Yd.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/1olLKziVPq9z4Kozh6TvJl3SK1Yp.xml b/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/1olLKziVPq9z4Kozh6TvJl3SK1Yp.xml new file mode 100644 index 00000000..cffe596c --- /dev/null +++ b/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/1olLKziVPq9z4Kozh6TvJl3SK1Yp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/35NJfWKQskKtcDIl1hJWi3fwexMd.xml b/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/35NJfWKQskKtcDIl1hJWi3fwexMd.xml new file mode 100644 index 00000000..99772b42 --- /dev/null +++ b/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/35NJfWKQskKtcDIl1hJWi3fwexMd.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/35NJfWKQskKtcDIl1hJWi3fwexMp.xml b/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/35NJfWKQskKtcDIl1hJWi3fwexMp.xml new file mode 100644 index 00000000..5c819077 --- /dev/null +++ b/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/35NJfWKQskKtcDIl1hJWi3fwexMp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/OLUT3b1C_0Sm03u1Ldnr4BCqBHMd.xml b/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/OLUT3b1C_0Sm03u1Ldnr4BCqBHMd.xml new file mode 100644 index 00000000..99772b42 --- /dev/null +++ b/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/OLUT3b1C_0Sm03u1Ldnr4BCqBHMd.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/OLUT3b1C_0Sm03u1Ldnr4BCqBHMp.xml b/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/OLUT3b1C_0Sm03u1Ldnr4BCqBHMp.xml new file mode 100644 index 00000000..eea84330 --- /dev/null +++ b/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/OLUT3b1C_0Sm03u1Ldnr4BCqBHMp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/PTQ0PKy-dA0VGdB7FDVLWpldPRwd.xml b/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/PTQ0PKy-dA0VGdB7FDVLWpldPRwd.xml new file mode 100644 index 00000000..99772b42 --- /dev/null +++ b/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/PTQ0PKy-dA0VGdB7FDVLWpldPRwd.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/PTQ0PKy-dA0VGdB7FDVLWpldPRwp.xml b/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/PTQ0PKy-dA0VGdB7FDVLWpldPRwp.xml new file mode 100644 index 00000000..2dc84bcf --- /dev/null +++ b/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/PTQ0PKy-dA0VGdB7FDVLWpldPRwp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/VRaLjKC2p8N3FgDKYrUwMEXXJvUd.xml b/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/VRaLjKC2p8N3FgDKYrUwMEXXJvUd.xml new file mode 100644 index 00000000..99772b42 --- /dev/null +++ b/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/VRaLjKC2p8N3FgDKYrUwMEXXJvUd.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/VRaLjKC2p8N3FgDKYrUwMEXXJvUp.xml b/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/VRaLjKC2p8N3FgDKYrUwMEXXJvUp.xml new file mode 100644 index 00000000..a6198141 --- /dev/null +++ b/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/VRaLjKC2p8N3FgDKYrUwMEXXJvUp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/sUQu55S2XaDiJu4JoMM4t6nwhtEd.xml b/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/sUQu55S2XaDiJu4JoMM4t6nwhtEd.xml new file mode 100644 index 00000000..99772b42 --- /dev/null +++ b/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/sUQu55S2XaDiJu4JoMM4t6nwhtEd.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/sUQu55S2XaDiJu4JoMM4t6nwhtEp.xml b/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/sUQu55S2XaDiJu4JoMM4t6nwhtEp.xml new file mode 100644 index 00000000..51013013 --- /dev/null +++ b/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/sUQu55S2XaDiJu4JoMM4t6nwhtEp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/uBBr3bLjPvCwf76y4VCoRVOHUikd.xml b/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/uBBr3bLjPvCwf76y4VCoRVOHUikd.xml new file mode 100644 index 00000000..99772b42 --- /dev/null +++ b/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/uBBr3bLjPvCwf76y4VCoRVOHUikd.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/uBBr3bLjPvCwf76y4VCoRVOHUikp.xml b/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/uBBr3bLjPvCwf76y4VCoRVOHUikp.xml new file mode 100644 index 00000000..19b3b962 --- /dev/null +++ b/resources/project/tDZtuUmULoUDOQJQB_jleHPjq6M/uBBr3bLjPvCwf76y4VCoRVOHUikp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/test/+wt/+test/+model/AggregatingBaseModel.m b/test/+wt/+test/+model/AggregatingBaseModel.m new file mode 100644 index 00000000..e26e4005 --- /dev/null +++ b/test/+wt/+test/+model/AggregatingBaseModel.m @@ -0,0 +1,17 @@ +classdef AggregatingBaseModel < wt.model.BaseModel + % Test-only BaseModel subclass with an aggregated child model. + + % Copyright 2026 The MathWorks, Inc. + + properties (AbortSet, SetObservable) + Child (:,1) wt.test.model.SimpleBaseModel = ... + wt.test.model.SimpleBaseModel.empty(0,1) + end + + methods (Access = protected) + function props = getAggregatedModelProperties(~) + props = "Child"; + end + end + +end diff --git a/test/+wt/+test/+model/SimpleBaseModel.m b/test/+wt/+test/+model/SimpleBaseModel.m new file mode 100644 index 00000000..04c71cb7 --- /dev/null +++ b/test/+wt/+test/+model/SimpleBaseModel.m @@ -0,0 +1,16 @@ +classdef SimpleBaseModel < wt.model.BaseModel + % Test-only BaseModel subclass with an observable property. + + % Copyright 2026 The MathWorks, Inc. + + properties (AbortSet, SetObservable) + Count (1,1) double = 0 + end + + methods (Access = protected) + function name = getDefaultName(~) + name = "Default Test Model"; + end + end + +end diff --git a/test/+wt/+test/BaseExternalDialog.m b/test/+wt/+test/BaseExternalDialog.m new file mode 100644 index 00000000..7c9543c2 --- /dev/null +++ b/test/+wt/+test/BaseExternalDialog.m @@ -0,0 +1,166 @@ +classdef BaseExternalDialog < wt.test.BaseDialogTest + % Tests for wt.abstract.BaseExternalDialog. + + % Copyright 2026 The MathWorks, Inc. + + properties (Access = private) + DialogButtonCount (1,1) double {mustBeInteger, mustBeNonnegative} = 0 + DialogButtonEvents (1,:) cell = {} + end + + methods (TestMethodSetup) + function resetEventTracking(testCase) + testCase.DialogButtonCount = 0; + testCase.DialogButtonEvents = {}; + end + end + + methods (Test) + function testDefaultsWithCallingFigure(testCase) + + dlg = testCase.createDialog(); + + testCase.verifyEqual(dlg.Size, [350 200]) + testCase.verifyFalse(dlg.Modal) + testCase.verifyEqual(dlg.Title, "") + testCase.verifyEqual(dlg.DeleteActions, ... + ["delete","close","ok","cancel","exit"]) + testCase.verifyFalse(dlg.IsWaitingForOutput) + testCase.verifyEmpty(dlg.Output) + testCase.verifyFalse(dlg.ModalImage.Visible) + testCase.verifyEqual(dlg.CallingFigure, testCase.Figure) + testCase.verifyTrue(isvalid(dlg.DialogFigure)) + + end + + function testDialogFigureProperties(testCase) + + dlg = testCase.createDialog(); + expTitle = "External Dialog"; + expPosition = [100 120 320 240]; + expTooltip = "Dialog is open"; + + dlg.Title = expTitle; + dlg.DialogPosition = expPosition; + dlg.ModalTooltip = expTooltip; + drawnow + + testCase.verifyEqual(dlg.Title, expTitle) + testCase.verifyEqual(dlg.DialogPosition, expPosition) + testCase.verifyEqual(dlg.ModalTooltip, expTooltip) + testCase.verifyEqual(string(dlg.DialogFigure.Name), expTitle) + + end + + function testDialogButtonProperties(testCase) + + dlg = testCase.createDialog(); + + dlg.DialogButtonText = ["Apply","Cancel"]; + dlg.DialogButtonTag = ["apply","cancel"]; + dlg.DialogButtonTooltip = ["Apply changes","Cancel changes"]; + dlg.DialogButtonEnable = [true false]; + drawnow + + testCase.verifyEqual(dlg.DialogButtonText, ["Apply","Cancel"]) + testCase.verifyEqual(dlg.DialogButtonTag, ["apply","cancel"]) + testCase.verifyEqual(dlg.DialogButtonTooltip, ... + ["Apply changes","Cancel changes"]) + testCase.verifyEqual(logical(dlg.DialogButtonEnable), ... + [true false]) + testCase.verifyNumElements(dlg.DialogButtons.Button, 2) + + end + + function testNonDeleteButtonNotifiesWithoutDeleting(testCase) + + dlg = testCase.createDialog(); + dlg.DialogButtonText = "Apply"; + dlg.DialogButtonTag = "apply"; + dlg.DialogButtonPushedFcn = ... + @(~,evt)testCase.onDialogButtonPushed(evt); + drawnow + + + testCase.press(dlg.DialogButtons.Button(1)) + drawnow + + testCase.verifyTrue(isvalid(dlg)) + testCase.verifyEqual(testCase.DialogButtonCount, 1) + testCase.verifyEqual(testCase.DialogButtonEvents{1}.Action, "apply") + + end + + function testDeleteButtonDeletesDialogAndFigure(testCase) + + dlg = testCase.createDialog(); + dlg.DialogButtonText = "Cancel"; + dlg.DialogButtonTag = "cancel"; + dialogFigure = dlg.DialogFigure; + drawnow + + testCase.press(dlg.DialogButtons.Button(1)) + drawnow + + testCase.verifyFalse(isvalid(dlg)) + testCase.verifyFalse(isvalid(dialogFigure)) + + end + + function testModalTogglesOverlay(testCase) + + dlg = testCase.createDialog(); + + dlg.Modal = true; + drawnow + + testCase.verifyTrue(dlg.ModalImage.Visible) + + dlg.Modal = false; + drawnow + + testCase.verifyFalse(dlg.ModalImage.Visible) + + end + + function testLifecycleOwnerDeletionDeletesDialog(testCase) + + dlg = testCase.createDialog(); + owner = uipanel(testCase.Figure); + + dlg.attachLifecycleListeners(owner); + delete(owner) + drawnow + + testCase.verifyFalse(isvalid(dlg)) + + end + end + + methods (Access = private) + function dlg = createDialog(testCase) + + fcn = @()wt.abstract.BaseExternalDialog(testCase.Figure); + dlg = testCase.verifyWarningFree(fcn); + testCase.addTeardown(@()testCase.deleteDialog(dlg)) + drawnow + + end + + function deleteDialog(~, dlg) + + if isvalid(dlg) + delete(dlg) + end + + end + + function onDialogButtonPushed(testCase, evt) + + testCase.DialogButtonCount = testCase.DialogButtonCount + 1; + testCase.DialogButtonEvents{end+1} = evt; + + end + end + +end diff --git a/test/+wt/+test/BaseInternalDialog.m b/test/+wt/+test/BaseInternalDialog.m new file mode 100644 index 00000000..ea7c3b8b --- /dev/null +++ b/test/+wt/+test/BaseInternalDialog.m @@ -0,0 +1,144 @@ +classdef BaseInternalDialog < wt.test.BaseDialogTest + % Tests for wt.abstract.BaseInternalDialog. + + % Copyright 2026 The MathWorks, Inc. + + properties (Access = private) + DialogButtonCount (1,1) double {mustBeInteger, mustBeNonnegative} = 0 + DialogButtonEvents (1,:) cell = {} + end + + methods (TestMethodSetup) + function resetEventTracking(testCase) + testCase.DialogButtonCount = 0; + testCase.DialogButtonEvents = {}; + end + end + + methods (Test) + function testDefaults(testCase) + + dlg = testCase.createDialog(); + + testCase.verifyEqual(dlg.Size, [350 200]) + testCase.verifyFalse(dlg.Modal) + testCase.verifyEqual(dlg.Title, "") + testCase.verifyEqual(dlg.DeleteActions, ... + ["delete","close","ok","cancel","exit"]) + testCase.verifyFalse(dlg.IsWaitingForOutput) + testCase.verifyEmpty(dlg.Output) + testCase.verifyFalse(dlg.ModalImage.Visible) + testCase.verifyEqual(dlg.Parent, testCase.Figure) + + end + + function testDialogButtonProperties(testCase) + + dlg = testCase.createDialog(); + + dlg.DialogButtonText = ["Apply","Cancel"]; + dlg.DialogButtonTag = ["apply","cancel"]; + dlg.DialogButtonTooltip = ["Apply changes","Cancel changes"]; + dlg.DialogButtonEnable = [true false]; + drawnow + + testCase.verifyEqual(dlg.DialogButtonText, ["Apply","Cancel"]) + testCase.verifyEqual(dlg.DialogButtonTag, ["apply","cancel"]) + testCase.verifyEqual(dlg.DialogButtonTooltip, ... + ["Apply changes","Cancel changes"]) + testCase.verifyEqual(logical(dlg.DialogButtonEnable), ... + [true false]) + testCase.verifyNumElements(dlg.DialogButtons.Button, 2) + testCase.verifyEqual(string(dlg.DialogButtons.Button(1).Text), ... + "Apply") + testCase.verifyEqual(string(dlg.DialogButtons.Button(2).Tag), ... + "cancel") + + end + + function testNonDeleteButtonNotifiesWithoutDeleting(testCase) + + dlg = testCase.createDialog(); + dlg.DialogButtonText = "Apply"; + dlg.DialogButtonTag = "apply"; + dlg.DialogButtonPushedFcn = ... + @(~,evt)testCase.onDialogButtonPushed(evt); + drawnow + + testCase.press(dlg.DialogButtons.Button(1)) + drawnow + + testCase.verifyTrue(isvalid(dlg)) + testCase.verifyEqual(testCase.DialogButtonCount, 1) + testCase.verifyEqual(testCase.DialogButtonEvents{1}.Action, "apply") + + end + + function testCloseButtonDeletesDialog(testCase) + + dlg = testCase.createDialog(); + + testCase.press(dlg.CloseButton) + drawnow + + testCase.verifyFalse(isvalid(dlg)) + + end + + function testModalTogglesOverlay(testCase) + + dlg = testCase.createDialog(); + + dlg.Modal = true; + drawnow + + testCase.verifyTrue(dlg.ModalImage.Visible) + + dlg.Modal = false; + drawnow + + testCase.verifyFalse(dlg.ModalImage.Visible) + + end + + function testLifecycleOwnerDeletionDeletesDialog(testCase) + + dlg = testCase.createDialog(); + owner = uipanel(testCase.Figure); + + dlg.attachLifecycleListeners(owner); + delete(owner) + drawnow + + testCase.verifyFalse(isvalid(dlg)) + + end + end + + methods (Access = private) + function dlg = createDialog(testCase) + + fcn = @()wt.abstract.BaseInternalDialog(testCase.Figure); + dlg = testCase.verifyWarningFree(fcn); + testCase.addTeardown(@()testCase.deleteDialog(dlg)) + drawnow + + end + + function deleteDialog(~, dlg) + + if isvalid(dlg) + delete(dlg) + end + + end + + function onDialogButtonPushed(testCase, evt) + + testCase.DialogButtonCount = testCase.DialogButtonCount + 1; + testCase.DialogButtonEvents{end+1} = evt; + + end + end + +end diff --git a/test/+wt/+test/BaseModel.m b/test/+wt/+test/BaseModel.m new file mode 100644 index 00000000..cca14706 --- /dev/null +++ b/test/+wt/+test/BaseModel.m @@ -0,0 +1,170 @@ +classdef BaseModel < matlab.unittest.TestCase + % Tests for wt.model.BaseModel. + + % Copyright 2026 The MathWorks, Inc. + + properties (Access = private) + PropertyChangedListener event.listener + ModelChangedListener event.listener + PropertyChangedCount (1,1) double {mustBeInteger, mustBeNonnegative} = 0 + ModelChangedCount (1,1) double {mustBeInteger, mustBeNonnegative} = 0 + PropertyChangedEvents (1,:) cell = {} + ModelChangedEvents (1,:) cell = {} + end + + methods (TestMethodSetup) + function resetEventTracking(testCase) + testCase.PropertyChangedCount = 0; + testCase.ModelChangedCount = 0; + testCase.PropertyChangedEvents = {}; + testCase.ModelChangedEvents = {}; + testCase.PropertyChangedListener = event.listener.empty(0,1); + testCase.ModelChangedListener = event.listener.empty(0,1); + end + end + + methods (Test) + function testConstructorAssignsPropertyValues(testCase) + + model = wt.test.model.SimpleBaseModel("Name","Configured", ... + "Count",7); + + testCase.verifyEqual(model.Name, "Configured") + testCase.verifyEqual(model.Count, 7) + + end + + function testDefaultNameUsesSubclassFallback(testCase) + + model = wt.test.model.SimpleBaseModel; + + testCase.verifyEqual(model.Name, "Default Test Model") + + end + + function testObservablePropertyChangeNotifies(testCase) + + model = wt.test.model.SimpleBaseModel; + testCase.listenToModel(model); + + model.Count = 3; + + testCase.verifyEqual(testCase.PropertyChangedCount, 1) + testCase.verifyEqual(testCase.ModelChangedCount, 1) + testCase.verifyPropertyChangedEvent("Count", 3) + testCase.verifyModelChangedEvent(model, "Count", 3) + + end + + function testDisableChangeListenersSuppressesNotifications(testCase) + + model = wt.test.model.SimpleBaseModel; + testCase.listenToModel(model); + + model.EnableChangeListeners = false; + model.Count = 4; + + testCase.verifyEqual(testCase.PropertyChangedCount, 0) + testCase.verifyEqual(testCase.ModelChangedCount, 0) + + end + + function testAggregatedModelChangePropagates(testCase) + + child = wt.test.model.SimpleBaseModel; + parent = wt.test.model.AggregatingBaseModel("Child",child); + testCase.listenToModel(parent); + + child.Count = 8; + + testCase.verifyEqual(testCase.PropertyChangedCount, 0) + testCase.verifyEqual(testCase.ModelChangedCount, 1) + testCase.verifyModelChangedEvent(child, "Count", 8) + testCase.verifyEqual(testCase.ModelChangedEvents{1}.Stack, ... + {parent, child}) + + end + + function testAggregatedModelReassignmentUpdatesListeners(testCase) + + child1 = wt.test.model.SimpleBaseModel; + child2 = wt.test.model.SimpleBaseModel; + parent = wt.test.model.AggregatingBaseModel("Child",child1); + testCase.listenToModel(parent); + + parent.Child = child2; + child1.Count = 1; + child2.Count = 2; + + testCase.verifyEqual(testCase.PropertyChangedCount, 1) + testCase.verifyEqual(testCase.ModelChangedCount, 2) + testCase.verifyModelChangedEvent(child2, "Count", 2) + + end + + function testCopyDeepCopiesAggregatedModelAndListeners(testCase) + + child = wt.test.model.SimpleBaseModel("Count",2); + parent = wt.test.model.AggregatingBaseModel("Child",child); + parentCopy = copy(parent); + testCase.listenToModel(parentCopy); + + parentCopy.Child.Count = 5; + + testCase.verifyNotEqual(parentCopy, parent) + testCase.verifyNotEqual(parentCopy.Child, child) + testCase.verifyEqual(parentCopy.Child.Count, 5) + testCase.verifyEqual(child.Count, 2) + testCase.verifyEqual(testCase.ModelChangedCount, 1) + testCase.verifyModelChangedEvent(parentCopy.Child, "Count", 5) + + end + end + + methods (Access = private) + function listenToModel(testCase, model) + + testCase.PropertyChangedListener = listener( ... + model, "PropertyChanged", ... + @(~,evt)testCase.onPropertyChanged(evt)); + testCase.ModelChangedListener = listener( ... + model, "ModelChanged", ... + @(~,evt)testCase.onModelChanged(evt)); + + end + + function onPropertyChanged(testCase, evt) + + testCase.PropertyChangedCount = testCase.PropertyChangedCount + 1; + testCase.PropertyChangedEvents{end+1} = evt; + + end + + function onModelChanged(testCase, evt) + + testCase.ModelChangedCount = testCase.ModelChangedCount + 1; + testCase.ModelChangedEvents{end+1} = evt; + + end + + function verifyPropertyChangedEvent(testCase, propertyName, value) + + evt = testCase.PropertyChangedEvents{end}; + + testCase.verifyEqual(string(evt.Property), propertyName) + testCase.verifyEqual(evt.Value, value) + + end + + function verifyModelChangedEvent(testCase, model, propertyName, value) + + evt = testCase.ModelChangedEvents{end}; + + testCase.verifyEqual(evt.Model, model) + testCase.verifyEqual(evt.Property, propertyName) + testCase.verifyEqual(evt.Value, value) + + end + end + +end diff --git a/test/+wt/+test/BaseTimeAlignedChart.m b/test/+wt/+test/BaseTimeAlignedChart.m new file mode 100644 index 00000000..3282d670 --- /dev/null +++ b/test/+wt/+test/BaseTimeAlignedChart.m @@ -0,0 +1,129 @@ +classdef BaseTimeAlignedChart < wt.test.BaseWidgetTest + % Tests for wt.abstract.BaseTimeAlignedChart. + + % Copyright 2026 The MathWorks, Inc. + + methods (TestMethodSetup) + function setup(testCase) + + fcn = @()wt.abstract.BaseTimeAlignedChart(testCase.Grid); + testCase.Widget = verifyWarningFree(testCase, fcn); + drawnow + + end + end + + methods (TestMethodTeardown) + function deleteChart(testCase) + + if ~isempty(testCase.Widget) && isvalid(testCase.Widget) + delete(testCase.Widget) + end + + end + end + + methods (Test) + function testDefaultAxesCreation(testCase) + + chart = testCase.Widget; + + testCase.verifyNumElements(chart.Axes, 1) + testCase.verifyEqual(chart.TiledLayout.GridSize, [1 1]) + testCase.verifyEqual(string(chart.Axes.XGrid), "on") + testCase.verifyEqual(string(chart.Axes.YGrid), "on") + + end + + function testNumAxesRecreatesLayout(testCase) + + chart = testCase.Widget; + + chart.NumAxes = 3; + drawnow + + testCase.verifyNumElements(chart.Axes, 3) + testCase.verifyEqual(chart.TiledLayout.GridSize, [3 1]) + + end + + function testForceUpdateAfterNumAxesChange(testCase) + + chart = testCase.Widget; + + chart.NumAxes = 2; + fcn = @()chart.forceUpdateChart(); + + testCase.verifyWarningFree(fcn) + testCase.verifyNumElements(chart.Axes, 2) + testCase.verifyEqual(chart.TiledLayout.GridSize, [2 1]) + + end + + function testLabelsGridAndLegendState(testCase) + + chart = testCase.Widget; + + chart.NumAxes = 2; + chart.ShowLegend = true; + chart.XLabel = ["Elapsed"; "Elapsed"]; + chart.YLabel = ["Top"; "Bottom"]; + chart.Title = ["Upper Signal"; "Lower Signal"]; + chart.GroupTitle = "Aligned Signals"; + chart.ShowGrid = false; + drawnow + + testCase.verifyNumElements(chart.Legend, 2) + testCase.verifyEqual(string(chart.Axes(1).XLabel.String), "Elapsed") + testCase.verifyEqual(string(chart.Axes(2).YLabel.String), "Bottom") + testCase.verifyEqual(string(chart.Axes(1).Title.String), "Upper Signal") + testCase.verifyEqual(string(chart.TiledLayout.Title.String), ... + "Aligned Signals") + testCase.verifyEqual(string({chart.Axes.XGrid}), ["off" "off"]) + testCase.verifyEqual(string({chart.Axes.YGrid}), ["off" "off"]) + + end + + function testYLimitDependentProperties(testCase) + + chart = testCase.Widget; + + chart.NumAxes = 2; + drawnow + chart.YLim = {[-1 1]; [10 20]}; + chart.YLimMode = ["manual"; "manual"]; + + testCase.verifyEqual(chart.YLim, {[-1 1]; [10 20]}) + testCase.verifyEqual(chart.YLimMode, ["manual"; "manual"]) + + end + + function testSelectedAxesColor(testCase) + + chart = testCase.Widget; + + chart.NumAxes = 2; + drawnow + chart.EnableSelection = true; + chart.AxesColor = [1 1 1]; + chart.AxesSelectedColor = [0.3 0.4 0.5]; + chart.SelectedAxes = 2; + chart.forceUpdateChart(); + + testCase.verifyEqual(chart.Axes(1).Color, [1 1 1], ... + "AbsTol", 1e-12) + testCase.verifyEqual(chart.Axes(2).Color, [0.3 0.4 0.5], ... + "AbsTol", 1e-12) + + chart.SelectedAxes = 1; + chart.forceUpdateChart(); + + testCase.verifyEqual(chart.Axes(1).Color, [0.3 0.4 0.5], ... + "AbsTol", 1e-12) + testCase.verifyEqual(chart.Axes(2).Color, [1 1 1], ... + "AbsTol", 1e-12) + + end + end + +end diff --git a/test/+wt/+test/ContextualView.m b/test/+wt/+test/ContextualView.m index f40bdc86..4eb04829 100644 --- a/test/+wt/+test/ContextualView.m +++ b/test/+wt/+test/ContextualView.m @@ -1,7 +1,7 @@ classdef ContextualView < wt.test.BaseWidgetTest % Implements a unit test for a widget or component - % Copyright 2025 The MathWorks, Inc. + % Copyright 2025-2026 The MathWorks, Inc. %% Class Setup @@ -41,6 +41,15 @@ function setup(testCase) %% Test methods methods (Test) + function testInvalidViewClassErrors(testCase) + + % Invalid class paths should fail with the documented error + model = zooexample.model.Animal; + fcn = @()testCase.Widget.launchView("not.a.real.ViewClass", model); + testCase.verifyError(fcn, "wt:ContextualView:InvalidPaneType"); + + end %function + function testLaunchViewWithModel(testCase) % This tests: % Launching the Animal view @@ -204,6 +213,62 @@ function testChangingViews(testCase) end %function + function testChangingViewsClearsInactiveModel(testCase) + % This tests: + % Deactivating a view clears its parent and model + % Relaunching a cached view attaches the latest model + + % Create models to display + model1 = testCase.createAnimalModel("Simba"); + model2 = testCase.createAnimalModel("Nala"); + model3 = zooexample.model.Enclosure; + model3.Name = "Lions' Den"; + model3.Location = [10 20]; + + % Launch two different view types + view1 = testCase.Widget.launchView("zooexample.view.Animal", model1); + view3 = testCase.Widget.launchView("zooexample.view.Enclosure", model3); + + % Verify the inactive view was detached from the UI and model + testCase.verifyEmpty(view1.Parent) + testCase.verifyEmpty(view1.Model) + testCase.verifyEqual(testCase.Widget.ActiveView, view3) + + % Relaunch the original view type with a different model + view1b = testCase.Widget.launchView("zooexample.view.Animal", model2); + + % Verify the cached view is reused with the latest model + testCase.verifyEqual(view1b, view1) + testCase.verifyEqual(view1.Model, model2) + testCase.verifyEqual(view1.Parent, testCase.Widget.ContentGrid) + testCase.verifyEmpty(view3.Parent) + testCase.verifyEmpty(view3.Model) + + end %function + + + function testDeletedLoadedViewIsRecreated(testCase) + % This tests: + % Deleted cached views are ignored and recreated + + % Launch a view, then delete it externally + model = testCase.createAnimalModel("Simba"); + view1 = testCase.Widget.launchView("zooexample.view.Animal", model); + delete(view1) + + % Relaunch the same view type + view2 = testCase.Widget.launchView("zooexample.view.Animal", model); + + % Verify a valid replacement is active and loaded + testCase.verifyFalse(isvalid(view1)) + testCase.verifyTrue(isvalid(view2)) + testCase.verifyEqual(testCase.Widget.ActiveView, view2) + testCase.verifyEqual(testCase.Widget.LoadedViews, view2) + testCase.verifyEqual(view2.Model, model) + + end %function + + function testClearView(testCase) % This tests debugging methods: % clearView @@ -325,6 +390,17 @@ function testReset(testCase) %% Helper methods methods (Access = protected) + function model = createAnimalModel(~, name) + % Create a populated animal model for view launch tests. + + model = zooexample.model.Animal; + model.Species = "Lion"; + model.Name = name; + model.Sex = "female"; + model.BirthDate = "September 13, 1994"; + + end %function + function launchMultipleViews(testCase) % Launches multiple views into the component to have it in a % used state @@ -355,4 +431,4 @@ function launchMultipleViews(testCase) end %methods -end %classdef \ No newline at end of file +end %classdef diff --git a/test/+wt/+test/DatetimeSelector.m b/test/+wt/+test/DatetimeSelector.m index 0ff4a014..4fd95195 100644 --- a/test/+wt/+test/DatetimeSelector.m +++ b/test/+wt/+test/DatetimeSelector.m @@ -129,9 +129,9 @@ function testRollOver(testCase) % Roll the minutes back by one testCase.press(minuteControl,"down"); + drawnow actVal = w.Value; testCase.verifyEqual(actVal, dt_12 - minutes(1)); - drawnow testCase.verifyEqual(dateControl.Value, dt_0); testCase.verifyEqual(hourControl.Value, 11); testCase.verifyEqual(minuteControl.Value, 59); diff --git a/test/+wt/+test/FileSelector.m b/test/+wt/+test/FileSelector.m index e24755bb..eb00a149 100644 --- a/test/+wt/+test/FileSelector.m +++ b/test/+wt/+test/FileSelector.m @@ -292,12 +292,12 @@ function testButtonVisibility(testCase) % Normal App - should show button testCase.Widget.IsWebApp = false; testCase.verifyEventuallyHasParent(buttonControl); - testCase.Widget.forceUpdate(); + testCase.Widget.forceUpdate(false); % Web App - should NOT show button testCase.Widget.IsWebApp = true; testCase.verifyEventuallyHasNoParent(buttonControl); % NO Parent - testCase.Widget.forceUpdate(); + testCase.Widget.forceUpdate(false); % --- SelectionType == file --- % @@ -307,12 +307,12 @@ function testButtonVisibility(testCase) % Normal App - should show button testCase.Widget.IsWebApp = false; testCase.verifyEventuallyHasParent(buttonControl); - testCase.Widget.forceUpdate(); + testCase.Widget.forceUpdate(false); % Web App - should show button testCase.Widget.IsWebApp = true; testCase.verifyEventuallyHasParent(buttonControl); - testCase.Widget.forceUpdate(); + testCase.Widget.forceUpdate(false); % --- SelectionType == putfile --- % @@ -322,12 +322,12 @@ function testButtonVisibility(testCase) % Normal App - should show button testCase.Widget.IsWebApp = false; testCase.verifyEventuallyHasParent(buttonControl); - testCase.Widget.forceUpdate(); + testCase.Widget.forceUpdate(false); % Web App - should show button testCase.Widget.IsWebApp = true; testCase.verifyEventuallyHasParent(buttonControl); - testCase.Widget.forceUpdate(); + testCase.Widget.forceUpdate(false); end %function diff --git a/test/+wt/+test/MenuButton.m b/test/+wt/+test/MenuButton.m index 773c6407..c6b30f07 100644 --- a/test/+wt/+test/MenuButton.m +++ b/test/+wt/+test/MenuButton.m @@ -1,7 +1,7 @@ classdef MenuButton < wt.test.BaseWidgetTest % Implements a unit test for a widget or component - % Copyright 2025 The MathWorks, Inc. + % Copyright 2025-2026 The MathWorks, Inc. %% Class Setup methods (TestClassSetup) @@ -60,6 +60,44 @@ function setup(testCase) %% Unit Tests methods (Test) + function testButtonPushedCallback(testCase) + + % Use the button-pushed callback rather than menu-selected + testCase.Widget.ButtonPushedFcn = @(s,e)onCallbackTriggered(testCase,e); + + % Press the button + testCase.press(testCase.Widget.Button); + + % Verify the callback fired once + testCase.verifyCallbackCount(1) + + end %function + + + function testIconAndTooltipProperties(testCase) + + % Update display properties + expIcon = "add_24.png"; + expTooltip = "Open menu"; + testCase.verifySetProperty("Icon", expIcon); + testCase.verifySetProperty("Tooltip", expTooltip); + + % Verify the internal button reflects them + testCase.verifyPropertyValue(testCase.Widget.Button, "Icon", char(expIcon)); + testCase.verifyPropertyValue(testCase.Widget.Button, "Tooltip", char(expTooltip)); + + end %function + + + function testAddItemsTagSizeError(testCase) + + % Mismatched tag count should error + fcn = @()testCase.Widget.addMenuItems(["Item 1";"Item 2"], "onlyOne"); + testCase.verifyError(fcn, "MATLAB:incorrectNumel"); + + end %function + + function testAddItemsWithName(testCase) % Add items by name diff --git a/test/+wt/+test/PasswordField.m b/test/+wt/+test/PasswordField.m index 53df023e..6c7646c7 100644 --- a/test/+wt/+test/PasswordField.m +++ b/test/+wt/+test/PasswordField.m @@ -1,7 +1,7 @@ classdef PasswordField < wt.test.BaseWidgetTest % Implements a unit test for a widget or component -% Copyright 2020-2025 The MathWorks Inc. +% Copyright 2020-2026 The MathWorks Inc. %% Test Method Setup @@ -37,6 +37,17 @@ function testValue(testCase) testCase.verifyMatches(passField.Data.Value, newValue); end %function + + + function testFocusMethod(testCase) + + % Prepare the public focus call. + fcn = @()testCase.Widget.focus(); + + % Verify the focus method runs without warnings. + testCase.verifyWarningFree(fcn) + + end %function @@ -118,4 +129,4 @@ function simulateTyping(S) end end -end \ No newline at end of file +end diff --git a/test/+wt/+test/ProgressBar.m b/test/+wt/+test/ProgressBar.m index 0fa59929..2754e0e8 100644 --- a/test/+wt/+test/ProgressBar.m +++ b/test/+wt/+test/ProgressBar.m @@ -1,7 +1,7 @@ classdef ProgressBar < wt.test.BaseWidgetTest % Implements a unit test for a widget or component -% Copyright 2020-2025 The MathWorks Inc. +% Copyright 2020-2026 The MathWorks Inc. %% Test Method Setup @@ -27,6 +27,41 @@ function setup(testCase) %% Unit Tests methods (Test) + function testCancelMethodPreservesStatusText(testCase) + + % Start, cancel, then attempt to overwrite the cancel status + testCase.verifyMethod("startProgress", "Running"); + testCase.verifyMethod("cancel"); + testCase.verifyMethod(@setProgress, 0.5, "Should not replace cancel text"); + + % Verify cancel state and callback + testCase.verifyTrue(testCase.Widget.CancelRequested) + testCase.verifyCallbackCount(1) + + % Verify cancel status is preserved + actVal = string(strtrim(testCase.Widget.StatusTextLabel.Text)); + testCase.verifyEqual(actVal, "Canceling...") + + end %function + + + function testBarColorAliasAndSetStatusText(testCase) + + % Deprecated BarColor alias should still update the displayed bar + expColor = [0.2 0.4 0.6]; + testCase.verifySetProperty("BarColor", expColor); + testCase.verifyEqual(testCase.Widget.SelectionColor, expColor) + testCase.verifyEqual(testCase.Widget.ProgressPanel.BackgroundColor, expColor, "AbsTol", 1e-12) + + % Public status-text helper should update the label + expText = "Halfway there"; + testCase.verifyMethod("setStatusText", expText) + actVal = string(strtrim(testCase.Widget.StatusTextLabel.Text)); + testCase.verifyEqual(actVal, expText) + + end %function + + function testProgress(testCase) import matlab.unittest.constraints.Eventually import matlab.unittest.constraints.IsEqualTo @@ -221,4 +256,4 @@ function testShowTimeRemaining(testCase) end %methods (Test) -end %classdef \ No newline at end of file +end %classdef diff --git a/test/+wt/+test/SearchDropDown.m b/test/+wt/+test/SearchDropDown.m index 3120f1ae..61359313 100644 --- a/test/+wt/+test/SearchDropDown.m +++ b/test/+wt/+test/SearchDropDown.m @@ -1,7 +1,7 @@ classdef SearchDropDown < wt.test.BaseWidgetTest % Implements a unit test for a widget or component - % Copyright 2025 The MathWorks Inc. + % Copyright 2025-2026 The MathWorks Inc. %% Properties properties @@ -70,6 +70,39 @@ function setup(testCase) %% Unit Test methods (Test) + function testPlaceholderProperty(testCase) + + % Set placeholder text + expValue = "Search for an item"; + testCase.verifySetProperty("Placeholder", expValue); + + % Verify it reaches the edit field + testCase.verifyPropertyValue(testCase.Widget.EditField, "Placeholder", char(expValue)) + + end %function + + + function testInteractiveNoMatchesAcceptsTypedValue(testCase) + + % Get the component + comp = testCase.Widget; + + % Type a value that is not in the list + expValue = "X"; + testCase.type(comp.EditField, expValue) + drawnow + + % Verify the filtered list is empty and the typed value is accepted + testCase.verifyEmpty(comp.ListBox.Items) + testCase.verifyPropertyValue(comp, "Value", expValue) + testCase.verifyPropertyValue(comp.EditField, "Value", char(expValue)) + + % Verify the callback fired once for the accepted value + testCase.verifyCallbackCount(1) + + end %function + + function testValueProperty(testCase) % Get the component @@ -126,4 +159,4 @@ function testInteractivePartialSearch(testCase) end %methods (Test) -end %classdef \ No newline at end of file +end %classdef diff --git a/test/+wt/+test/ThemeCompatibility.m b/test/+wt/+test/ThemeCompatibility.m new file mode 100644 index 00000000..7bd3c4eb --- /dev/null +++ b/test/+wt/+test/ThemeCompatibility.m @@ -0,0 +1,33 @@ +classdef ThemeCompatibility < matlab.unittest.TestCase + % Tests for UI theme compatibility helpers. + + % Copyright 2026 The MathWorks, Inc. + + methods (Test) + function testSupportsUIThemesReturnsLogicalScalar(testCase) + + isSupported = wt.utility.supportsUIThemes(); + + testCase.verifyClass(isSupported, "logical") + testCase.verifySize(isSupported, [1 1]) + + end + + function testGetThemeColorReturnsRgbTriplet(testCase) + + testCase.assumeTrue(wt.utility.supportsUIThemes()) + + fig = uifigure("Visible","off"); + testCase.addTeardown(@()delete(fig)) + + color = wt.utility.getThemeColor(fig.Theme, "--mw-color-primary"); + + testCase.verifyClass(color, "double") + testCase.verifySize(color, [1 3]) + testCase.verifyGreaterThanOrEqual(color, 0) + testCase.verifyLessThanOrEqual(color, 1) + + end + end + +end diff --git a/test/+wt/+test/Toolbar.m b/test/+wt/+test/Toolbar.m index a9d74218..e28586c8 100644 --- a/test/+wt/+test/Toolbar.m +++ b/test/+wt/+test/Toolbar.m @@ -214,6 +214,9 @@ function testStateButtons(testCase) % Attach the sections to the toolbar testCase.verifySetProperty("Section", section) + % Give time to render + drawnow + % Press a state button testCase.ButtonPushCallbackDone = false; testCase.press(b1) diff --git a/widgets/+wt/+abstract/BaseExternalDialog.m b/widgets/+wt/+abstract/BaseExternalDialog.m index 85dd1bfa..ef4ea73e 100644 --- a/widgets/+wt/+abstract/BaseExternalDialog.m +++ b/widgets/+wt/+abstract/BaseExternalDialog.m @@ -22,9 +22,6 @@ %% Public Properties properties (AbortSet, Access = public) - % Dialog Size - Size double {mustBePositive} = [350 200] - % Modal (block other figure interaction) Modal (1,1) logical = false @@ -33,6 +30,9 @@ properties (AbortSet, Dependent, Access = public) + % Dialog Size + Size + % Modal tooltip ModalTooltip (1,1) string @@ -68,16 +68,16 @@ end function value = get.Size(obj) - if isscalar(obj.DialogFigure) + if isscalar(obj.DialogFigure) && isvalid(obj.DialogFigure) value = obj.DialogFigure.Position(3:4); else - value = obj.Size; + value = obj.Size_I; end end function set.Size(obj, value) - obj.Size = value; - if isscalar(obj.DialogFigure) %#ok - obj.DialogFigure.Position(3:4) = value; %#ok + obj.Size_I = value; + if isscalar(obj.DialogFigure) && isvalid(obj.DialogFigure) + obj.DialogFigure.Position(3:4) = value; end end @@ -200,6 +200,9 @@ % This dialog's figure DialogFigure matlab.ui.Figure + % Stored dialog size while the dialog figure is unavailable + Size_I (1,2) double {mustBePositive} = [350 200] + end %properties @@ -354,7 +357,7 @@ function attachLifecycleListeners(obj, owners) % specified "owner" graphics objects arguments - obj (1,1) wt.abstract.BaseInternalDialog + obj (1,1) wt.abstract.BaseExternalDialog owners (1,:) matlab.graphics.Graphics end @@ -454,13 +457,13 @@ function postSetup(obj) % end % Update the size - obj.DialogFigure.Position(3:4) = obj.Size; + obj.DialogFigure.Position(3:4) = obj.Size_I; % Position over the calling figure obj.positionOverCallingFigure() % Apply the same theme as CallingFigure (R2025a and later) - if ~isMATLABReleaseOlderThan("R2025a") && ... + if wt.utility.supportsUIThemes() && ... isscalar(obj.CallingFigure) && isvalid(obj.CallingFigure) obj.DialogFigure.Theme = obj.CallingFigure.Theme; end @@ -657,4 +660,4 @@ function onDialogButtonPushed(obj,evt) end %methods -end %classdef \ No newline at end of file +end %classdef diff --git a/widgets/+wt/+abstract/BaseInternalDialog.m b/widgets/+wt/+abstract/BaseInternalDialog.m index bc935c39..0a2bb693 100644 --- a/widgets/+wt/+abstract/BaseInternalDialog.m +++ b/widgets/+wt/+abstract/BaseInternalDialog.m @@ -13,7 +13,7 @@ % ** This is a prototype component that may change in the future. - % Copyright 2022-2025 The MathWorks Inc. + % Copyright 2022-2026 The MathWorks Inc. %% Events @@ -432,7 +432,7 @@ function setup(obj) obj.Grid.Scrollable = true; % Apply theme colors - if ~isMATLABReleaseOlderThan("R2025a") + if wt.utility.supportsUIThemes() obj.OuterPanel.ForegroundColor = ... obj.getThemeColor("--mw-color-primary"); obj.OuterPanel.BorderColor = ... @@ -808,7 +808,7 @@ function applyCloseButtonColor(obj) end % Determine the color to use - if ~isMATLABReleaseOlderThan("R2025a") + if wt.utility.supportsUIThemes() bgColor = obj.getThemeColor("--mw-backgroundColor-secondary"); iconColor = obj.getThemeColor("--mw-backgroundColor-iconuiFill-primary"); else @@ -834,4 +834,4 @@ function applyCloseButtonColor(obj) end %methods -end %classdef \ No newline at end of file +end %classdef diff --git a/widgets/+wt/+abstract/BaseTimeAlignedChart.m b/widgets/+wt/+abstract/BaseTimeAlignedChart.m index 7b0cdf5d..98f81dc1 100644 --- a/widgets/+wt/+abstract/BaseTimeAlignedChart.m +++ b/widgets/+wt/+abstract/BaseTimeAlignedChart.m @@ -3,7 +3,7 @@ wt.mixin.ErrorHandling % Base class for a chart with time-aligned axes - % Copyright 2022-2025 The MathWorks Inc. + % Copyright 2022-2026 The MathWorks Inc. %% Public Properties @@ -193,6 +193,7 @@ function forceUpdateChart(obj) % Forces update to run (For debugging only!) + drawnow("nocallbacks") obj.update(); end %function @@ -257,45 +258,61 @@ function requestUpdate(obj) function recreateContent(obj) % Create / recreate all the content - % Delete existing axes - % This is required to change the tiledlayout size - delete(obj.Axes); - delete(obj.TiledLayout.Children) + % Delete existing layout children before changing tile count. + oldLegends = obj.Legend; + oldLegends(~isvalid(oldLegends)) = []; + delete(oldLegends); + oldChildren = obj.TiledLayout.Children; + oldChildren(~isvalid(oldChildren)) = []; + delete(oldChildren) obj.Axes(:) = []; + obj.Legend(:) = []; % Update the TiledLayout obj.TiledLayout.GridSize = [obj.NumAxes 1]; % Create the axes - ax = gobjects(1,obj.NumAxes); - lgnd = matlab.graphics.illustration.Legend.empty(1,0); for idx = 1:obj.NumAxes % Create the axes - ax(idx) = nexttile(obj.TiledLayout); + ax = nexttile(obj.TiledLayout); % Configure axes - ax(idx).NextPlot = "add"; - ax(idx).XAxis = matlab.graphics.axis.decorator.DurationRuler(); + ax.NextPlot = "add"; + ax.XAxis = matlab.graphics.axis.decorator.DurationRuler(); % Keep X ticks only on the last axes if idx < obj.NumAxes - ax(idx).XTickLabel = {}; + ax.XTickLabel = {}; end % Configure Interpreters - ax(idx).Title.Interpreter = "none"; - ax(idx).XLabel.Interpreter = "none"; - ax(idx).YLabel.Interpreter = "none"; - ax(idx).YAxis.TickLabelInterpreter = "none"; + ax.Title.Interpreter = "none"; + ax.XLabel.Interpreter = "none"; + ax.YLabel.Interpreter = "none"; + ax.YAxis.TickLabelInterpreter = "none"; + + end %for - % Start legend - if obj.ShowLegend + % Retrieve valid layout-owned axes in tile order. + ax = obj.TiledLayout.Children; + ax(~isvalid(ax)) = []; + isAxes = arrayfun(@(thisAx)isa(thisAx, ... + "matlab.graphics.axis.Axes"), ax); + ax = ax(isAxes); + tileNumbers = arrayfun(@(thisAx)thisAx.Layout.Tile, ax); + [~, tileOrder] = sort(tileNumbers); + ax = reshape(ax(tileOrder), 1, []); + + % Start legends + lgnd = matlab.graphics.illustration.Legend.empty(1,0); + if obj.ShowLegend + lgnd = matlab.graphics.illustration.Legend.empty(1,0); + for idx = 1:numel(ax) lgnd(idx) = legend(ax(idx),'Location',"northwest"); lgnd(idx).Interpreter = "none"; end - - end %for + end % Set modes set(ax, "ZLimMode", "manual") @@ -449,4 +466,4 @@ function onAxesButtonDown(obj, evt) AxesGridColorMode end %properties -end %classdef \ No newline at end of file +end %classdef diff --git a/widgets/+wt/+abstract/BaseViewController.m b/widgets/+wt/+abstract/BaseViewController.m index 05185cd2..067dd1ef 100644 --- a/widgets/+wt/+abstract/BaseViewController.m +++ b/widgets/+wt/+abstract/BaseViewController.m @@ -7,7 +7,7 @@ % Base class for views/controllers referencing a BaseModel class - % Copyright 2025 The MathWorks Inc. + % Copyright 2025-2026 The MathWorks Inc. %% Events @@ -72,7 +72,7 @@ obj@matlab.ui.componentcontainer.ComponentContainer(varargin{:}); % Listen to theme changes (R2025a and later only) - if ~isMATLABReleaseOlderThan("R2025a") + if wt.utility.supportsUIThemes() obj.InternalThemeChangedListener = listener(obj,"ThemeChanged",... @(~,evt)onWidgetThemeChanged_I(obj)); % obj.WidgetTheme = obj.getTheme(); @@ -243,17 +243,14 @@ function onFieldEdited(obj,evt,fieldName,index) function color = getThemeColor(obj, semanticColorId) % Get color from theme and semantic variable - msg = "MATLAB R2025a or later is needed to call wt.abstract.BaseWidget.getThemeColor()."; - assert(~isMATLABReleaseOlderThan("R2025a"), msg) + msg = "MATLAB R2025a or later is needed to call wt.abstract.BaseViewController.getThemeColor()."; + assert(wt.utility.supportsUIThemes(), msg) % Get the theme theme = obj.getTheme(); % Get theme from semantic variable - % This is undocumented and may change. Better to call the - % getThemeColor method rather than reusing this directly. - color = matlab.graphics.internal.themes.getAttributeValue(... - theme, semanticColorId); + color = wt.utility.getThemeColor(theme, semanticColorId); end %function @@ -273,4 +270,4 @@ function onWidgetThemeChanged_I(obj) end %methods -end %classdef \ No newline at end of file +end %classdef diff --git a/widgets/+wt/+abstract/BaseWidget.m b/widgets/+wt/+abstract/BaseWidget.m index 113672fa..47eb6fe9 100644 --- a/widgets/+wt/+abstract/BaseWidget.m +++ b/widgets/+wt/+abstract/BaseWidget.m @@ -5,7 +5,7 @@ wt.mixin.ErrorHandling % Base class for a graphical widget - % Copyright 2020-2025 The MathWorks Inc. + % Copyright 2020-2026 The MathWorks Inc. %% Events @@ -85,7 +85,7 @@ function forceUpdate(obj, showMessage) obj = obj@matlab.ui.componentcontainer.ComponentContainer(args{:}); % Listen to theme changes (R2025a and later only) - if ~isMATLABReleaseOlderThan("R2025a") + if wt.utility.supportsUIThemes() obj.InternalThemeChangedListener = listener(obj,"ThemeChanged",... @(~,evt)onWidgetThemeChanged_I(obj)); % obj.WidgetTheme = obj.getTheme(); @@ -165,16 +165,13 @@ function updateBackgroundColorableComponents(obj) % Get color from theme and semantic variable msg = "MATLAB R2025a or later is needed to call wt.abstract.BaseWidget.getThemeColor()."; - assert(~isMATLABReleaseOlderThan("R2025a"), msg) + assert(wt.utility.supportsUIThemes(), msg) % Get the theme theme = obj.getTheme(); % Get theme from semantic variable - % This is undocumented and may change. Better to call the - % getThemeColor method rather than reusing this directly. - color = matlab.graphics.internal.themes.getAttributeValue(... - theme, semanticColorId); + color = wt.utility.getThemeColor(theme, semanticColorId); end %function @@ -206,4 +203,4 @@ function onWidgetThemeChanged_I(obj) end %methods -end %classdef \ No newline at end of file +end %classdef diff --git a/widgets/+wt/+apps/BaseApp.m b/widgets/+wt/+apps/BaseApp.m index c5f7c511..dcd3bad9 100644 --- a/widgets/+wt/+apps/BaseApp.m +++ b/widgets/+wt/+apps/BaseApp.m @@ -4,7 +4,7 @@ wt.mixin.ErrorHandling % Base class for Widgets Toolbox apps - % Copyright 2020-2025 The MathWorks, Inc. + % Copyright 2020-2026 The MathWorks, Inc. %% Events @@ -192,7 +192,7 @@ function forceUpdate(app) 'Visible','off'); % Listen to theme changes (R2025a and later only) - if ~isMATLABReleaseOlderThan("R2025a") + if wt.utility.supportsUIThemes() app.InternalThemeChangedListener = listener(app.Figure,"ThemeChanged",... @(~,evt)onThemeChanged_I(app)); end @@ -536,7 +536,7 @@ function updateTitle(app) % Get color from theme and semantic variable msg = "MATLAB R2025a or later is needed to call wt.apps.BaseApp.getThemeColor()."; - assert(~isMATLABReleaseOlderThan("R2025a"), msg) + assert(wt.utility.supportsUIThemes(), msg) % Get the theme try @@ -546,10 +546,7 @@ function updateTitle(app) end % Get theme from semantic variable - % This is undocumented and may change. Better to call the - % getThemeColor method rather than reusing this directly. - color = matlab.graphics.internal.themes.getAttributeValue(... - theme, semanticColorId); + color = wt.utility.getThemeColor(theme, semanticColorId); end %function @@ -644,4 +641,4 @@ function onThemeChanged_I(app) end %methods -end % classdef \ No newline at end of file +end % classdef diff --git a/widgets/+wt/+mixin/ButtonColorable.m b/widgets/+wt/+mixin/ButtonColorable.m index dc9aca8b..1a16fd6a 100644 --- a/widgets/+wt/+mixin/ButtonColorable.m +++ b/widgets/+wt/+mixin/ButtonColorable.m @@ -1,7 +1,7 @@ classdef ButtonColorable < handle % Mixin to add styles to a component - % Copyright 2020-2025 The MathWorks Inc. + % Copyright 2020-2026 The MathWorks Inc. %% Properties properties (AbortSet, Dependent) @@ -83,7 +83,7 @@ % Confirm BaseWidget and R2025a or newer if matches("WidgetThemeChanged", events(obj)) ... - && ~isMATLABReleaseOlderThan("R2025a") + && wt.utility.supportsUIThemes() % Listen to theme changes obj.ThemeChangedListener = ... @@ -143,7 +143,7 @@ function applyTheme(obj) % If color mode is auto, use standard theme color if obj.ButtonColorMode == "auto" ... - && ~isMATLABReleaseOlderThan("R2025a") + && wt.utility.supportsUIThemes() % Use standard theme color obj.ButtonColor_I = obj.getDefaultButtonColor(); @@ -154,4 +154,4 @@ function applyTheme(obj) end %methods -end %classdef \ No newline at end of file +end %classdef diff --git a/widgets/+wt/+mixin/FieldColorable.m b/widgets/+wt/+mixin/FieldColorable.m index b1d6eb1b..d851418b 100644 --- a/widgets/+wt/+mixin/FieldColorable.m +++ b/widgets/+wt/+mixin/FieldColorable.m @@ -1,7 +1,7 @@ classdef FieldColorable < handle % Mixin to add styles to a component - % Copyright 2020-2025 The MathWorks Inc. + % Copyright 2020-2026 The MathWorks Inc. %% Properties @@ -84,7 +84,7 @@ % Confirm BaseWidget and R2025a or newer if matches("WidgetThemeChanged", events(obj)) ... - && ~isMATLABReleaseOlderThan("R2025a") + && wt.utility.supportsUIThemes() % Listen to theme changes obj.ThemeChangedListener = ... @@ -143,7 +143,7 @@ function applyTheme(obj) % If color mode is auto, use standard theme color if obj.FieldColorMode == "auto" ... - && ~isMATLABReleaseOlderThan("R2025a") + && wt.utility.supportsUIThemes() % Use standard theme color obj.FieldColor_I = obj.getDefaultFieldColor(); @@ -155,4 +155,4 @@ function applyTheme(obj) end %methods -end %classdef \ No newline at end of file +end %classdef diff --git a/widgets/+wt/+mixin/FontColorable.m b/widgets/+wt/+mixin/FontColorable.m index 864f500d..4ab8b870 100644 --- a/widgets/+wt/+mixin/FontColorable.m +++ b/widgets/+wt/+mixin/FontColorable.m @@ -1,7 +1,7 @@ classdef FontColorable < handle % Mixin to add styles to a component - % Copyright 2020-2025 The MathWorks Inc. + % Copyright 2020-2026 The MathWorks Inc. %% Properties @@ -85,7 +85,7 @@ % Confirm BaseWidget and R2025a or newer if matches("WidgetThemeChanged", events(obj)) ... - && ~isMATLABReleaseOlderThan("R2025a") + && wt.utility.supportsUIThemes() % Listen to theme changes obj.ThemeChangedListener = ... @@ -145,7 +145,7 @@ function applyTheme(obj) % If color mode is auto, use standard theme color if obj.FontColorMode == "auto" ... - && ~isMATLABReleaseOlderThan("R2025a") + && wt.utility.supportsUIThemes() % Use standard theme color obj.FontColor_I = obj.getDefaultFontColor(); @@ -157,4 +157,4 @@ function applyTheme(obj) end %methods -end %classdef \ No newline at end of file +end %classdef diff --git a/widgets/+wt/+mixin/FontStyled.m b/widgets/+wt/+mixin/FontStyled.m index 320071f3..56c9ce93 100644 --- a/widgets/+wt/+mixin/FontStyled.m +++ b/widgets/+wt/+mixin/FontStyled.m @@ -1,7 +1,7 @@ classdef FontStyled < handle % Mixin for component with Font properties - % Copyright 2020-2025 The MathWorks Inc. + % Copyright 2020-2026 The MathWorks Inc. %% Properties @@ -122,7 +122,7 @@ % Confirm BaseWidget and R2025a or newer if matches("WidgetThemeChanged", events(obj)) ... - && ~isMATLABReleaseOlderThan("R2025a") + && wt.utility.supportsUIThemes() % Listen to theme changes obj.ThemeChangedListener = ... @@ -202,7 +202,7 @@ function applyTheme(obj) % If color mode is auto, use standard theme color if obj.FontColorMode == "auto" ... - && ~isMATLABReleaseOlderThan("R2025a") + && wt.utility.supportsUIThemes() % Use standard theme color obj.FontColor_I = obj.getDefaultFontColor(); @@ -214,4 +214,4 @@ function applyTheme(obj) end %methods -end %classdef \ No newline at end of file +end %classdef diff --git a/widgets/+wt/+mixin/Orderable.m b/widgets/+wt/+mixin/Orderable.m index e7521ef4..3b395bd1 100644 --- a/widgets/+wt/+mixin/Orderable.m +++ b/widgets/+wt/+mixin/Orderable.m @@ -148,7 +148,7 @@ % Define arguments arguments %(Input) % Total number of items in the list - numItems (1,1) double {mustBeInteger, mustBeNonnegative} + numItems (1,1) double {mustBeInteger, mustBeNonnegative} %#ok % Selected indices idxSel (1,:) double {mustBeInteger, mustBePositive, mustBeLessThanOrEqual(idxSel,numItems)} diff --git a/widgets/+wt/+mixin/SelectionColorable.m b/widgets/+wt/+mixin/SelectionColorable.m index 8c0322a4..f2f15c36 100644 --- a/widgets/+wt/+mixin/SelectionColorable.m +++ b/widgets/+wt/+mixin/SelectionColorable.m @@ -6,7 +6,7 @@ % update method. Selection is typically something that is handled % there. - % Copyright 2020-2025 The MathWorks Inc. + % Copyright 2020-2026 The MathWorks Inc. %% Properties @@ -70,7 +70,7 @@ % Confirm BaseWidget and R2025a or newer if matches("WidgetThemeChanged", events(obj)) ... - && ~isMATLABReleaseOlderThan("R2025a") + && wt.utility.supportsUIThemes() % Listen to theme changes obj.ThemeChangedListener = ... @@ -119,7 +119,7 @@ function applyTheme(obj) % If color mode is auto, use standard theme color if obj.SelectionColorMode == "auto" ... - && ~isMATLABReleaseOlderThan("R2025a") + && wt.utility.supportsUIThemes() % Use standard theme color obj.SelectionColor_I = obj.getDefaultSelectionColor(); @@ -131,4 +131,4 @@ function applyTheme(obj) end %methods -end %classdef \ No newline at end of file +end %classdef diff --git a/widgets/+wt/+mixin/SeparatorColorable.m b/widgets/+wt/+mixin/SeparatorColorable.m index 5bd6b9b5..9d0088e7 100644 --- a/widgets/+wt/+mixin/SeparatorColorable.m +++ b/widgets/+wt/+mixin/SeparatorColorable.m @@ -1,7 +1,7 @@ classdef SeparatorColorable < handle % Mixin to add styles to a component - % Copyright 2020-2025 The MathWorks Inc. + % Copyright 2020-2026 The MathWorks Inc. %% Properties @@ -84,7 +84,7 @@ % Confirm BaseWidget and R2025a or newer if matches("WidgetThemeChanged", events(obj)) ... - && ~isMATLABReleaseOlderThan("R2025a") + && wt.utility.supportsUIThemes() % Listen to theme changes obj.ThemeChangedListener = ... @@ -143,7 +143,7 @@ function applyTheme(obj) % If color mode is auto, use standard theme color if obj.SeparatorColorMode == "auto" ... - && ~isMATLABReleaseOlderThan("R2025a") + && wt.utility.supportsUIThemes() % Use standard theme color obj.SeparatorColor_I = obj.getDefaultSeparatorColor(); @@ -155,4 +155,4 @@ function applyTheme(obj) end %methods -end %classdef \ No newline at end of file +end %classdef diff --git a/widgets/+wt/+mixin/TitleColorable.m b/widgets/+wt/+mixin/TitleColorable.m index fb027f87..beb505d2 100644 --- a/widgets/+wt/+mixin/TitleColorable.m +++ b/widgets/+wt/+mixin/TitleColorable.m @@ -1,7 +1,7 @@ classdef TitleColorable < handle % Mixin to add styles to a component - % Copyright 2020-2025 The MathWorks Inc. + % Copyright 2020-2026 The MathWorks Inc. %% Properties @@ -84,7 +84,7 @@ % Confirm BaseWidget and R2025a or newer if matches("WidgetThemeChanged", events(obj)) ... - && ~isMATLABReleaseOlderThan("R2025a") + && wt.utility.supportsUIThemes() % Listen to theme changes obj.ThemeChangedListener = ... @@ -143,7 +143,7 @@ function applyTheme(obj) % If color mode is auto, use standard theme color if obj.TitleColorMode == "auto" ... - && ~isMATLABReleaseOlderThan("R2025a") + && wt.utility.supportsUIThemes() % Use standard theme color obj.TitleColor_I = obj.getDefaultTitleColor(); @@ -155,4 +155,4 @@ function applyTheme(obj) end %methods -end %classdef \ No newline at end of file +end %classdef diff --git a/widgets/+wt/+mixin/TitleFontStyled.m b/widgets/+wt/+mixin/TitleFontStyled.m index ae2eada0..9d3c8ce4 100644 --- a/widgets/+wt/+mixin/TitleFontStyled.m +++ b/widgets/+wt/+mixin/TitleFontStyled.m @@ -1,7 +1,7 @@ classdef TitleFontStyled < handle % Mixin for component with Font properties - % Copyright 2020-2025 The MathWorks Inc. + % Copyright 2020-2026 The MathWorks Inc. %% Properties @@ -122,7 +122,7 @@ % Confirm BaseWidget and R2025a or newer if matches("WidgetThemeChanged", events(obj)) ... - && ~isMATLABReleaseOlderThan("R2025a") + && wt.utility.supportsUIThemes() % Listen to theme changes obj.ThemeChangedListener = ... @@ -228,7 +228,7 @@ function applyTheme(obj) % If color mode is auto, use standard theme color if obj.TitleColorMode == "auto" ... - && ~isMATLABReleaseOlderThan("R2025a") + && wt.utility.supportsUIThemes() % Use standard theme color obj.TitleColor_I = obj.getDefaultTitleColor(); @@ -240,4 +240,4 @@ function applyTheme(obj) end %methods -end %classdef \ No newline at end of file +end %classdef diff --git a/widgets/+wt/+model/BaseModel.m b/widgets/+wt/+model/BaseModel.m index 84de642e..6e9eabdb 100644 --- a/widgets/+wt/+model/BaseModel.m +++ b/widgets/+wt/+model/BaseModel.m @@ -14,7 +14,7 @@ % notifications in a hierarchy of BaseWidget classes % - % Copyright 2020-2025 The MathWorks, Inc. + % Copyright 2020-2026 The MathWorks, Inc. @@ -360,8 +360,11 @@ function onPropChanged(obj,evt) % evtOutM.Stack = {obj}; % evtOutM.ClassStack = class(obj); - % Revise listeners for model changes given the new value - if isa(evtOutM.Value, "wt.model.BaseModel") + % Revise listeners for aggregated model changes given the new value + changedProp = string(evt.Source.Name); + if any(changedProp == obj.getAggregatedModelProperties()) + obj.attachModelListeners(); + elseif isa(evtOutM.Value, "wt.model.BaseModel") evtOutM.Model.attachModelListeners(); end @@ -426,6 +429,9 @@ function attachModelListeners(obj) % Get the properties to listen for propNames = obj.getAggregatedModelProperties(); + % Replace old aggregated model listeners when properties change. + obj.clearModelListeners(); + if obj.Debug if isempty(propNames) propDisp = ""; @@ -536,4 +542,4 @@ function clearModelListeners(obj) end %methods -end %classdef \ No newline at end of file +end %classdef diff --git a/widgets/+wt/+utility/getThemeColor.m b/widgets/+wt/+utility/getThemeColor.m new file mode 100644 index 00000000..27e1fa26 --- /dev/null +++ b/widgets/+wt/+utility/getThemeColor.m @@ -0,0 +1,17 @@ +function color = getThemeColor(theme, semanticColorId) +% Get an RGB color from a uifigure theme semantic color identifier. + +% Copyright 2026 The MathWorks, Inc. + +arguments + theme + semanticColorId (1,1) string +end + +msg = "MATLAB R2025a or later is needed to call wt.utility.getThemeColor()."; +assert(wt.utility.supportsUIThemes(), msg) + +% Keep the internal theme API isolated to this compatibility helper. +color = matlab.graphics.internal.themes.getAttributeValue(theme, semanticColorId); + +end diff --git a/widgets/+wt/+utility/supportsUIThemes.m b/widgets/+wt/+utility/supportsUIThemes.m new file mode 100644 index 00000000..34525814 --- /dev/null +++ b/widgets/+wt/+utility/supportsUIThemes.m @@ -0,0 +1,8 @@ +function tf = supportsUIThemes() +% True when MATLAB supports uifigure themes. + +% Copyright 2026 The MathWorks, Inc. + +tf = ~isMATLABReleaseOlderThan("R2025a"); + +end diff --git a/widgets/+wt/Toolbar.m b/widgets/+wt/Toolbar.m index 91de4801..aa42d6bb 100644 --- a/widgets/+wt/Toolbar.m +++ b/widgets/+wt/Toolbar.m @@ -5,7 +5,7 @@ % A configurable toolbar - % Copyright 2020-2025 The MathWorks Inc. + % Copyright 2020-2026 The MathWorks Inc. %% Events events (HasCallbackProperty, NotifyAccess = protected) @@ -106,7 +106,7 @@ function setup(obj) % Adjust default title fonts for toolbar purposes obj.TitleFontSize = 10; obj.TitleFontWeight = "normal"; - if isMATLABReleaseOlderThan("R2025a") + if ~wt.utility.supportsUIThemes() obj.TitleColor = [0.5333 0.5333 0.5333]; end @@ -460,4 +460,4 @@ function onPanelButtonPushed(obj,e) 0 0 0 0 1 0 0 0 0 ]); -end %function \ No newline at end of file +end %function diff --git a/widgets/examples/WidgetsExampleApp.mlapp b/widgets/examples/WidgetsExampleApp.mlapp index 977c7f9c..edee41ba 100644 Binary files a/widgets/examples/WidgetsExampleApp.mlapp and b/widgets/examples/WidgetsExampleApp.mlapp differ diff --git a/widgets/examples/templates/TemplateBaseApp.m b/widgets/examples/templates/TemplateBaseApp.m index 8944df06..b47e9865 100644 --- a/widgets/examples/templates/TemplateBaseApp.m +++ b/widgets/examples/templates/TemplateBaseApp.m @@ -3,6 +3,7 @@ % Copyright 2022-2025 The MathWorks Inc. + %#ok<*INUSD,*MANU> %% Internal Components % Create properties here for each control, layout, or view component diff --git a/widgets/examples/templates/TemplateBaseMultiSessionApp.m b/widgets/examples/templates/TemplateBaseMultiSessionApp.m index 73fda5d6..03b5c04a 100644 --- a/widgets/examples/templates/TemplateBaseMultiSessionApp.m +++ b/widgets/examples/templates/TemplateBaseMultiSessionApp.m @@ -3,6 +3,8 @@ % Copyright 2022-2025 The MathWorks Inc. + %#ok<*INUSD,*MANU> + %% Internal Components % Create properties here for each control, layout, or view component % that will be placed directly into the main app window. diff --git a/widgets/examples/templates/TemplateBaseSingleSessionApp.m b/widgets/examples/templates/TemplateBaseSingleSessionApp.m index 6b10fc0f..42b0df31 100644 --- a/widgets/examples/templates/TemplateBaseSingleSessionApp.m +++ b/widgets/examples/templates/TemplateBaseSingleSessionApp.m @@ -1,7 +1,9 @@ classdef TemplateBaseSingleSessionApp < wt.apps.BaseSingleSessionApp % Implements a template for a BaseSingleSessionApp - % Copyright 2022-2025 The MathWorks Inc. + % Copyright 2022-2026 The MathWorks Inc. + + %#ok<*INUSD,*MANU> %% Internal Components @@ -207,7 +209,7 @@ function update(app) % Update the session description text app.SessionDescription.Value = app.Session.Description; - + % Update toolbar button enables app.updateToolbarEnables() @@ -298,4 +300,4 @@ function onHelpButton(app) end %methods -end %classdef \ No newline at end of file +end %classdef diff --git a/widgets/templates/TemplateBaseApp.m b/widgets/templates/TemplateBaseApp.m index 11f53584..f4206e59 100644 --- a/widgets/templates/TemplateBaseApp.m +++ b/widgets/templates/TemplateBaseApp.m @@ -1,7 +1,9 @@ classdef TemplateBaseApp < wt.apps.BaseApp % Implements a template for a BaseApp -% Copyright 2020-2025 The MathWorks Inc. + % Copyright 2020-2025 The MathWorks Inc. + + %#ok<*INUSD,*MANU> %% Internal Components diff --git a/widgets/templates/TemplateBaseSingleSessionApp.m b/widgets/templates/TemplateBaseSingleSessionApp.m index 6a1c5fc9..f9377e82 100644 --- a/widgets/templates/TemplateBaseSingleSessionApp.m +++ b/widgets/templates/TemplateBaseSingleSessionApp.m @@ -1,7 +1,9 @@ classdef TemplateBaseSingleSessionApp < wt.apps.BaseSingleSessionApp % Implements a template for a BaseSingleSessionApp -% Copyright 2020-2025 The MathWorks Inc. + % Copyright 2020-2025 The MathWorks Inc. + + %#ok<*INUSD,*MANU> %% Internal Components