diff --git a/AGENTS.md b/AGENTS.md index 60da6807..b85972c4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -98,6 +98,14 @@ When creating a new MathWorks-owned file: - Add the standard MathWorks copyright notice. - Match the format used by similar files in the repository. +For contributor-owned or externally contributed files: + +- Preserve existing contributor copyright and license notices. +- Do not add a MathWorks copyright notice unless ownership is explicitly + confirmed by maintainers. +- If an external-contributor file has no notice, leave the notice unchanged + and treat missing attribution as a licensing review question. + Exception: - If a file contains a third-party copyright notice that is not owned by MathWorks, preserve the existing copyright and licensing information. @@ -348,4 +356,4 @@ 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 +Do not introduce confidential customer information, internal project details, credentials, or proprietary code not already contained within the repository. diff --git a/deploy/wtDeployVersion.txt b/deploy/wtDeployVersion.txt index 59aa62c1..437459cd 100644 --- a/deploy/wtDeployVersion.txt +++ b/deploy/wtDeployVersion.txt @@ -1 +1 @@ -2.4.5 +2.5.0 diff --git a/release/Widgets Toolbox 2.5.0.mltbx b/release/Widgets Toolbox 2.5.0.mltbx new file mode 100644 index 00000000..d15bbfd1 Binary files /dev/null and b/release/Widgets Toolbox 2.5.0.mltbx differ diff --git a/resources/project/FVIFMHuse2VWlFgbqX2S_OQzxiU/aGwJeQZnd-mOnvPxR2SlTQNwcNkd.xml b/resources/project/FVIFMHuse2VWlFgbqX2S_OQzxiU/aGwJeQZnd-mOnvPxR2SlTQNwcNkd.xml new file mode 100644 index 00000000..4356a6ae --- /dev/null +++ b/resources/project/FVIFMHuse2VWlFgbqX2S_OQzxiU/aGwJeQZnd-mOnvPxR2SlTQNwcNkd.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/FVIFMHuse2VWlFgbqX2S_OQzxiU/aGwJeQZnd-mOnvPxR2SlTQNwcNkp.xml b/resources/project/FVIFMHuse2VWlFgbqX2S_OQzxiU/aGwJeQZnd-mOnvPxR2SlTQNwcNkp.xml new file mode 100644 index 00000000..45e178b8 --- /dev/null +++ b/resources/project/FVIFMHuse2VWlFgbqX2S_OQzxiU/aGwJeQZnd-mOnvPxR2SlTQNwcNkp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/NdsL0Nh4wtc5dxoI0TU1uVF-B00/KBOHiLXFz6XgUJzAHA_ukXjHSn4d.xml b/resources/project/NdsL0Nh4wtc5dxoI0TU1uVF-B00/KBOHiLXFz6XgUJzAHA_ukXjHSn4d.xml new file mode 100644 index 00000000..4356a6ae --- /dev/null +++ b/resources/project/NdsL0Nh4wtc5dxoI0TU1uVF-B00/KBOHiLXFz6XgUJzAHA_ukXjHSn4d.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/NdsL0Nh4wtc5dxoI0TU1uVF-B00/KBOHiLXFz6XgUJzAHA_ukXjHSn4p.xml b/resources/project/NdsL0Nh4wtc5dxoI0TU1uVF-B00/KBOHiLXFz6XgUJzAHA_ukXjHSn4p.xml new file mode 100644 index 00000000..35442286 --- /dev/null +++ b/resources/project/NdsL0Nh4wtc5dxoI0TU1uVF-B00/KBOHiLXFz6XgUJzAHA_ukXjHSn4p.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/NdsL0Nh4wtc5dxoI0TU1uVF-B00/jmSeTFRuzXEKQEpXFs77_ZoPzA0d.xml b/resources/project/NdsL0Nh4wtc5dxoI0TU1uVF-B00/jmSeTFRuzXEKQEpXFs77_ZoPzA0d.xml new file mode 100644 index 00000000..4356a6ae --- /dev/null +++ b/resources/project/NdsL0Nh4wtc5dxoI0TU1uVF-B00/jmSeTFRuzXEKQEpXFs77_ZoPzA0d.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/NdsL0Nh4wtc5dxoI0TU1uVF-B00/jmSeTFRuzXEKQEpXFs77_ZoPzA0p.xml b/resources/project/NdsL0Nh4wtc5dxoI0TU1uVF-B00/jmSeTFRuzXEKQEpXFs77_ZoPzA0p.xml new file mode 100644 index 00000000..ed4ac3c2 --- /dev/null +++ b/resources/project/NdsL0Nh4wtc5dxoI0TU1uVF-B00/jmSeTFRuzXEKQEpXFs77_ZoPzA0p.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/QfRan8VIS8FJWu9sq7_nfr0NcFw/RisM0YgOPCrcJTdEFBZG82yj6SEd.xml b/resources/project/QfRan8VIS8FJWu9sq7_nfr0NcFw/RisM0YgOPCrcJTdEFBZG82yj6SEd.xml new file mode 100644 index 00000000..99772b42 --- /dev/null +++ b/resources/project/QfRan8VIS8FJWu9sq7_nfr0NcFw/RisM0YgOPCrcJTdEFBZG82yj6SEd.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/resources/project/QfRan8VIS8FJWu9sq7_nfr0NcFw/RisM0YgOPCrcJTdEFBZG82yj6SEp.xml b/resources/project/QfRan8VIS8FJWu9sq7_nfr0NcFw/RisM0YgOPCrcJTdEFBZG82yj6SEp.xml new file mode 100644 index 00000000..9bacd347 --- /dev/null +++ b/resources/project/QfRan8VIS8FJWu9sq7_nfr0NcFw/RisM0YgOPCrcJTdEFBZG82yj6SEp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/6HCO7Foijl2GvlPW46F3ZM_cQLgd.xml b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/6HCO7Foijl2GvlPW46F3ZM_cQLgd.xml new file mode 100644 index 00000000..99772b42 --- /dev/null +++ b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/6HCO7Foijl2GvlPW46F3ZM_cQLgd.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/6HCO7Foijl2GvlPW46F3ZM_cQLgp.xml b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/6HCO7Foijl2GvlPW46F3ZM_cQLgp.xml new file mode 100644 index 00000000..3d80f384 --- /dev/null +++ b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/6HCO7Foijl2GvlPW46F3ZM_cQLgp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/B47jLUwAaIUm6crXBMKlb3Q6COwd.xml b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/B47jLUwAaIUm6crXBMKlb3Q6COwd.xml new file mode 100644 index 00000000..99772b42 --- /dev/null +++ b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/B47jLUwAaIUm6crXBMKlb3Q6COwd.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/B47jLUwAaIUm6crXBMKlb3Q6COwp.xml b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/B47jLUwAaIUm6crXBMKlb3Q6COwp.xml new file mode 100644 index 00000000..79fcab93 --- /dev/null +++ b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/B47jLUwAaIUm6crXBMKlb3Q6COwp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/_4YOR5tEgAGGcZwXZclzXwFW-GAd.xml b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/_4YOR5tEgAGGcZwXZclzXwFW-GAd.xml new file mode 100644 index 00000000..378b6137 --- /dev/null +++ b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/_4YOR5tEgAGGcZwXZclzXwFW-GAd.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/_4YOR5tEgAGGcZwXZclzXwFW-GAp.xml b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/_4YOR5tEgAGGcZwXZclzXwFW-GAp.xml new file mode 100644 index 00000000..3e7102a3 --- /dev/null +++ b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/_4YOR5tEgAGGcZwXZclzXwFW-GAp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/xqI62fLx--N2sol_JNm27WKGd4od.xml b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/xqI62fLx--N2sol_JNm27WKGd4od.xml new file mode 100644 index 00000000..378b6137 --- /dev/null +++ b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/xqI62fLx--N2sol_JNm27WKGd4od.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/xqI62fLx--N2sol_JNm27WKGd4op.xml b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/xqI62fLx--N2sol_JNm27WKGd4op.xml new file mode 100644 index 00000000..e0b15404 --- /dev/null +++ b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/xqI62fLx--N2sol_JNm27WKGd4op.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/elO4D6tOV7Lp-Jfo7ptgr2NlB30/Lt1EHy842jSV1u9vDUKkes8nLXUd.xml b/resources/project/elO4D6tOV7Lp-Jfo7ptgr2NlB30/Lt1EHy842jSV1u9vDUKkes8nLXUd.xml new file mode 100644 index 00000000..99772b42 --- /dev/null +++ b/resources/project/elO4D6tOV7Lp-Jfo7ptgr2NlB30/Lt1EHy842jSV1u9vDUKkes8nLXUd.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/resources/project/elO4D6tOV7Lp-Jfo7ptgr2NlB30/Lt1EHy842jSV1u9vDUKkes8nLXUp.xml b/resources/project/elO4D6tOV7Lp-Jfo7ptgr2NlB30/Lt1EHy842jSV1u9vDUKkes8nLXUp.xml new file mode 100644 index 00000000..3d80f384 --- /dev/null +++ b/resources/project/elO4D6tOV7Lp-Jfo7ptgr2NlB30/Lt1EHy842jSV1u9vDUKkes8nLXUp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/elO4D6tOV7Lp-Jfo7ptgr2NlB30/Nf4dyIZxIDorh2EoUV6nzvPsAOgd.xml b/resources/project/elO4D6tOV7Lp-Jfo7ptgr2NlB30/Nf4dyIZxIDorh2EoUV6nzvPsAOgd.xml new file mode 100644 index 00000000..99772b42 --- /dev/null +++ b/resources/project/elO4D6tOV7Lp-Jfo7ptgr2NlB30/Nf4dyIZxIDorh2EoUV6nzvPsAOgd.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/resources/project/elO4D6tOV7Lp-Jfo7ptgr2NlB30/Nf4dyIZxIDorh2EoUV6nzvPsAOgp.xml b/resources/project/elO4D6tOV7Lp-Jfo7ptgr2NlB30/Nf4dyIZxIDorh2EoUV6nzvPsAOgp.xml new file mode 100644 index 00000000..79fcab93 --- /dev/null +++ b/resources/project/elO4D6tOV7Lp-Jfo7ptgr2NlB30/Nf4dyIZxIDorh2EoUV6nzvPsAOgp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/test/+wt/+test/AppDesignerMetadata.m b/test/+wt/+test/AppDesignerMetadata.m new file mode 100644 index 00000000..2080354f --- /dev/null +++ b/test/+wt/+test/AppDesignerMetadata.m @@ -0,0 +1,159 @@ +classdef AppDesignerMetadata < matlab.unittest.TestCase + % Verify App Designer component metadata. + +% Copyright 2026 The MathWorks, Inc. + + methods (Test) + + function testRequiredComponentFields(testCase) + + metadataText = fileread(testCase.getMetadataFile()); + testCase.assertNotEmpty(metadataText) + + % R2025b's registration framework rejects this resource when + % appDesigner.json has a final line terminator. + finalChar = double(metadataText(end)); + testCase.verifyNotEqual(finalChar, double(newline)) + testCase.verifyNotEqual(finalChar, double(sprintf('\r'))) + + metadata = jsondecode(metadataText); + testCase.assertTrue(isfield(metadata, "components")) + + components = testCase.toCell(metadata.components); + requiredFields = [ + "componentName" + "version" + "description" + "category" + "icon" + "authorName" + "authorEmail" + "className" + "avatar" + "avatarDark" + "defaultPosition" + ]; + + classNames = strings(numel(components), 1); + for idx = 1:numel(components) + component = components{idx}; + classNames(idx) = string(component.className); + missingFields = setdiff(requiredFields, string(fieldnames(component))); + testCase.verifyEmpty(missingFields, ... + "Missing App Designer metadata field(s) for " + classNames(idx)) + end + + testCase.verifyComponentName(components, "wt.DateSlider", ... + "Date Slider (R2023b+)") + testCase.verifyComponentDefaultWidth(components, ... + "wt.DateSlider", 600) + testCase.verifyComponentName(components, "wt.DateRangeSlider", ... + "Date Range Slider (R2024b+)") + testCase.verifyComponentDefaultWidth(components, ... + "wt.DateRangeSlider", 600) + + end %function + + function testRegistrationFrameworkCanParseMetadata(testCase) + + testCase.assumeNotEmpty( ... + meta.class.fromName('matlab.internal.regfwk.ResourceSpecification'), ... + "MATLAB registration framework is not available.") + + metadataRoot = testCase.getMetadataRoot(); + metadataFile = testCase.getMetadataFile(); + + spec = matlab.internal.regfwk.ResourceSpecification( ... + struct('ResourceName', 'appDesigner', 'ResourceType', 'json')); + + matlab.internal.regfwk.enableResources(metadataRoot); + resourceInfo = matlab.internal.regfwk.getResourceList(spec, 'enabled'); + + resourceFiles = string({resourceInfo.resourcesFile}); + metadataIdx = find(strcmpi(resourceFiles, metadataFile), 1); + testCase.assertNotEmpty(metadataIdx, ... + "App Designer metadata was not registered.") + + contents = resourceInfo(metadataIdx).resourcesFileContents; + testCase.assertTrue(isstruct(contents), ... + "App Designer metadata did not parse through the registration framework.") + + components = testCase.toCell(contents.components); + classNames = string(cellfun(@(component) component.className, ... + components, 'UniformOutput', false)); + testCase.verifyTrue(any(classNames == "wt.DateSlider"), ... + "wt.DateSlider was not present in registered metadata.") + testCase.verifyTrue(any(classNames == "wt.DateRangeSlider"), ... + "wt.DateRangeSlider was not present in registered metadata.") + + end %function + + end %methods + + methods (Access = private) + + function metadataRoot = getMetadataRoot(testCase) + + metadataRoot = fullfile(testCase.getProjectRoot(), "widgets"); + + end %function + + function metadataFile = getMetadataFile(testCase) + + metadataFile = fullfile(testCase.getMetadataRoot(), ... + "resources", "appDesigner.json"); + + end %function + + function verifyComponentName(testCase, components, className, ... + componentName) + + component = testCase.getComponent(components, className); + testCase.verifyEqual( ... + string(component.componentName), ... + componentName) + + end %function + + function verifyComponentDefaultWidth(testCase, components, className, ... + defaultWidth) + + component = testCase.getComponent(components, className); + defaultPosition = reshape(component.defaultPosition, 1, []); + testCase.verifyEqual(defaultPosition(3), defaultWidth) + + end %function + + function component = getComponent(testCase, components, className) + + classNames = string(cellfun(@(component) component.className, ... + components, 'UniformOutput', false)); + componentIdx = find(classNames == className, 1); + testCase.assertNotEmpty(componentIdx, ... + className + " is missing from App Designer metadata.") + component = components{componentIdx}; + + end %function + + end %methods + + methods (Static, Access = private) + + function projectRoot = getProjectRoot() + + projectRoot = fileparts(fileparts(fileparts( ... + fileparts(mfilename('fullpath'))))); + + end %function + + function components = toCell(components) + + if isstruct(components) + components = num2cell(components); + end + + end %function + + end %methods + +end %classdef diff --git a/test/+wt/+test/DateRangeSlider.m b/test/+wt/+test/DateRangeSlider.m new file mode 100644 index 00000000..e3e2b9cc --- /dev/null +++ b/test/+wt/+test/DateRangeSlider.m @@ -0,0 +1,561 @@ +classdef DateRangeSlider < wt.test.BaseWidgetTest + % Implements a unit test for a widget or component + +% Copyright 2026 The MathWorks, Inc. + + %% Test Method Setup + methods (TestMethodSetup) + + function setup(testCase) + + % Assure the correct MATLAB version is present + assumeMinimumRelease(testCase, "R2024b") + + % Call superclass method + testCase.setup@wt.test.BaseWidgetTest(); + + % Change figure size to accomodate for slider ticks + testCase.Figure.Position(3) = 800; + + % Setup widget + fcn = @()wt.DateRangeSlider(testCase.Grid); + testCase.Widget = verifyWarningFree(testCase,fcn); + + % Set callback + testCase.Widget.ValueChangedFcn = @(s,e)onCallbackTriggered(testCase,e); + + % Ensure it renders + drawnow + + end %function + + end %methods + + + %% Unit Tests + methods (Test) + + function testValueProperty(testCase) + + % Set the value + newValue = datetime("01-Jan-2020") + [days(0) days(2)]; + testCase.verifySetProperty("Value", newValue); + testCase.verifyControlValues(newValue); + + % Set the value + newValue = datetime("01-Jan-2020") + [days(1) days(3)]; + testCase.verifySetProperty("Value", newValue); + testCase.verifyControlValues(newValue); + + end %function + + + function testValueBoundaries(testCase) + + % Configure the control + expValue = datetime("yesterday") + [-days(20) -days(19)]; + newLimits = datetime("yesterday") + [-days(20) days(15)]; + testCase.verifySetProperty("Limits", newLimits); + testCase.verifyControlLimits(newLimits); + testCase.verifyControlValues(expValue); + + % Set the value within limits + newValue = datetime("yesterday") + [days(10) days(15)]; + testCase.verifySetProperty("Value", newValue); + testCase.verifyControlValues(newValue); + + % Change the limits out of bounds + newLimits = datetime("yesterday") + [days(20) days(30)]; + testCase.verifySetProperty("Limits", newLimits); + testCase.verifyControlLimits(newLimits); + testCase.verifyControlValues([newLimits(1) newLimits(1) + testCase.Widget.MinGap]); + + % Set an invalid value + expValue = [datetime("yesterday") + days(20) datetime("yesterday") + days(20) + testCase.Widget.MinGap]; + invldValue = [datetime("yesterday") + days(31) datetime("yesterday") + days(32)]; + testCase.verifySetPropertyError("Value", invldValue, 'MATLAB:validators:mustBeInRange'); + testCase.verifyControlValues(expValue); + + % Set an invalid value + expValue = [datetime("yesterday") + days(20) datetime("yesterday") + days(20) + testCase.Widget.MinGap]; + invldValue = [datetime("yesterday") + days(15) datetime("yesterday") + days(25)]; + testCase.verifySetPropertyError("Value", invldValue, 'MATLAB:validators:mustBeInRange'); + testCase.verifyControlValues(expValue); + + end %function + + function testDatepicker(testCase) + + % Configure the control + expValue = datetime("today") + [-days(20) -days(19)]; + newLimits = [datetime("today") datetime("today")] + [-days(20) days(15)]; + testCase.verifySetProperty("Limits", newLimits); + testCase.verifyControlLimits(newLimits); + testCase.verifyControlValues(expValue); + + % Enable the datepicker to be able to type + testCase.Widget.DatepickerLeft.Editable = true; + testCase.Widget.DatepickerRight.Editable = true; + + % Pick a date using datepicker (right) + newValue = datetime("today") + [-days(20) days(15)]; + testCase.verifyTypeAction(testCase.Widget.DatepickerRight, newValue(2), "Value", newValue) + testCase.verifyControlValues(newValue); + testCase.verifyControlLimits(newLimits); + testCase.verifyButtonsEnabled([true false false true]) + testCase.verifyCallbackCount(1); + + newValue = datetime("today") + [-days(20) days(10)]; + testCase.verifyTypeAction(testCase.Widget.DatepickerRight, newValue(2), "Value", newValue) + testCase.verifyControlValues(newValue); + testCase.verifyControlLimits(newLimits); + testCase.verifyButtonsEnabled([true false true true]) + testCase.verifyCallbackCount(2); + + % Pick a date using datepicker (left) + newValue = datetime("today") + [days(9) days(10)]; + testCase.verifyTypeAction(testCase.Widget.DatepickerLeft, newValue(1), "Value", newValue) + testCase.verifyControlValues(newValue); + testCase.verifyControlLimits(newLimits); + testCase.verifyButtonsEnabled([false true true false]) + testCase.verifyCallbackCount(3); + + newValue = datetime("today") + [-days(10) days(10)]; + testCase.verifyTypeAction(testCase.Widget.DatepickerLeft, newValue(1), "Value", newValue) + testCase.verifyControlValues(newValue); + testCase.verifyControlLimits(newLimits); + testCase.verifyButtonsEnabled([true true true true]) + testCase.verifyCallbackCount(4); + + + % Pick an out-of-range date using datepicker + expValue = datetime("today") + [-days(10) days(10)]; + + invldValue = datetime("today") + days(10); + testCase.verifyTypeAction(testCase.Widget.DatepickerLeft, invldValue, "Value", expValue) + testCase.verifyControlValues(expValue); + testCase.verifyControlLimits(newLimits); + testCase.verifyButtonsEnabled([true true true true]) + testCase.verifyCallbackCount(4); + + invldValue = datetime("today") - days(10); + testCase.verifyTypeAction(testCase.Widget.DatepickerRight, invldValue, "Value", expValue) + testCase.verifyControlValues(expValue); + testCase.verifyControlLimits(newLimits); + testCase.verifyButtonsEnabled([true true true true]) + testCase.verifyCallbackCount(4); + + end + + function testDatepickerButtons(testCase) + + % Configure the control + newLimits = datetime("yesterday") + [days(0) days(10)]; + expValue = newLimits; + testCase.verifySetProperty("Limits", newLimits); + testCase.verifySetProperty("Value", expValue); + testCase.verifyControlLimits(newLimits); + testCase.verifyControlValues(expValue); + + % Verify buttons enabled state + testCase.verifyButtonsEnabled([true false false true]) + + % Push down is disabled + testCase.verifyButtonPushAction("left", "down", expValue) + testCase.verifyButtonPushAction("right", "up", expValue) + testCase.verifyCallbackCount(0); + + % Push the button up once + testCase.verifyButtonPushAction("left", "up", expValue + days([1 0])) + testCase.verifyButtonPushAction("right", "down", expValue + days([1 -1])) + testCase.verifyControlValues(expValue + days([1 -1])); + testCase.verifyControlLimits(newLimits); + testCase.verifyButtonsEnabled([true true true true]) + testCase.verifyCallbackCount(2); + + % Change step size and push down + testCase.Widget.Step = caldays(4); + testCase.verifyButtonPushAction("left", "down", expValue + days([0 -1])) + testCase.verifyButtonsEnabled([true false true true]) + testCase.verifyCallbackCount(3); + + % Push up + testCase.verifyButtonPushAction("left", "up", expValue + days([4 -1])) + testCase.verifyButtonPushAction("left", "up", expValue + days([8 -1])) + testCase.verifyCallbackCount(5); + + % Push up disabled at this point + testCase.verifyButtonsEnabled([false true true false]) + testCase.verifyButtonPushAction("left", "up", expValue + days([8 -1])) + testCase.verifyCallbackCount(5); + + % Push down + testCase.verifyButtonPushAction("left", "down", expValue + days([4 -1])) + testCase.verifyButtonPushAction("left", "down", expValue + days([0 -1])) + testCase.verifyButtonsEnabled([true false true true]) + testCase.verifyCallbackCount(7); + + % Push up + testCase.verifyButtonPushAction("right", "up", expValue + days([0 0])) + testCase.verifyButtonsEnabled([true false false true]) + testCase.verifyCallbackCount(8); + + % Push down + testCase.verifyButtonPushAction("right", "down", expValue + days([0 -4])) + testCase.verifyButtonPushAction("right", "down", expValue + days([0 -8])) + testCase.verifyButtonPushAction("right", "down", expValue + days([0 -9])) + testCase.verifyButtonsEnabled([false false true false]) + testCase.verifyControlValues(expValue + days([0 -9])); + testCase.verifyControlLimits(newLimits); + testCase.verifyCallbackCount(11); + + end + + function testTickLabels(testCase) + + % Change the limits + newLimits = datetime("today") + days([5 10]); + testCase.verifySetProperty("Limits", newLimits); + testCase.verifyControlLimits(newLimits); + testCase.verifyControlValues(datetime("today") + days([5 6])); + + % Allow some time for the component to catch up + drawnow + + actualLabels = testCase.Widget.Slider.MajorTickLabels; + actualLabels = convertCharsToStrings(actualLabels); + actualTicks = testCase.Widget.Slider.MajorTicks; + expLabels = string(newLimits(1) + days(actualTicks - 1)); + testCase.verifySliderTicks(actualTicks) + testCase.verifyEqual(actualTicks(1), 1) + testCase.verifyEqual(actualTicks(end), 6) + testCase.verifyEqual(actualLabels, expLabels) + + % Change the limits + newLimits = datetime("today") + days([0 10]); + testCase.verifySetProperty("Limits", newLimits); + testCase.verifyControlLimits(newLimits); + testCase.verifyControlValues(datetime("today") + days([5 6])); + + % Allow some time for the component to catch up + drawnow + + % Check tick labels + actualLabels = testCase.Widget.Slider.MajorTickLabels; + actualLabels = convertCharsToStrings(actualLabels); + actualTicks = testCase.Widget.Slider.MajorTicks; + expLabels = string(newLimits(1) + days(actualTicks - 1)); + testCase.verifySliderTicks(actualTicks) + testCase.verifyEqual(actualTicks(1), 1) + testCase.verifyEqual(actualTicks(end), 11) + testCase.verifyEqual(numel(actualLabels), numel(actualTicks)) + testCase.verifyEqual(actualLabels, expLabels) + + % Change datepicker size so that slider barely fits + testCase.Widget.DatepickerSize = 220; + + % Allow some time for the component to catch up + drawnow + + % Change the limits + newLimits = datetime("today") + days([0 20]); + testCase.verifySetProperty("Limits", newLimits); + testCase.verifyControlLimits(newLimits); + testCase.verifyControlValues(datetime("today") + days([5 6])); + + % Allow some time for the component to catch up + drawnow + + % Check ticks remain valid when fewer labels fit + actualLabels = testCase.Widget.Slider.MajorTickLabels; + actualLabels = convertCharsToStrings(actualLabels); + actualTicks = testCase.Widget.Slider.MajorTicks; + expLabels = string(newLimits(1) + days(actualTicks - 1)); + testCase.verifySliderTicks(actualTicks) + testCase.verifyEqual(actualTicks(1), 1) + testCase.verifyEqual(actualTicks(end), 21) + testCase.verifyEqual(numel(actualLabels), numel(actualTicks)) + testCase.verifyEqual(actualLabels, expLabels) + + end %function + + + function testMinGap(testCase) + + % Configure the control + newLimits = datetime("today") + days([50 100]); + newValue = datetime("today") + days([50 50]); + testCase.verifySetProperty("MinGap", 0, days(0)); + testCase.verifySetProperty("Limits", newLimits); + testCase.verifyControlLimits(newLimits); + testCase.verifyControlValues(newValue) + + % Change the minimum gap + minGap = 10; + newValue = datetime("today") + days(50 + [0 minGap]); + testCase.verifySetProperty("MinGap", minGap, days(minGap)); + testCase.verifyControlValues(newValue) + testCase.verifyButtonsEnabled(["off" "off" "on" "off"]) + + % Change the value + newValue = datetime("today") + days(70 + [-1 minGap + 1]); + testCase.verifySetProperty("Value", newValue); + testCase.verifyControlValues(newValue) + testCase.verifyButtonsEnabled(["on" "on" "on" "on"]) + + newValue = datetime("today") + days(70 + [0 minGap]); + testCase.verifySetProperty("Value", newValue); + testCase.verifyControlValues(newValue) + testCase.verifyButtonsEnabled(["off" "on" "on" "off"]) + + % Increase gap + minGap = 20; + newValue = datetime("today") + days(70 + [0 minGap]); + testCase.verifySetProperty("MinGap", minGap, days(minGap)); + testCase.verifyControlValues(newValue) + + minGap = 30; + newValue = datetime("today") + days(70 + [0 minGap]); + testCase.verifySetProperty("MinGap", minGap, days(minGap)); + testCase.verifyControlValues(newValue) + testCase.verifyButtonsEnabled(["off" "on" "off" "off"]) + + % Increate gap even further + minGap = 40; + newValue = datetime("today") + days(60 + [0 minGap]); + testCase.verifySetProperty("MinGap", minGap, days(minGap)); + testCase.verifyControlValues(newValue) + + minGap = 50; + newValue = datetime("today") + days(50 + [0 minGap]); + testCase.verifySetProperty("MinGap", minGap, days(minGap)); + testCase.verifyControlValues(newValue) + testCase.verifyButtonsEnabled(["off" "off" "off" "off"]) + + % Check decreasing the limits + newLimits = datetime("today") + days(60 + [0 20]); + testCase.verifySetProperty("Limits", newLimits); + testCase.verifyControlLimits(newLimits) + testCase.verifyEqual(testCase.Widget.MinGap, days(20)); + + % MinGap cannot exceed limits + testCase.verifyError(@() set(testCase.Widget, 'MinGap', 30), 'MATLAB:validators:mustBeGreaterThanOrEqual') + testCase.verifyError(@() set(testCase.Widget, 'MinGap', -1), 'MATLAB:validators:mustBeNonnegative') + testCase.verifyError(@() set(testCase.Widget, 'MinGap', hours(12)), 'MATLAB:validators:mustBeInteger') + testCase.verifyWarningFree(@() set(testCase.Widget, 'MinGap', days(10))) + testCase.verifyWarningFree(@() set(testCase.Widget, 'MinGap', 10)) + testCase.verifyButtonsEnabled(["on" "off" "off" "on"]) + + % No callbacks fired + testCase.verifyCallbackCount(0) + + end %function + + function testValueIndexMappingAndBounds(testCase) + newLimits = datetime("today") + [days(0) days(10)]; + testCase.verifySetProperty("Limits", newLimits); + testCase.verifyControlLimits(newLimits); + + % Set by indices (0-based offsets from lower limit) + testCase.verifySetProperty("ValueIndex", [1 11]); + testCase.verifyControlValues([newLimits(1) newLimits(2)]); + + % Mid-range indices + testCase.verifySetProperty("ValueIndex", [2 7]); + testCase.verifyControlValues([newLimits(1)+days(1) newLimits(1)+days(6)]); + + % Non-increasing should error + testCase.verifySetPropertyError("ValueIndex", [5 4], 'wt:DateRangeSlider:mustBeIncreasing'); + + % Out-of-bounds should error + testCase.verifySetPropertyError("ValueIndex", [-1 3], 'MATLAB:validators:mustBeInRange'); + testCase.verifySetPropertyError("ValueIndex", [0 11], 'MATLAB:validators:mustBeInRange'); + end %function + + function testOrientationVerticalLayout(testCase) + % Switch to vertical and verify positions/size wiring + testCase.verifySetProperty("Orientation", wt.enum.HorizontalVerticalState.vertical); + % Force a distinctive size to assert against + testCase.verifySetProperty("DatepickerSize", 180); + + % Let layout settle + drawnow; + + % Grid is 2 rows; first row equals DatepickerSize + testCase.verifyEqual(testCase.Widget.Grid.RowHeight{1}, 180); + + % Left side positions + testCase.verifyEqual(testCase.Widget.GridButtonLeft.Layout.Row, 1); + testCase.verifyEqual(testCase.Widget.GridButtonLeft.Layout.Column, 1); + testCase.verifyEqual(testCase.Widget.DatepickerLeft.Layout.Row, 1); + testCase.verifyEqual(testCase.Widget.DatepickerLeft.Layout.Column, 2); + + % Right side positions + testCase.verifyEqual(testCase.Widget.GridButtonRight.Layout.Row, 1); + testCase.verifyEqual(testCase.Widget.GridButtonRight.Layout.Column, 5); + testCase.verifyEqual(testCase.Widget.DatepickerRight.Layout.Row, 1); + testCase.verifyEqual(testCase.Widget.DatepickerRight.Layout.Column, 4); + + % Slider spans the second row + testCase.verifyEqual(testCase.Widget.Slider.Layout.Row, 2); + testCase.verifyEqual(testCase.Widget.Slider.Layout.Column, [1 5]); + end %function + + function testDisplayFormatPropagation(testCase) + fmt = "yyyy-MM-dd"; + testCase.verifySetProperty("DisplayFormat", fmt); + drawnow; + + % Datepickers should reflect the same format + testCase.verifyEquality(testCase.Widget.DatepickerLeft.DisplayFormat, fmt); + testCase.verifyEquality(testCase.Widget.DatepickerRight.DisplayFormat, fmt); + + % Tick labels non-empty (format checked indirectly) + labels = testCase.Widget.Slider.MajorTickLabels; + testCase.verifyTrue(~isempty(labels)); + end %function + + % function testValueChangingEventOnDrag(testCase) + % + % % Add this callback only for this test + % testCase.Widget.ValueChangingFcn = @(s,e)onCallbackTriggered(testCase,e); + % + % % NOTE: "drag" gesture does not support objects of class "matlab.ui.control.RangeSlider". + % end %function + + function testStepWithCalendarMonthsAndMinGap(testCase) + base = datetime(2020,1,1); + newLimits = base + [days(0) calmonths(3)]; % Jan 1 .. Apr 1 + testCase.verifySetProperty("Limits", newLimits); + + % Start mid-range + startVal = [base + calmonths(1), base + calmonths(2)]; % Feb 1 .. Mar 1 + testCase.verifySetProperty("Value", startVal); + + % Use month step + testCase.Widget.Step = calmonths(1); + testCase.verifyButtonsEnabled(["on" "on" "on" "on"]); + + % Increase MinGap; inward moves should clamp to the gap + testCase.verifySetProperty("MinGap", 20, days(20)); + + % Left "up" narrows the gap to MinGap + expValue = [base + caldays(40), base + calmonths(2)]; + testCase.verifyButtonPushAction("left", "up", expValue); + + % Right "down" also narrows the gap to MinGap + testCase.verifySetProperty("Value", startVal); + expValue = [base + calmonths(1), base + caldays(51)]; + testCase.verifyButtonPushAction("right", "down", expValue); + end %function + + function testLimitsNormalizationToStartOfDay(testCase) + d1 = dateshift(datetime("today"), 'start', 'day') + hours(9); + d2 = d1 + days(5) + hours(17); + + testCase.verifySetProperty("Limits", [d1 d2], [d1 d2] - timeofday([d1 d2])); + end + + end %methods (Test) + + %% Helper methods + methods (Access = private) + + function verifyControlValues(testCase, dateValue, absTol) + % Verifies the control fields have the specified value + + arguments + testCase + dateValue (1,2) datetime + absTol (1,1) double = 0 + end + + drawnow + + numValue = days(dateValue - testCase.Widget.Limits(1)) + 1; + testCase.verifyEqual(testCase.Widget.Slider.Value, numValue, 'AbsTol', absTol); + testCase.verifyEqual(testCase.Widget.DatepickerLeft.Value, dateValue(1), 'AbsTol', absTol); + testCase.verifyEqual(testCase.Widget.DatepickerRight.Value, dateValue(2), 'AbsTol', absTol); + + end %function + + function verifyControlLimits(testCase, dateLimits, absTol) + % Verifies the control fields have the specified limit + + arguments + testCase + dateLimits (1,2) datetime + absTol (1,1) double = 0 + end + + drawnow + + % What is the minimum gap? + minGap = testCase.Widget.MinGap; + + % Verify slider limits + numLimits = [0 days(dateLimits(2) - dateLimits(1))] + 1; + testCase.verifyEqual(testCase.Widget.Slider.Limits, numLimits, 'AbsTol', absTol); + + % Verify left datepicker + limitsLeft = [dateLimits(1) testCase.Widget.DatepickerRight.Value - minGap]; + testCase.verifyEqual(testCase.Widget.DatepickerLeft.Limits, limitsLeft, 'AbsTol', absTol); + + % Verify right datepicker + limitsRight = [testCase.Widget.DatepickerLeft.Value + minGap dateLimits(2)]; + testCase.verifyEqual(testCase.Widget.DatepickerRight.Limits, limitsRight, 'AbsTol', absTol); + + end %function + + function verifyButtonsEnabled(testCase, enabledStatus) + % Verifies the control buttons have the specified enable status + + arguments + testCase + enabledStatus (1,4) matlab.lang.OnOffSwitchState = [true true true true] + end + + testCase.verifyEqual([testCase.Widget.ButtonsLeft.Enable testCase.Widget.ButtonsRight.Enable], enabledStatus); + end %function + + function verifyButtonPushAction(testCase, side, direction, expValue) + + arguments + testCase + side (1,1) string {mustBeMember(side, ["right", "left"])} + direction (1,1) string {mustBeMember(direction, ["up", "down"])} + expValue (1,2) datetime + end + + if side == "right" + buttons = testCase.Widget.ButtonsRight; + else + buttons = testCase.Widget.ButtonsLeft; + end + + if direction == "up" + buttonIdx = 1; + else + buttonIdx = 2; + end + + % Type the new value into the control + testCase.press(buttons(buttonIdx)); + + % Verify new property value + testCase.verifyControlValues(expValue); + + end %function + + function verifySliderTicks(testCase, ticks) + % Verifies slider ticks are sorted, unique, and in range + + testCase.verifyEqual(ticks, unique(ticks)) + testCase.verifyTrue(all(ticks == round(ticks))) + testCase.verifyTrue(all(ticks >= testCase.Widget.Slider.Limits(1))) + testCase.verifyTrue(all(ticks <= testCase.Widget.Slider.Limits(2))) + end %function + + end %private methods + +end %classdef diff --git a/test/+wt/+test/DateSlider.m b/test/+wt/+test/DateSlider.m new file mode 100644 index 00000000..487ea8a9 --- /dev/null +++ b/test/+wt/+test/DateSlider.m @@ -0,0 +1,425 @@ +classdef DateSlider < wt.test.BaseWidgetTest + % Implements a unit test for a widget or component + +% Copyright 2026 The MathWorks, Inc. + + %% Test Method Setup + methods (TestMethodSetup) + + function setup(testCase) + + % Call superclass method + testCase.setup@wt.test.BaseWidgetTest(); + + % Setup widget + fcn = @()wt.DateSlider(testCase.Grid); + testCase.Widget = verifyWarningFree(testCase,fcn); + + % Set callback + testCase.Widget.ValueChangedFcn = @(s,e)onCallbackTriggered(testCase,e); + + % Ensure it renders + drawnow + + end %function + + end %methods + + %% Unit Tests + methods (Test) + + function testValuePropertyAndBoundaries(testCase) + + % Set the value + newValue = datetime("01-Jan-2020"); + testCase.verifySetProperty("Value", newValue); + testCase.verifyControlValue(newValue); + + % Set the value + newValue = datetime("03-Jan-2020"); + testCase.verifySetProperty("Value", newValue); + testCase.verifyControlValue(newValue); + + % Configure the control + newLimits = datetime("today") + [-days(20) days(15)]; + testCase.verifySetProperty("Limits", newLimits); + testCase.verifyControlLimits(newLimits); + testCase.verifyControlValue(newLimits(1)); + + % Set the value within limits + newValue = datetime("today") + days(15); + testCase.verifySetProperty("Value", newValue); + testCase.verifyControlValue(newValue); + + % Change the limits out of bounds + newLimits = datetime("today") + [days(20) days(30)]; + testCase.verifySetProperty("Limits", newLimits); + testCase.verifyControlLimits(newLimits); + testCase.verifyControlValue(newLimits(1)); + + % Set an invalid value + expValue = datetime("today") + days(20); + invldValue = datetime("today") + days(31); + testCase.verifySetPropertyError("Value", invldValue, 'MATLAB:ui:DatePicker:valueNotValid'); + testCase.verifyControlValue(expValue); + + % Set an invalid value + expValue = datetime("today") + days(20); + invldValue = datetime("today") + days(15); + testCase.verifySetPropertyError("Value", invldValue, 'MATLAB:ui:DatePicker:valueNotValid'); + testCase.verifyControlValue(expValue); + + % Configure a simple range + newLimits = datetime("today") + [days(0) days(10)]; + testCase.verifySetProperty("Limits", newLimits); + testCase.verifyControlLimits(newLimits); + + % Expect index 1 -> min date, index 11 -> max date + testCase.verifySetProperty("ValueIndex", 1); + testCase.verifyControlValue(newLimits(1)); + testCase.verifySetProperty("ValueIndex", 11); + testCase.verifyControlValue(newLimits(2)); + + % Out-of-bounds should error + testCase.verifySetPropertyError("ValueIndex", -1, 'MATLAB:validators:mustBeInRange'); + testCase.verifySetPropertyError("ValueIndex", 12, 'MATLAB:validators:mustBeInRange'); + + end %function + + function testDatepicker(testCase) + + % Configure the control + newLimits = datetime("today") + [-days(20) days(15)]; + testCase.verifySetProperty("Limits", newLimits); + testCase.verifyControlLimits(newLimits); + testCase.verifyControlValue(newLimits(1)); + + % Enable the datepicker to be able to type + testCase.Widget.Datepicker.Editable = true; + + % Pick a date using datepicker + newValue = datetime("today") + days(15); + testCase.verifyTypeAction(testCase.Widget.Datepicker, newValue, "Value") + testCase.verifyEqual(testCase.Widget.Value, newValue) + testCase.verifyCallbackCount(1); + + newValue = datetime("today") + days(10); + testCase.verifyTypeAction(testCase.Widget.Datepicker, newValue, "Value") + testCase.verifyEqual(testCase.Widget.Value, newValue) + testCase.verifyCallbackCount(2); + + % Pick an out-of-range date using datepicker + invldValue = datetime("today") + days(20); + testCase.verifyTypeAction(testCase.Widget.Datepicker, invldValue, "Value", newValue) + testCase.verifyEqual(testCase.Widget.Value, newValue) + testCase.verifyCallbackCount(2); + + % Change date using value property + newValue = datetime("today") - days(20); + testCase.verifySetProperty("Value", newValue) + testCase.verifyEqual(testCase.Widget.Datepicker.Value, newValue) + testCase.verifyCallbackCount(2); + + end + + function testDatepickerButtons(testCase) + + % Configure the control + newLimits = datetime("today") + [days(0) days(10)]; + testCase.verifySetProperty("Limits", newLimits); + testCase.verifyControlLimits(newLimits); + testCase.verifyControlValue(newLimits(1)); + + % Verify buttons enabled state + testCase.verifyButtonsEnabled([true false]) + + % Push down is disabled + testCase.verifyButtonPushAction("down", newLimits(1)) + testCase.verifyCallbackCount(0); + + % Push the button up once + testCase.verifyButtonPushAction("up", newLimits(1) + days(1)) + testCase.verifyButtonsEnabled([true true]) + testCase.verifyCallbackCount(1); + + % Change step size and push down + testCase.Widget.Step = caldays(4); + testCase.verifyButtonPushAction("down", newLimits(1)) + testCase.verifyButtonsEnabled([true false]) + testCase.verifyCallbackCount(2); + + % Push up + testCase.verifyButtonPushAction("up", newLimits(1) + days(4)) + testCase.verifyButtonPushAction("up", newLimits(1) + days(8)) + testCase.verifyButtonPushAction("up", newLimits(2)) + testCase.verifyCallbackCount(5); + + % Push up disabled at this point + testCase.verifyButtonsEnabled([false true]) + testCase.verifyButtonPushAction("up", newLimits(2)) + testCase.verifyCallbackCount(5); + + end + + function testValueChangingEventOnDrag(testCase) + % Attach ValueChanging callback to count intermediate events + + % Add ValueChanging callback function only for this test + testCase.Widget.ValueChangingFcn = @(s,e)onCallbackTriggered(testCase,e); + + % Configure range + newLimits = datetime("today") + [days(0) days(50)]; + testCase.verifySetProperty("Limits", newLimits); + + % Drag the slider; ValueChanging should fire at least once + sliderControl = testCase.Widget.Slider; + startValue = 5; newValue = 25; + testCase.drag(sliderControl, startValue, newValue); + + % We don't assert exact count (depends on platform), just > 1 + testCase.verifyTrue(testCase.CallbackCount > 1); + end %function + + function testStepWithCalendarMonths(testCase) + % Configure range across months + base = datetime(2020,1,1); + newLimits = base + [days(0) calmonths(2)]; % Jan 1 .. Mar 1 (approx) + testCase.verifySetProperty("Limits", newLimits); + testCase.verifyControlLimits(newLimits); + testCase.verifySetProperty("Value", newLimits(1)); + testCase.verifyControlValue(newLimits(1)); + + % Set step to one calendar month, start at min + testCase.verifySetProperty("Step", calmonths(1)); + + % Push 'up' twice: Jan->Feb->Mar (clamped at max) + testCase.verifyButtonPushAction("up", base + calmonths(1)); + testCase.verifyButtonPushAction("up", newLimits(2)); + + % Push 'down' twice: back to Feb->Jan + testCase.verifyButtonPushAction("down", base + calmonths(1)); + testCase.verifyButtonPushAction("down", newLimits(1)); + end %function + + function testButtonsEnabledMidRange(testCase) + % In mid-range both buttons must be enabled + newLimits = datetime("today") + [days(0) days(10)]; + testCase.verifySetProperty("Limits", newLimits); + mid = newLimits(1) + days(5); + testCase.verifySetProperty("Value", mid); + testCase.verifyButtonsEnabled([true true]); + end %function + + function testLimitsNormalizationToStartOfDay(testCase) + % Provide limits with non-midnight time components + d1 = dateshift(datetime("today"), 'start', 'day') + hours(10); + d2 = d1 + days(3) + hours(12); + testCase.verifySetProperty("Limits", [d1 d2], [d1 d2 ] - timeofday([d1 d2])); + end %function + + function testTickLabels(testCase) + + % Change the limits + expValue = datetime("today") + days(5); + newLimits = datetime("today") + [days(5) days(10)]; + testCase.verifySetProperty("Limits", newLimits); + testCase.verifyControlLimits(newLimits); + testCase.verifyControlValue(newLimits(1)); + + actualLabels = testCase.Widget.Slider.MajorTickLabels; + actualLabels = convertCharsToStrings(actualLabels); + actualTicks = testCase.Widget.Slider.MajorTicks; + expLabels = string(newLimits(1) + days(actualTicks - 1)); + testCase.verifySliderTicks(actualTicks) + testCase.verifyEqual(actualTicks(1), 1) + testCase.verifyEqual(actualTicks(end), 6) + testCase.verifyEqual(actualLabels, expLabels) + + % Change the limits + newLimits = datetime("today") + [days(0) days(10)]; + testCase.verifySetProperty("Limits", newLimits); + testCase.verifyControlLimits(newLimits); + testCase.verifyControlValue(expValue); + + % Change datepicker size so that slider barely fits + testCase.Widget.DatepickerSize = 240; + + % Check ticks remain valid when fewer labels fit + actualLabels = testCase.Widget.Slider.MajorTickLabels; + actualLabels = convertCharsToStrings(actualLabels); + actualTicks = testCase.Widget.Slider.MajorTicks; + expLabels = string(newLimits(1) + days(actualTicks - 1)); + + testCase.verifySliderTicks(actualTicks) + testCase.verifyEqual(actualTicks(1), 1) + testCase.verifyEqual(actualTicks(end), 11) + testCase.verifyEqual(numel(actualLabels), numel(actualTicks)) + testCase.verifyEqual(actualLabels, expLabels) + + end %function + + function testSlider(testCase) + + % Configure the control + newLimits = datetime("today") + [days(0) days(100)]; + testCase.verifySetProperty("Limits", newLimits); + testCase.verifyControlLimits(newLimits); + + % Get the control + sliderControl = testCase.Widget.Slider; + + % Click the slider + newValue = 75; + expValue = datetime("today") + days(newValue - 1); + testCase.choose(sliderControl,newValue); + testCase.verifyControlValue(expValue); + testCase.verifyEqual(testCase.Widget.Value, expValue); + + % Verify callback triggered + testCase.verifyEqual(testCase.CallbackCount, 1) + + % Drag the slider + startValue = 1; + newValue = 32; + expValue = datetime("today") + days(newValue - 1); + testCase.drag(sliderControl,startValue,newValue); + testCase.verifyControlValue(expValue); + testCase.verifyEqual(testCase.Widget.Value, expValue); + + % Verify callback triggered + testCase.verifyEqual(testCase.CallbackCount, 2) + + % Drag the slider to a fraction (with rounding turned on) + startValue = 32; + newValue = 13.8; + expValue = datetime("today") + days(round(newValue - 1)); + testCase.drag(sliderControl,startValue,newValue); + testCase.verifyControlValue(expValue); + testCase.verifyEqual(testCase.Widget.Value, expValue); + + % Verify callback triggered + testCase.verifyEqual(testCase.CallbackCount, 3) + + end %function + + function testOrientationVerticalLayout(testCase) + % Switch to vertical orientation + testCase.verifySetProperty("Orientation", wt.enum.HorizontalVerticalState.vertical); + + % Slider orientation must change + testCase.verifyEquality(testCase.Widget.Slider.Orientation, "vertical"); + + % Layout positions should match update() vertical branch + drawnow + testCase.verifyEqual(testCase.Widget.Datepicker.Layout.Row, 2); + testCase.verifyEqual(testCase.Widget.Datepicker.Layout.Column, 1); + testCase.verifyEqual(testCase.Widget.GridButtons.Layout.Row, 2); + testCase.verifyEqual(testCase.Widget.GridButtons.Layout.Column, 2); + + % DatepickerSize affects height in vertical orientation + testCase.verifySetProperty("DatepickerSize", 180); + drawnow + % Grid.RowHeight should include DatepickerSize as second row + testCase.verifyEqual(testCase.Widget.Grid.RowHeight{2}, 180); + end %function + + function testDisplayFormatPropagationAndTicks(testCase) + % Change the display format + fmt = "uuuu-MM-dd"; + testCase.Widget.DisplayFormat = fmt; + drawnow + + % Verify propagation to datepicker and limits + testCase.verifyEquality(testCase.Widget.Datepicker.DisplayFormat, fmt); + testCase.verifyEquality(testCase.Widget.Limits.Format, fmt); + + % Ticks/labels should be updated (non-empty categorical labels) + drawnow + labels = testCase.Widget.Slider.MajorTickLabels; + testCase.verifyTrue(~isempty(labels)); + end %function + + end %methods (Test) + + %% Helper methods + methods (Access = private) + + function verifyControlValue(testCase, dateValue, absTol) + % Verifies the control fields have the specified value + + arguments + testCase + dateValue (1,1) datetime + absTol (1,1) double = 0 + end + + drawnow + + numValue = days(dateValue - testCase.Widget.Limits(1)) + 1; + testCase.verifyEqual(testCase.Widget.Slider.Value, numValue, 'AbsTol', absTol); + testCase.verifyEqual(testCase.Widget.Datepicker.Value, dateValue, 'AbsTol', absTol); + + end %function + + function verifyControlLimits(testCase, dateLimits, absTol) + % Verifies the control fields have the specified limit + + arguments + testCase + dateLimits (1,2) datetime + absTol (1,1) double = 0 + end + + drawnow + + numLimits = [0 days(dateLimits(2) - dateLimits(1))] + 1; + testCase.verifyEqual(testCase.Widget.Slider.Limits, numLimits, 'AbsTol', absTol); + testCase.verifyEqual(testCase.Widget.Datepicker.Limits, dateLimits, 'AbsTol', absTol); + + end %function + + function verifyButtonsEnabled(testCase, enabledStatus) + % Verifies the control buttons have the specified enable status + + arguments + testCase + enabledStatus (1,2) matlab.lang.OnOffSwitchState = [true true] + end + + testCase.verifyEqual([testCase.Widget.Buttons.Enable], enabledStatus); + end %function + + function verifyButtonPushAction(testCase, direction, expValue) + + arguments + testCase + direction (1,1) string {mustBeMember(direction, ["up", "down"])} + expValue (1,1) datetime + end + + if direction == "up" + buttonIdx = 1; + else + buttonIdx = 2; + end + + % Type the new value into the control + testCase.press(testCase.Widget.Buttons(buttonIdx)); + + % Verify new property value + testCase.verifyControlValue(expValue); + + end %function + + function verifySliderTicks(testCase, ticks) + % Verifies slider ticks are sorted, unique, and in range + + testCase.verifyEqual(ticks, unique(ticks)) + testCase.verifyTrue(all(ticks == round(ticks))) + testCase.verifyTrue(all(ticks >= testCase.Widget.Slider.Limits(1))) + testCase.verifyTrue(all(ticks <= testCase.Widget.Slider.Limits(2))) + end %function + + end %methods + +end %classdef diff --git a/test/+wt/+test/DateSliderDefaults.m b/test/+wt/+test/DateSliderDefaults.m new file mode 100644 index 00000000..72c918a6 --- /dev/null +++ b/test/+wt/+test/DateSliderDefaults.m @@ -0,0 +1,65 @@ +classdef DateSliderDefaults < matlab.unittest.TestCase + % Verify default date slider configuration. + +% Copyright 2026 The MathWorks, Inc. + + methods (Test) + + function testDateSliderDefaults(testCase) + + widget = testCase.createWidget(@wt.DateSlider); + expLimits = testCase.getExpectedLimits(widget); + expSliderLimits = [0 days(diff(expLimits))] + 1; + + testCase.verifyEqual(widget.Position(3:4), [600 40]) + testCase.verifyEqual(widget.Limits, expLimits) + testCase.verifyEqual(widget.Slider.Limits, expSliderLimits) + testCase.verifyEqual(widget.Datepicker.Limits, expLimits) + + end %function + + function testDateRangeSliderDefaults(testCase) + + testCase.assumeFalse(isMATLABReleaseOlderThan("R2024b"), ... + "Release not supported.") + + widget = testCase.createWidget(@wt.DateRangeSlider); + expLimits = testCase.getExpectedLimits(widget); + expSliderLimits = days([0 diff(expLimits)]) + 1; + + testCase.verifyEqual(widget.Position(3:4), [600 40]) + testCase.verifyEqual(widget.Limits, expLimits) + testCase.verifyEqual(widget.Slider.Limits, expSliderLimits) + testCase.verifyEqual(widget.DatepickerLeft.Limits(1), expLimits(1)) + testCase.verifyEqual(widget.DatepickerRight.Limits(2), expLimits(2)) + + end %function + + end %methods + + methods (Access = private) + + function widget = createWidget(testCase, constructorFcn) + + fig = uifigure("Visible", "off"); + testCase.addTeardown(@()delete(fig)); + + widget = verifyWarningFree(testCase, @()constructorFcn(fig)); + drawnow + + end %function + + end %methods + + methods (Static, Access = private) + + function expLimits = getExpectedLimits(widget) + + expLimits = datetime("01-Jan-2020") + calyears([0 1]); + expLimits.Format = widget.Limits.Format; + + end %function + + end %methods + +end %classdef diff --git a/widgets/+wt/+mixin/TickableDateSlider.m b/widgets/+wt/+mixin/TickableDateSlider.m new file mode 100644 index 00000000..9ffa551d --- /dev/null +++ b/widgets/+wt/+mixin/TickableDateSlider.m @@ -0,0 +1,104 @@ +classdef TickableDateSlider < handle + %TickableDateSlider - Add date ticks to a slider component + % OBJ = TickableDateSlider(PARENT) adds shared tick calculation for + % date-based slider widgets. + % + % See also wt.DateSlider, wt.DateRangeSlider + + % Copyright 2026 The MathWorks, Inc. + + %% Abstract properties + properties (Abstract, Transient, NonCopyable, Hidden, SetAccess = protected) + + % Slider + Slider + + end + + %% Protected methods + methods (Access = protected) + + function [majorTicks, minorTicks] = getSliderTicks(obj, orientation, options) + %getSliderTicks - Calculate major and minor date slider ticks + + arguments + obj (1,1) + orientation (1,1) string {mustBeMember(orientation, ["horizontal" "vertical"])} = "horizontal"; + options.TickLength (1,1) double {mustBeInteger, mustBePositive} = 11 + end + + sliderLimits = obj.Slider.Limits; + lowerTick = ceil(sliderLimits(1)); + upperTick = floor(sliderLimits(2)); + + if lowerTick > upperTick + lowerTick = round(sliderLimits(1)); + upperTick = lowerTick; + end + + % What space is needed for the labels? + % Depends on font size and font weight. + if string(obj.Slider.FontWeight) == "bold" + elSize = 0.6; + else + elSize = 0.5; + end + fontSize = obj.Slider.FontSize; + + % What is the slider orientation? + if orientation == "horizontal" + + % Size of slider. Always in pixel units (cannot be changed) + sliderSpace = obj.Slider.Position(3); + + % Minimum space required for label + minSpaceForTickLabel = ... + elSize * fontSize * options.TickLength; + else + % Size of slider. Always in pixel units (cannot be changed) + sliderSpace = obj.Slider.Position(4); + + % Minimum space required for label + minSpaceForTickLabel = elSize * fontSize * 2; + end + + minSpaceForTickLabel = max(1, minSpaceForTickLabel); + maxMajorTicks = max(2, floor(sliderSpace / minSpaceForTickLabel) + 1); + majorTicks = selectSliderTicks(lowerTick, upperTick, maxMajorTicks); + + % How much space is left for the minor ticks? + minSpaceForMinorTick = 10; + maxMinorTicks = max(2, floor(sliderSpace / minSpaceForMinorTick) + 1); + minorTicks = selectSliderTicks(lowerTick, upperTick, maxMinorTicks); + end + + end + +end + +function ticks = selectSliderTicks(lowerTick, upperTick, maxTickCount) +%selectSliderTicks - Select integer ticks between two slider limits + +span = upperTick - lowerTick; +if span == 0 + ticks = lowerTick; + return +end + +allTicks = lowerTick:upperTick; +if numel(allTicks) <= maxTickCount + ticks = allTicks; + return +end + +maxTickCount = max(2, floor(maxTickCount)); +stepSize = max(1, ceil(span / (maxTickCount - 1))); +ticks = lowerTick:stepSize:upperTick; + +if ticks(end) ~= upperTick + ticks(end + 1) = upperTick; +end + +ticks = unique(ticks); + +end diff --git a/widgets/+wt/DateRangeSlider.m b/widgets/+wt/DateRangeSlider.m new file mode 100644 index 00000000..0c1a70a2 --- /dev/null +++ b/widgets/+wt/DateRangeSlider.m @@ -0,0 +1,662 @@ +classdef DateRangeSlider < wt.abstract.BaseWidget & ... + wt.mixin.TickableDateSlider & ... + wt.mixin.Enableable & ... + wt.mixin.FieldColorable & ... + wt.mixin.FontStyled & ... + wt.mixin.ButtonColorable + %DateRangeSlider - Select a date interval + % DateRangeSlider requires MATLAB R2024b or later. + % + % OBJ = DateRangeSlider(PARENT) creates a date-range slider in the + % specified parent container. The Value property is a two-element + % datetime row vector, and slider values map to whole-day offsets + % from Limits(1). + % + % DateRangeSlider properties: + % Value - Selected lower and upper dates + % ValueIndex - One-based selected day indices + % Limits - Lower and upper selectable dates + % MinGap - Minimum gap as a duration + % DisplayFormat - Date display format + % Step - Button step size as a calendar duration + % Orientation - Slider orientation + % DatepickerSize - Date picker width or height + % + % See also uidatepicker, uislider, DateSlider + + % Copyright 2026 The MathWorks, Inc. + + %% Events + events (HasCallbackProperty, NotifyAccess = protected) + + % Triggered on value changed, has companion callback + ValueChanged + + % Triggered on value changing during slider motion, has companion callback + ValueChanging + + end %events + + %% Public properties + properties (AbortSet, Dependent, UsedInUpdate = false) + % These properties do not trigger the update method + + % Value of the slider and date-time selector + Value (1,2) datetime + + end + + properties (AbortSet) + + % Limits of the slider and date pickers + Limits (1,2) datetime = datetime("01-Jan-2020") + calyears([0 1]); + + % Date format + DisplayFormat (1,1) string = "dd-MMM-uuuu" + + % Orientation of the date pickers and slider + Orientation (1,1) wt.enum.HorizontalVerticalState = wt.enum.HorizontalVerticalState.horizontal + + % Size of date-picker (width for horizontal, height for vertical) + DatepickerSize = 120 + + end %properties + + properties (AbortSet, Dependent, UsedInUpdate = false) + + % Minimum gap in the selected date range + MinGap + + end + + properties (AbortSet, Dependent) + + % Index of selected item + ValueIndex (1,2) double {mustBeInteger} + + end + + properties (AbortSet, UsedInUpdate = false) + % These properties do not trigger the update method + + % Define step size for buttons + Step (1,1) calendarDuration = calendarDuration(0,0,1) + + end + + %% Internal Properties + properties (Access = private) + + % Minimum gap + MinGap_I = 1; + + end + + properties (Transient, NonCopyable, Hidden, SetAccess = protected) + + % Slider + Slider + + % Date picker on the left + DatepickerLeft matlab.ui.control.DatePicker + + % Date picker on the right + DatepickerRight matlab.ui.control.DatePicker + + % Button grid on the left + GridButtonLeft matlab.ui.container.GridLayout + ButtonsLeft (1,:) matlab.ui.control.Button + + % Button grid on the right + GridButtonRight matlab.ui.container.GridLayout + ButtonsRight (1,:) matlab.ui.control.Button + + end + + %% Accessors + methods + + % ValueIndex + function set.ValueIndex(obj, val) + + % Validate input + maxRange = days(obj.Limits(2) - obj.Limits(1)) + 1; + try + mustBeInRange(val, 1, maxRange) + mustBeIncreasing(val) + catch ME + throwAsCaller(ME) + end + + % Set value + obj.Value = obj.Limits(1) + days(val - 1); + + end %function + function val = get.ValueIndex(obj) + val = days([obj.DatepickerLeft.Value obj.DatepickerRight.Value] - obj.Limits(1)) + 1; + end %function + + % Value + function set.Value(obj, val) + + % Validate input + valueIndex = days(val - obj.Limits(1)) + 1; + valueGap = days(diff(val)); + maxRange = days(obj.Limits(2) - obj.Limits(1)) + 1; + try + mustBeInRange(valueIndex, 1, maxRange) + mustBeGreaterThanOrEqual(valueGap, days(obj.MinGap)) + mustBeIncreasing(valueIndex) + catch ME + if strcmp(ME.identifier, 'MATLAB:validators:mustBeInRange') + msg = 'Value must be greater than or equal to %s, and less than or equal to %s.'; + Merr = MException(ME.identifier, ... + msg, ... + char(obj.Limits(1)), ... + char(obj.Limits(2))); + throwAsCaller(Merr); + elseif strcmp(ME.identifier, 'MATLAB:validators:mustBeGreaterThanOrEqual') + msg = 'Value gap must be greater than or equal to %d days.'; + Merr = MException(ME.identifier, ... + msg, ... + days(obj.MinGap)); + throwAsCaller(Merr); + else + throwAsCaller(ME) + end + end + + % Update datepicker components + updateControlLimitsAndValue(obj, val); + + % Update buttons + updateButtonEnable(obj); + + end %function + function val = get.Value(obj) + val = [obj.DatepickerLeft.Value obj.DatepickerRight.Value]; + end %function + + % Display format + function set.DisplayFormat(obj, val) + try + d = uidatepicker('Parent', []); + d.DisplayFormat = val; + obj.DisplayFormat = d.DisplayFormat; + catch ME + throwAsCaller(ME) + end + end %function + + % Limits + function set.Limits(obj, val) + startOfDay = dateshift(val, 'start', 'days'); + limitIndex = days(val - val(1)); + try + mustBeIncreasing(limitIndex) + catch ME + throwAsCaller(ME) + end + obj.Limits = startOfDay; + end %function + + % Gap + function set.MinGap(obj, val) + + % Parse input + if isduration(val) + val = days(val); + else + mustBeNumeric(val) + end + + mustBeReal(val) + mustBeScalarOrEmpty(val) + mustBeNonempty(val) + mustBeFinite(val) + mustBeNonnegative(val) + mustBeInteger(val) + + % Validate input + maxGap = days(diff(obj.Limits)); + try + mustBeGreaterThanOrEqual(maxGap, val) + catch ME + if strcmp(ME.identifier, 'MATLAB:validators:mustBeGreaterThanOrEqual') + msg = 'Value must be smaller than or equal to %d days.'; + Merr = MException(ME.identifier, ... + msg, ... + maxGap); + throwAsCaller(Merr); + else + throwAsCaller(ME) + end + end + + % Set value + obj.MinGap_I = val; + + if ~isempty(obj.Slider) && ~isempty(obj.DatepickerLeft) ... + && ~isempty(obj.DatepickerRight) + updateControlLimitsAndValue(obj); + updateButtonEnable(obj); + end + end + + function val = get.MinGap(obj) + val = days(obj.MinGap_I); + end + + % Datepicker size + function set.DatepickerSize(obj, val) + try + g = uigridlayout("Parent", []); + cleanup = onCleanup(@() delete(g)); + g.ColumnWidth{1} = val; + catch ME + throwAsCaller(ME) + end + obj.DatepickerSize = val; + end %function + + end + + %% Protected methods + methods (Access = protected) + + function setup(obj) + + if isMATLABReleaseOlderThan("R2024b") + error("wt:DateRangeSlider:UnsupportedRelease", ... + "DateRangeSlider requires MATLAB R2024b or later.") + end + + % Call superclass method + obj.setup@wt.abstract.BaseWidget() + + % Set default size + obj.Position = [20 100 600 40]; + + defaultValue = obj.Limits(1) + days([1 2]); + + % Configure grid + obj.Grid.ColumnWidth = {'fit', obj.DatepickerSize, '1x', obj.DatepickerSize, 'fit'}; + obj.Grid.RowHeight = {'1x'}; + obj.Grid.ColumnSpacing = 5; + + % Buttons left + obj.GridButtonLeft = uigridlayout(obj.Grid); + obj.GridButtonLeft.RowHeight = {"1x", "1x"}; + obj.GridButtonLeft.ColumnWidth = "fit"; + obj.GridButtonLeft.ColumnSpacing = 0; + obj.GridButtonLeft.RowSpacing = 2; + obj.GridButtonLeft.Padding(:) = 0; + + obj.ButtonsLeft = [uibutton(obj.GridButtonLeft), uibutton(obj.GridButtonLeft)]; + obj.ButtonsLeft(1).ButtonPushedFcn = @(h,e)obj.onButtonPushed(e); + obj.ButtonsLeft(1).Tag = "leftUp"; + obj.ButtonsLeft(1).Icon = "up_grey_12.png"; + obj.ButtonsLeft(1).Text = ""; + obj.ButtonsLeft(2).ButtonPushedFcn = @(h,e)obj.onButtonPushed(e); + obj.ButtonsLeft(2).Tag = "leftDown"; + obj.ButtonsLeft(2).Icon = "down_grey_12.png"; + obj.ButtonsLeft(2).Text = ""; + + % Date picker left + obj.DatepickerLeft = uidatepicker(obj.Grid); + obj.DatepickerLeft.ValueChangedFcn = @(h,e)obj.onDatepickerChanged(e); + obj.DatepickerLeft.Value = defaultValue(1); + obj.DatepickerLeft.Limits = [obj.Limits(1) defaultValue(1)]; + obj.DatepickerLeft.Editable = false; + obj.DatepickerLeft.Tag = "left"; + + % Slider + obj.Slider = uislider(obj.Grid, 'range'); + obj.Slider.ValueChangedFcn = @(h,e)obj.onSliderChanged(e); + obj.Slider.ValueChangingFcn = @(h,e)obj.onSliderChanging(e); + obj.Slider.Limits = days([0 diff(obj.Limits)]) + 1; + obj.Slider.Value = days(defaultValue - obj.Limits(1)) + 1; + + % Date picker right + obj.DatepickerRight = uidatepicker(obj.Grid); + obj.DatepickerRight.ValueChangedFcn = @(h,e)obj.onDatepickerChanged(e); + obj.DatepickerRight.Value = defaultValue(2); + obj.DatepickerRight.Limits = [defaultValue(2) obj.Limits(2)]; + obj.DatepickerRight.Editable = false; + obj.DatepickerRight.Tag = "right"; + + % Buttons right + obj.GridButtonRight = uigridlayout(obj.Grid); + obj.GridButtonRight.RowHeight = {"1x", "1x"}; + obj.GridButtonRight.ColumnWidth = "fit"; + obj.GridButtonRight.ColumnSpacing = 0; + obj.GridButtonRight.RowSpacing = 2; + obj.GridButtonRight.Padding(:) = 0; + + obj.ButtonsRight = [uibutton(obj.GridButtonRight), uibutton(obj.GridButtonRight)]; + obj.ButtonsRight(1).ButtonPushedFcn = @(h,e)obj.onButtonPushed(e); + obj.ButtonsRight(1).Tag = "rightUp"; + obj.ButtonsRight(1).Icon = "up_grey_12.png"; + obj.ButtonsRight(1).Text = ""; + obj.ButtonsRight(2).ButtonPushedFcn = @(h,e)obj.onButtonPushed(e); + obj.ButtonsRight(2).Tag = "rightDown"; + obj.ButtonsRight(2).Icon = "down_grey_12.png"; + obj.ButtonsRight(2).Text = ""; + + % Update the internal component lists + obj.BackgroundColorableComponents = [obj.Grid, obj.GridButtonLeft, obj.GridButtonRight]; + obj.FontStyledComponents = [obj.DatepickerLeft, obj.Slider, obj.DatepickerRight]; + obj.EnableableComponents = [obj.DatepickerLeft, obj.Slider, obj.DatepickerRight, obj.ButtonsLeft, obj.ButtonsRight]; + obj.FieldColorableComponents = [obj.DatepickerLeft, obj.DatepickerRight]; + obj.ButtonColorableComponents = [obj.ButtonsLeft, obj.ButtonsRight]; + + % Update the slider ticks + updateSliderTicks(obj); + + end %function + + + function update(obj) + % Set the value ranges + + % Set orientation in grid + if obj.Orientation == "vertical" + + % Grid + obj.Grid.ColumnWidth = {'fit', 'fit', '1x', 'fit', 'fit'}; + obj.Grid.RowHeight = {obj.DatepickerSize, '1x'}; + + % Left + obj.GridButtonLeft.Layout.Row = 1; + obj.GridButtonLeft.Layout.Column = 1; + obj.DatepickerLeft.Layout.Row = 1; + obj.DatepickerLeft.Layout.Column = 2; + + % Right + obj.GridButtonRight.Layout.Row = 1; + obj.GridButtonRight.Layout.Column = 5; + obj.DatepickerRight.Layout.Row = 1; + obj.DatepickerRight.Layout.Column = 4; + + % Slider + obj.Slider.Layout.Row = 2; + obj.Slider.Layout.Column = [1 5]; + + else + + % Grid + obj.Grid.ColumnWidth = {'fit', obj.DatepickerSize, '1x', obj.DatepickerSize, 'fit'}; + obj.Grid.RowHeight = {'1x'}; + + % Left + obj.GridButtonLeft.Layout.Row = 1; + obj.GridButtonLeft.Layout.Column = 1; + obj.DatepickerLeft.Layout.Row = 1; + obj.DatepickerLeft.Layout.Column = 2; + + % Right + obj.GridButtonRight.Layout.Row = 1; + obj.GridButtonRight.Layout.Column = 5; + obj.DatepickerRight.Layout.Row = 1; + obj.DatepickerRight.Layout.Column = 4; + + % Slider + obj.Slider.Layout.Row = 1; + obj.Slider.Layout.Column = 3; + end + + % Make sure values are a datetime number + if isnat(obj.DatepickerLeft.Value) + obj.DatepickerLeft.Value = obj.DatepickerLeft.Limits(1); + end + if isnat(obj.DatepickerRight.Value) + obj.DatepickerRight.Value = obj.DatepickerRight.Limits(2); + end + + % Gap is bounded by limits + obj.MinGap = boundValue(obj, days(obj.MinGap), 0, days(diff(obj.Limits))); + + % Update datepicker limits + updateControlLimitsAndValue(obj); + + % Update datepicker display formats + obj.DatepickerLeft.DisplayFormat = obj.DisplayFormat; + obj.DatepickerRight.DisplayFormat = obj.DisplayFormat; + + % Update ticks + updateSliderTicks(obj); + + % Update buttons + updateButtonEnable(obj); + + end %function + + + function onButtonPushed(obj, evt) + % Triggered on button pushed + + % What button was pushed? + oldDate = obj.Value; + newDate = obj.Value; + + switch evt.Source.Tag + case "leftUp" + newDate(1) = min(newDate(1) + obj.Step, newDate(2) - obj.MinGap); + case "leftDown" + newDate(1) = max(newDate(1) - obj.Step, obj.Limits(1)); + case "rightUp" + newDate(2) = min(newDate(2) + obj.Step, obj.Limits(2)); + case "rightDown" + newDate(2) = max(newDate(2) - obj.Step, newDate(1) + obj.MinGap); + end + + % Update the value + obj.Value = newDate; + + % Trigger event + evtOut = wt.eventdata.PropertyChangedData('Value', newDate, oldDate); + notify(obj, "ValueChanged", evtOut); + + end %function + + function onSliderChanged(obj,evt) + % Triggered on slider moved + + % What changed the date? + prevDate = obj.Limits(1) + days(round(evt.PreviousValue) - 1); + newDate = obj.Limits(1) + days(round(evt.Value) - 1); + + % Update datepicker limits + updateControlLimitsAndValue(obj, newDate); + + % Update button enable status + updateButtonEnable(obj) + + % Event data + evtOut = wt.eventdata.PropertyChangedData('Value', newDate, prevDate); + + % Trigger event "ValueChanged" + notify(obj, "ValueChanged", evtOut); + + end %function + + function onSliderChanging(obj,evt) + % Triggered on slider moving + + % What changed the date? + changedDate = obj.Limits(1) + days(round(evt.Value) - 1); + + % Update datepicker values + obj.DatepickerLeft.Value = changedDate(1); + obj.DatepickerRight.Value = changedDate(2); + + % Event data + evtOut = wt.eventdata.PropertyChangedData('Value', changedDate); + + % Trigger event "ValueChanging" + notify(obj, "ValueChanging", evtOut); + + end %function + + + function onDatepickerChanged(obj,evt) + % Triggered on button pushed + + % What changed? + prevValue = obj.Value; + newValue = obj.Value; + + switch evt.Source.Tag + case "left" + prevValue(1) = evt.PreviousValue; + case "right" + prevValue(2) = evt.PreviousValue; + end + + % Prepare event data + evtOut = wt.eventdata.PropertyChangedData('Value', newValue, prevValue); + + % Update the components with the new value + updateControlLimitsAndValue(obj, newValue) + + % Update the buttons + updateButtonEnable(obj) + + % Trigger event + notify(obj, "ValueChanged", evtOut); + + end %function + + + function updateButtonEnable(obj) + + % What are the buttons? + buttonsLeft = obj.ButtonsLeft; + buttonsRight = obj.ButtonsRight; + + if ~(numel(buttonsLeft) == 2 && numel(buttonsRight) == 2) + return + end + + % Can interval increase to lower bound? + if obj.Value(1) > obj.Limits(1) + buttonsLeft(2).Enable = true; + else + buttonsLeft(2).Enable = false; + end + + % Can interval further increase to upper bound? + if obj.Value(2) < obj.Limits(2) + buttonsRight(1).Enable = true; + else + buttonsRight(1).Enable = false; + end + + % Can interval further decrease to center? + if obj.Value(1) == obj.Value(2) - obj.MinGap + buttonsLeft(1).Enable = false; + buttonsRight(2).Enable = false; + else + buttonsLeft(1).Enable = true; + buttonsRight(2).Enable = true; + end + + end %function + + + function updateSliderTicks(obj) + % Update the limits in date-picker and slider + + % Get ticks + [majorTicks, minorTicks] = getSliderTicks( ... + obj, ... + obj.Orientation, ... + "TickLength", strlength(obj.DisplayFormat)); + + % Set slider limits and ticks + obj.Slider.MajorTicks = majorTicks; + obj.Slider.MinorTicks = minorTicks; + + minLimit = obj.Limits(1); + minLimit.Format = obj.DisplayFormat; + obj.Slider.MajorTickLabels = categorical(minLimit + days(majorTicks - 1)); + end %function + + + function updateControlLimitsAndValue(obj, val) + % Update values in datepicker components + + % Current control value + currentVal = [obj.DatepickerLeft.Value obj.DatepickerRight.Value]; + + % Value provided? + if nargin < 2 + val = currentVal; + end + + % Bound current value by lower limit and upper limit. + if eq(currentVal(1), val(1)) + val(1) = boundValue(obj, val(1), obj.Limits(1), obj.Limits(2) - obj.MinGap); + val(2) = boundValue(obj, val(2), val(1) + obj.MinGap, obj.Limits(2)); + else + val(2) = boundValue(obj, val(2), obj.Limits(1) + obj.MinGap, obj.Limits(2)); + val(1) = boundValue(obj, val(1), obj.Limits(1), val(2) - obj.MinGap); + end + + % Update the limits and value + obj.DatepickerLeft.Limits = [obj.Limits(1) val(2) - obj.MinGap]; + obj.DatepickerLeft.Value = val(1); + + obj.DatepickerRight.Limits = [val(1) + obj.MinGap obj.Limits(2)]; + obj.DatepickerRight.Value = val(2); + + % Update the limits and value in the slider + obj.Slider.Limits = days([0, diff(obj.Limits)]) + 1; + obj.Slider.Value = days(val - obj.Limits(1)) + 1; + + end %function + + end %methods + + %% Private helper functions + methods (Access = private) + + function boundedVal = boundValue(~, val, lowerBound, upperBound) + boundedVal = min( ... + max(val, lowerBound), ... + upperBound); + end + + end + +end % classdef + +function mustBeIncreasing(A) +%MUSTBEINCREASING Validate that value array is increasing +% MUSTBEINCREASING(A) throws an error if array A is not increating. +% MATLAB calls sort to determine if A is increasing. +% +% Class support: +% All numeric classes +% MATLAB classes that define these methods: +% sort, isnumeric +% +% See also: MUSTBENUMERIC, MUSTBEREAL, MUSTBEVECTOR. + +try + mustBeNumeric(A) + mustBeReal(A) + mustBeVector(A) +catch ME + throwAsCaller(ME) +end + +[~, idx] = sort(A, 'ascend'); +if ~all(eq(idx, 1:numel(idx))) + ME = MException( ... + 'wt:DateRangeSlider:mustBeIncreasing', ... + 'Value must be increasing.'); + throwAsCaller(ME); +end + +end diff --git a/widgets/+wt/DateSlider.m b/widgets/+wt/DateSlider.m new file mode 100644 index 00000000..b59081a4 --- /dev/null +++ b/widgets/+wt/DateSlider.m @@ -0,0 +1,396 @@ +classdef DateSlider < wt.abstract.BaseWidget & ... + wt.mixin.TickableDateSlider & ... + wt.mixin.Enableable & ... + wt.mixin.FieldColorable & ... + wt.mixin.FontStyled & ... + wt.mixin.ButtonColorable + %DateSlider - Select a single date with a date picker and slider + % DateSlider requires MATLAB R2023b or later. + % + % OBJ = DateSlider(PARENT) creates a date slider in the specified + % parent container. The Value property is a datetime value, and the + % slider position maps to whole-day offsets from Limits(1). + % + % DateSlider properties: + % Value - Selected date + % ValueIndex - One-based selected day index + % Limits - Lower and upper selectable dates + % DisplayFormat - Date display format + % Step - Button step size as a calendar duration + % Orientation - Slider orientation + % DatepickerSize - Date picker width or height + % + % See also uidatepicker, uislider, DateRangeSlider + + % Copyright 2026 The MathWorks, Inc. + + %% Events + events (HasCallbackProperty, NotifyAccess = protected) + + % Triggered on value changed, has companion callback + ValueChanged + + % Triggered on value changing during slider motion, has companion callback + ValueChanging + + end %events + + %% Public dependent properties + properties (AbortSet, Dependent) + % These properties do not trigger the update method + + % Value of the slider and date-time selector + Value (1,1) datetime + + % Index of selected item + ValueIndex (1,1) double {mustBeInteger} + + % Date format + DisplayFormat (1,1) string + + end %properties + + properties (AbortSet, UsedInUpdate = false) + % These properties do not trigger the update method + + % Define step size for buttons + Step (1,1) calendarDuration = calendarDuration(0,0,1) + + end + + %% Public properties + properties (AbortSet) + % These properties trigger the update method + + % Limits of the slider and date picker + Limits (1,2) datetime = datetime("01-Jan-2020") + calyears([0 1]); + + % Orientation of the date picker and slider + Orientation (1,1) wt.enum.HorizontalVerticalState = wt.enum.HorizontalVerticalState.horizontal + + % Size of date-picker (width for horizontal, height for vertical) + DatepickerSize = 120 + + end %properties + + + + %% Internal Properties + properties (Transient, NonCopyable, Hidden, SetAccess = protected) + + % Slider + Slider = matlab.ui.control.Slider.empty + + % Date picker + Datepicker matlab.ui.control.DatePicker + + % Selection buttons + GridButtons matlab.ui.container.GridLayout + Buttons (1,:) matlab.ui.control.Button + + end + + %% Accessors + methods + + % ValueIndex + function set.ValueIndex(obj, val) + maxRange = days(obj.Limits(2) - obj.Limits(1)) + 1; + try + mustBeInRange(val, 1, maxRange) + catch ME + throwAsCaller(ME) + end + obj.Value = obj.Limits(1) + days(val - 1); + end %function + function val = get.ValueIndex(obj) + val = obj.Slider.Value; + end %function + + % Value + function set.Value(obj, val) + try + obj.Datepicker.Value = val; + catch ME + throwAsCaller(ME); + end + obj.Slider.Value = days(val - obj.Limits(1)) + 1; + obj.Datepicker.Value = val; + end %function + function val = get.Value(obj) + val = obj.Datepicker.Value; + end %function + + % Display format + function set.DisplayFormat(obj, val) + try + obj.Datepicker.DisplayFormat = val; + catch ME + throwAsCaller(ME) + end + end %function + function val = get.DisplayFormat(obj) + val = obj.Datepicker.DisplayFormat; + end %function + + % Limits + function set.Limits(obj, val) + startOfDay = dateshift(val, 'start', 'days'); + try + numVal = days(startOfDay - startOfDay(1)); + mustBeGreaterThan(numVal(2), numVal(1)) + catch ME + if strcmp(ME.identifier, 'MATLAB:validators:mustBeGreaterThanOrEqual') + msg = 'Error setting property ''Limits'' of class ''%s''. Value must be increasing.'; + Merr = MException(ME.identifier, msg, classname2link(class(obj))); + throwAsCaller(Merr); + else + rethrow(ME) + end + end + obj.Limits = startOfDay; + end %function + function val = get.Limits(obj) + val = obj.Limits; + if ~isempty(obj.Datepicker) + val.Format = obj.Datepicker.DisplayFormat; + end + end %function + + % Datepicker size + function set.DatepickerSize(obj, val) + try + g = uigridlayout("Parent", []); + cleanup = onCleanup(@() delete(g)); + g.ColumnWidth{1} = val; + catch ME + throwAsCaller(ME) + end + obj.DatepickerSize = val; + end %function + + end + + %% Protected methods + methods (Access = protected) + + function setup(obj) + + if isMATLABReleaseOlderThan("R2023b") + error("wt:DateSlider:UnsupportedRelease", ... + "DateSlider requires MATLAB R2023b or later.") + end + + % Call superclass method + obj.setup@wt.abstract.BaseWidget() + + % Set default size + obj.Position(3:4) = [600 40]; + + defaultValue = obj.Limits(1) + days(1); + + % Configure grid + obj.Grid.ColumnWidth = {'1x', obj.DatepickerSize, 'fit'}; + obj.Grid.ColumnSpacing = 5; + + % Slider + obj.Slider = uislider(obj.Grid); + obj.Slider.ValueChangedFcn = @(h,e)obj.onSliderChanged(e); + obj.Slider.ValueChangingFcn = @(h,e)obj.onSliderChanged(e); + obj.Slider.Limits = [0 days(obj.Limits(2) - obj.Limits(1))] + 1; + obj.Slider.Value = days(defaultValue - obj.Limits(1)) + 1; + + % Date picker + obj.Datepicker = uidatepicker(obj.Grid); + obj.Datepicker.ValueChangedFcn = @(h,e)obj.onDatepickerChanged(e); + obj.Datepicker.Value = defaultValue; + obj.Datepicker.Limits = obj.Limits; + obj.Datepicker.Editable = false; + + % Button grid + obj.GridButtons = uigridlayout(obj.Grid); + obj.GridButtons.RowHeight = {"1x", "1x"}; + obj.GridButtons.ColumnWidth = "fit"; + obj.GridButtons.ColumnSpacing = 0; + obj.GridButtons.RowSpacing = 2; + obj.GridButtons.Padding(:) = 0; + + obj.Buttons = [uibutton(obj.GridButtons) uibutton(obj.GridButtons)]; + obj.Buttons(1).ButtonPushedFcn = @(h,e)obj.onButtonPushed(e); + obj.Buttons(1).Tag = "up"; + obj.Buttons(1).Icon = "up_grey_12.png"; + obj.Buttons(1).Text = ""; + obj.Buttons(2).ButtonPushedFcn = @(h,e)obj.onButtonPushed(e); + obj.Buttons(2).Tag = "down"; + obj.Buttons(2).Icon = "down_grey_12.png"; + obj.Buttons(2).Text = ""; + + % Update the internal component lists + obj.BackgroundColorableComponents = obj.Grid; + obj.FontStyledComponents = [obj.Datepicker, obj.Slider]; + obj.EnableableComponents = [obj.Datepicker, obj.Slider, obj.Buttons]; + obj.FieldColorableComponents = [obj.Datepicker]; + obj.ButtonColorableComponents = [obj.Buttons]; + + end %function + + + function update(obj) + + % Set the value ranges + + % Set orientation in grid + if obj.Orientation == "vertical" + obj.Grid.ColumnWidth = {'1x', 'fit'}; + obj.Grid.RowHeight = {'1x', obj.DatepickerSize}; + obj.Datepicker.Layout.Row = 2; + obj.Datepicker.Layout.Column = 1; + obj.GridButtons.Layout.Row = 2; + obj.GridButtons.Layout.Column = 2; + obj.Slider.Orientation = "vertical"; + else + obj.Grid.ColumnWidth = {'1x', obj.DatepickerSize, 'fit'}; + obj.Grid.RowHeight = {'1x'}; + obj.Datepicker.Layout.Row = 1; + obj.Datepicker.Layout.Column = 2; + obj.GridButtons.Layout.Row = 1; + obj.GridButtons.Layout.Column = 3; + obj.Slider.Orientation = "horizontal"; + end + + % Set component range + obj.Slider.Limits = [0 days(obj.Limits(2) - obj.Limits(1))] + 1; + obj.Datepicker.Limits = obj.Limits; + + % Update ticks + updateSliderTicks(obj) + + % Set value + if isnat(obj.Datepicker.Value) + obj.Datepicker.Value = obj.Limits(1); + end + obj.Slider.Value = days(obj.Datepicker.Value - obj.Limits(1)) + 1; + + % Update the buttons + updateButtonEnable(obj) + + end %function + + + function onButtonPushed(obj, evt) + % Triggered on button pushed + + % What button was pushed? + oldDate = obj.Value; + newDate = obj.Value; + switch evt.Source.Tag + case "up" + newDate = min(newDate + obj.Step, obj.Limits(2)); + case "down" + newDate = max(newDate - obj.Step, obj.Limits(1)); + + end + + % Update the state + obj.Datepicker.Value = newDate; + obj.Slider.Value = days(newDate - obj.Limits(1)) + 1; + + % Update button enable status + updateButtonEnable(obj) + + % Trigger event + evtOut = wt.eventdata.PropertyChangedData('Value', newDate, oldDate); + notify(obj, "ValueChanged", evtOut); + + end %function + + function onSliderChanged(obj,evt) + % Triggered on slider moved + + % What changed? + newValue = evt.Value; + oldValue = obj.Datepicker.Value; + + % Round value to whole days + newValue = round(newValue); + newValue = min(max(newValue,1), days(obj.Limits(2) - obj.Limits(1)) + 1); + + % Prepare event data + newDate = obj.Limits(1) + days(newValue - 1); + evtOut = wt.eventdata.PropertyChangedData('Value', newDate, oldValue); + + % Update the state + obj.Datepicker.Value = newDate; + + % Skip for event ValueChanging for performance + if evt.EventName == "ValueChanged" + obj.Slider.Value = newValue; + + % Update button enable status + updateButtonEnable(obj) + end + + % Trigger event ("ValueChanged" or "ValueChanging") + notify(obj, evt.EventName, evtOut); + + end %function + + + function onDatepickerChanged(obj,e) + % Triggered on button pushed + + % What changed? + newValue = e.Value; + + % Prepare event data + evtOut = wt.eventdata.PropertyChangedData('Value', newValue, e.PreviousValue); + + % Update the state + obj.Slider.Value = days(newValue - obj.Limits(1)) + 1; + + % Update button enable status + updateButtonEnable(obj) + + % Trigger event + notify(obj, "ValueChanged", evtOut); + + end %function + + + function updateButtonEnable(obj) + + % What are the buttons? + + % Limits of interval reached? + if obj.Value <= obj.Limits(1) + obj.Buttons(1).Enable = true; + obj.Buttons(2).Enable = false; + elseif obj.Value >= obj.Limits(2) + obj.Buttons(1).Enable = false; + obj.Buttons(2).Enable = true; + else + obj.Buttons(1).Enable = true; + obj.Buttons(2).Enable = true; + end %if + + end %function + + function updateSliderTicks(obj) + % Update the limits in date-picker and slider + + % Get ticks + [majorTicks, minorTicks] = getSliderTicks( ... + obj, ... + obj.Orientation, ... + "TickLength", strlength(obj.DisplayFormat)); + + % Set slider limits and ticks + obj.Slider.MajorTicks = majorTicks; + obj.Slider.MinorTicks = minorTicks; + obj.Slider.MajorTickLabels = categorical(obj.Limits(1) + days(majorTicks - 1)); + + end %function + + end %methods + +end % classdef diff --git a/widgets/doc/WidgetsList.html b/widgets/doc/WidgetsList.html index 30ced371..8b7554ca 100644 --- a/widgets/doc/WidgetsList.html +++ b/widgets/doc/WidgetsList.html @@ -9,8 +9,7 @@ .S5 { border-left: 0.666667px solid rgb(217, 217, 217); border-right: 0.666667px solid rgb(217, 217, 217); border-top: 0px none rgb(33, 33, 33); border-bottom: 0.666667px solid rgb(217, 217, 217); border-radius: 0px; padding: 0px 45px 4px 13px; line-height: 18.004px; min-height: 0px; white-space: nowrap; color: rgb(33, 33, 33); font-family: Menlo, Monaco, Consolas, "Courier New", monospace, Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 14px; } .S6 { color: rgb(33, 33, 33); padding: 10px 0px 6px 17px; background: rgb(255, 255, 255) none repeat scroll 0% 0% / auto padding-box border-box; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 14px; overflow-x: hidden; line-height: 17.234px; } .S7 { margin: 10px 10px 9px 4px; padding: 0px; line-height: 21px; min-height: 0px; white-space: pre-wrap; color: rgb(33, 33, 33); font-family: Helvetica, Arial, sans-serif, Helvetica, Arial, sans-serif; font-style: normal; font-size: 14px; font-weight: 400; text-align: left; } -.S8 { margin: 20px 10px 5px 4px; padding: 0px; line-height: 25px; min-height: 0px; white-space: pre-wrap; color: rgb(33, 33, 33); font-family: Helvetica, Arial, sans-serif, Helvetica, Arial, sans-serif; font-style: normal; font-size: 20px; font-weight: 700; text-align: left; } -.S9 { border-left: 0.666667px solid rgb(217, 217, 217); border-right: 0.666667px solid rgb(217, 217, 217); border-top: 0px none rgb(33, 33, 33); border-bottom: 0.666667px solid rgb(217, 217, 217); border-radius: 0px 0px 4px 4px; padding: 0px 45px 4px 13px; line-height: 18.004px; min-height: 0px; white-space: nowrap; color: rgb(33, 33, 33); font-family: Menlo, Monaco, Consolas, "Courier New", monospace, Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 14px; }

Listing of Included Widgets

Button Grid

The wt.ButtonGrid widget provides an array of pushbutton controls that are grouped together. You provide an array of icons and/or text labels for the buttons. It has a single callback function that provides eventdata indicating which button was pushed.
% Create a figure with a grid layout
figObj = uifigure("Position",[100 100 250 75]);
gridObj = uigridlayout(figObj,[1 1],"BackgroundColor",[.6 .8 1]);
 
% Create the widget
buttonGridWidget = wt.ButtonGrid(gridObj);
buttonGridWidget.BackgroundColor = [.6 .8 1];
buttonGridWidget.Icon = [
"add_24.png"
"delete_24.png"
"play_24.png"
"pause_24.png"
"stop_24.png"
];
buttonGridWidget.Text = [
"Add"
"Delete"
"Play"
"Pause"
"Stop"
];

Checkbox List

The wt.CheckboxList widget provides an array of checkboxes within a scrollable panel.
% Create a figure with a grid layout
figObj = uifigure("Position",[100 100 250 200]);
gridObj = uigridlayout(figObj,[1 1],"BackgroundColor",[.6 .8 1]);
 
% Create the widget
cbListWidget = wt.CheckboxList(gridObj);
 
% Configure the widget
cbListWidget.Items = [
"Boston"
"Chicago"
"Houston"
"Los Angeles"
"Miami"
"New York"
"Seattle"
];
cbListWidget.Value(3) = false;
cbListWidget.ShowSelectAll = true;

Color Selector

The wt.ColorSelector widget enables you to have a color selection edit field along with a button that opens a color selection dialog.
% Create a figure with a grid layout
figObj = uifigure("Position",[100 100 250 45]);
gridObj = uigridlayout(figObj,[1 1],"BackgroundColor",[.6 .8 1]);
 
% Create the widget
colorWidget = wt.ColorSelector(gridObj);
colorWidget.Value = [1 0 0];
If you are using R2024a or later, consider if the built-in uicolorpicker meets your needs instead.

Contextual View

The wt.ContextualView widget is an advanced component that is visually like a tabpanel but without the tabs. It is intended for use in apps with a modular implementation using the model-view-controller (MVC) design pattern. The ContextualView provides a multiple-view container for recangular contents similar to a uitabgroup. However, unliike uitabgroup, the ContextualView provides only a programmatic interface for launching a component and placing it within the widget.
ContextualViewAnimated.gif

Datetime Selector

The wt.DatetimeSelector widget provides a combined date and time selection. You may optionally include seconds, time zone, and AM/PM selection. It's value uses a datetime object.
% Create a figure with a grid layout
figObj = uifigure("Position",[100 100 350 45]);
gridObj = uigridlayout(figObj,[1 1],"BackgroundColor",[.6 .8 1]);
 
% Create the widget
dateWidget = wt.DatetimeSelector(gridObj);

Drop Down List Manager

The wt.DropDownListManager widget provides the user to edit and select from a list of names, and it does so within a compact space. You can easily add, remove, or rename items from the dropdown control using the connected buttons.
% Create a figure with a grid layout
figObj = uifigure("Position",[100 100 350 50]);
gridObj = uigridlayout(figObj,[1 1],"BackgroundColor",[.6 .8 1]);
gridObj.RowHeight = {30};
 
% Create the widget
ddlWidget = wt.DropDownListManager(gridObj);
ddlWidget.AllowRemove = true;
ddlWidget.AllowRename = true;
ddlWidget.Items = [
"Initial Configuration"
"Revision 1"
"Revision 2"
];
ddlWidget.Value = "Revision 1";

File Selector

The wt.FileSelector widget provides a file path edit field along with a button that opens a file selection dialog.
% Create a figure with a grid layout
figObj = uifigure("Position",[100 100 400 45]);
gridObj = uigridlayout(figObj,[1 1],"BackgroundColor",[.6 .8 1]);
 
% Create the widget
fileWidget = wt.FileSelector(gridObj);
fileWidget.Value = matlabroot;
fileWidget.ShowHistory = true;

List Selector (single pane)

The wt.ListSelector widget is intended for adding items to a list from a known set. You can optionally enable reordering the items, and add custom buttons.
% Create a figure with a grid layout
figObj = uifigure("Position",[100 100 250 215]);
gridObj = uigridlayout(figObj,[1 1],"BackgroundColor",[.6 .8 1]);
 
% Load data
load("usastates.mat","usastates");
stateNames = string({usastates.Name});
 
% Create the widget
listWidget = wt.ListSelector(gridObj);
listWidget.Items = stateNames;
listWidget.Value = ["Massachusetts","Michigan"];

List Selector Two Pane

The wt.ListSelectorTwoPane widget is similar to the one-pane variant. It is intended for adding items to a list from a known set. In this case, the set of available items is displayed in a list on the left side, and items are selectively added to the right list.
Again you can optionally enable reordering the items and add custom buttons.
% Create a figure with a grid layout
figObj = uifigure("Position",[100 100 300 215]);
gridObj = uigridlayout(figObj,[1 1],"BackgroundColor",[.6 .8 1]);
 
% Load data
load("usastates.mat","usastates");
stateNames = string({usastates.Name});
 
% Create the widget
listWidget2 = wt.ListSelectorTwoPane(gridObj);
listWidget2.Items = stateNames;
listWidget2.Value = ["California","Massachusetts","Michigan","Texas"];

Menu Button

The wt.MenuButton widget implements a small button that provides a pop-up menu when pushed.
% Create a figure with a grid layout
figObj = uifigure("Position",[100 100 250 150]);
gridObj = uigridlayout(figObj,"BackgroundColor",[.6 .8 1]);
gridObj.RowHeight = {'fit','1x'};
gridObj.ColumnWidth = {'1x','fit'};
 
% Create the widget
mbWidget = wt.MenuButton(gridObj);
mbWidget.Layout.Column = 2;
mbWidget.MenuSelectedFcn = @(src,evt)disp(evt);
names = ["Item 1","Item 2","Item 3"];
menuItems = mbWidget.addMenuItems(names);

Numeric Array

The wt.NumericArray widget combines multiple numeric edit fields that form elements of a 1-dimensional array. It is intended for small arrays typically 2 to 5 values. Optionally you may impose restrictions on the numerical range. You can also force the array to be increasing or decreasing. You can also set orientation as horizontal or vertical.
% Create a figure with a grid layout
figObj = uifigure("Position",[100 100 250 50]);
gridObj = uigridlayout(figObj,[1 1],"BackgroundColor",[.6 .8 1]);
 
% Create the widget
naWidget = wt.NumericArray(gridObj);
naWidget.Value = [10 20 30 40];

Password Field

The wt.PasswordField widget provides a password field with text displayed as dots.
% Create a figure with a grid layout
figObj = uifigure("Position",[100 100 250 45]);
gridObj = uigridlayout(figObj,[1 1],"BackgroundColor",[.6 .8 1]);
 
% Create the widget
passwordWidget = wt.PasswordField(gridObj);
passwordWidget.Value = "MathWorks";
PasswordWidget.png

Progress Bar

The wt.ProgressBar widget provides a progress indicator that can be embedded within your app.
% Create a figure with a grid layout
figObj = uifigure("Position",[100 100 300 60]);
gridObj = uigridlayout(figObj,[1 1],"BackgroundColor",[.6 .8 1]);
 
% Create the widget
progressWidget = wt.ProgressBar(gridObj);
 
% Configure the widget
progressWidget.ShowCancel = true;
progressWidget.setProgress(0.5, "The task is running...");

Range Field

The wt.RangeField widget combines multiple numeric edit fields that form elements of a 1-dimensional array. It is intended for small arrays typically 2 to 5 values. Optionally you may impose restrictions on the numerical range. You can also force the array to be increasing or decreasing. You can also set orientation as horizontal or vertical.
% Create a figure with a grid layout
figObj = uifigure("Position",[100 100 250 50]);
gridObj = uigridlayout(figObj,[1 1],"BackgroundColor",[.6 .8 1]);
 
% Create the widget
rfWidget = wt.RangeField(gridObj);
rfWidget.Value = [100 500];

Row Entries Table

The wt.RowEntriesTable widget is intended to enable display and editing of simple lists when each item (row) has multiple attributes (columns) to configure. You can add or remove rows using the buttons. You may optionally enable ordering buttons to move items up and down the list.
% Create a figure with a grid layout
figObj = uifigure("Position",[100 100 250 220]);
gridObj = uigridlayout(figObj,[1 1],"BackgroundColor",[.6 .8 1]);
 
% Create the widget
rtWidget = wt.RowEntriesTable(gridObj);
rtWidget.Orderable = true; % Enables the up/down buttons to reorder items
fruitData = cell2table({
"Apple", 195
"Banana", 120
"Lemon", 100
"Lime", 50
"Orange", 130
"Pear", 180
}, "VariableNames", ["Name","Mass (g)"]);
meanMass = mean(fruitData.("Mass (g)"));
fruitData = [
{"Overall Average", meanMass}
fruitData
];
rtWidget.AllowItemRemove(1) = false;
rtWidget.AllowItemOrdering(1) = false;
firstRowStyle = uistyle("BackgroundColor",[.8 .9 .8],"FontWeight","bold","FontColor",[.5 0 1]);
rtWidget.addStyle(firstRowStyle, "row", 1)
pause(2); drawnow
rtWidget.Data = fruitData; drawnow

Search Dropdown

The wt.SliderCheckboxGroup widget provides a set of slider values with checkboxes that enable or disable them. This is useful for setting things like the visibilities of colors or layer transparency that varies between 0 and 1.
% Create a figure with a grid layout
figObj = uifigure("Position",[100 100 250 250]);
gridObj = uigridlayout(figObj,[1 1],"BackgroundColor",[.6 .8 1]);
gridObj.RowHeight = {'fit'};
 
% Create and configure the widget:
sdWidget = wt.SearchDropDown(gridObj);
sdWidget.Items = ["Primary Current","Secondary Current","Primary Voltage","Secondary Voltage"];
SearchDropdownExampleOpen.png SearchDropdownExample.png

Slider Checkbox Combination Group

The wt.SearchDropdown widget provides an edit field that will search the typed substring from a list of items that pops up in a listbox below.
% Create a figure with a grid layout
figObj = uifigure("Position",[100 100 350 150]);
gridObj = uigridlayout(figObj,[1 1],"BackgroundColor",[.6 .8 1]);
 
% Create the widget
sliderCheckboxWidget = wt.SliderCheckboxGroup(gridObj);
 
% Configure the widget
sliderCheckboxWidget.Name = ["Red", "Green", "Blue", "Alpha"];
sliderCheckboxWidget.CheckboxWidth = 100;
sliderCheckboxWidget.Value = [0.8, 0.3, 0, 1];
sliderCheckboxWidget.State = [true, true, false, true];

Slider Spinner Combination

The wt.SliderSpinner widget provides a slider alongside a numeric edit field with spinner buttons. It is useful if the user needs to be able to both slide the value or enter it as text.
% Create a figure with a grid layout
figObj = uifigure("Position",[100 100 250 70]);
gridObj = uigridlayout(figObj,[1 1],"BackgroundColor",[.6 .8 1]);
 
% Create the widget
sliderSpinnerWidget = wt.SliderSpinner(gridObj);
 
% Configure the widget
sliderSpinnerWidget.Limits = [-10 30];
sliderSpinnerWidget.RoundFractionalValues = "off";
sliderSpinnerWidget.Step = 0.5;
sliderSpinnerWidget.Value = 16.5;

Task Status Table

The wt.TaskStatusTable widget provides a list of process or workflow steps that should be performed in order. It enables stepping forward or back using the included buttons. You can also choose from standard status icons beside each item in the list.
% Create a figure with a grid layout
figObj = uifigure("Position",[100 100 250 225]);
gridObj = uigridlayout(figObj,[1 1],"BackgroundColor",[.6 .8 1]);
 
% Create the widget
taskStatusWidget = wt.TaskStatusTable(gridObj);
 
% Configure the widget
taskStatusWidget.Items = [
"Import Data"
"Preprocess Data"
"Analyze Data"
"Plot Results"
"Save Results"
];
taskStatusWidget.Status = [
"complete"
"warning"
"running"
"none"
"none"
];
taskStatusWidget.SelectedIndex = 3;

Toolbar

The wt.Toolbar widget enables you to construct a lightweight toolstrip or toolbar for controlling your app. You can add buttons with icons and text and create multiple sections.
% Create a figure with a grid layout
figObj = uifigure("Position",[100 100 660 120]);
gridObj = uigridlayout(figObj,[1 1],"BackgroundColor",[.6 .8 1]);
 
% Create the widget
toolbarWidget1 = wt.Toolbar(gridObj);
 
% Create a horizontal section
section1 = wt.toolbar.HorizontalSection();
section1.Title = "NORMAL BUTTONS";
section1.addButton("open_24.png", "Open");
section1.addButton("save_24.png", "Save");
 
% Create a horizontal section with state buttons
section2 = wt.toolbar.HorizontalSection();
section2.Title = "STATE BUTTONS";
stateButton1 = section2.addStateButton("","Mode 1");
stateButton2 = section2.addStateButton("","Mode 2");
stateButton3 = section2.addStateButton("","Mode 3");
 
% Set the state of the buttons
stateButton1.Value = true;
stateButton2.Value = false;
stateButton3.Value = false;
 
% Create a horizontal section with vertical subsection
section3 = wt.toolbar.HorizontalSection();
section3.Title = "MIXED HORIZONTAL / VERTICAL";
section3.addButton("left_24.png","");
section3.addButton("right_24.png","");
 
% Create the vertical subsection
section3v1 = section3.addVerticalSection();
section3v1.addButton("play_24.png","Play");
section3v1.addButton("stop_24.png","Stop");
 
% Add a manual component to the vertical section
sliderObj = uislider("Parent",[]);
section3v1.Component(end+1) = sliderObj;
 
% Attach a callback to the slider
% This is required for manually adding components
sliderObj.ValueChangedFcn = @(src,evt)disp(evt);
 
% Create more horizontal items
section3.addButton("pause_24.png","Pause");
 
% Add another basic section
section4 = wt.toolbar.HorizontalSection();
section4.Title = "HELP";
section4.addButton('help_24.png','Help');
 
% Attach the horizontal sections to the toolbar
toolbarWidget1.Section = [
section1
section2
section3
];
drawnow % to get it to render the width before snapshot occurs
toolbarWidget1.Section(end+1) = section4;
Copyright 2020-2025 The MathWorks, Inc.
+.S8 { border-left: 0.666667px solid rgb(217, 217, 217); border-right: 0.666667px solid rgb(217, 217, 217); border-top: 0px none rgb(33, 33, 33); border-bottom: 0.666667px solid rgb(217, 217, 217); border-radius: 0px 0px 4px 4px; padding: 0px 45px 4px 13px; line-height: 18.004px; min-height: 0px; white-space: nowrap; color: rgb(33, 33, 33); font-family: Menlo, Monaco, Consolas, "Courier New", monospace, Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 14px; }

Listing of Included Widgets

Button Grid

The wt.ButtonGrid widget provides an array of pushbutton controls that are grouped together. You provide an array of icons and/or text labels for the buttons. It has a single callback function that provides eventdata indicating which button was pushed.
% Create a figure with a grid layout
figObj = uifigure("Position",[100 100 250 75]);
gridObj = uigridlayout(figObj,[1 1],"BackgroundColor",[.6 .8 1]);
 
% Create the widget
buttonGridWidget = wt.ButtonGrid(gridObj);
buttonGridWidget.BackgroundColor = [.6 .8 1];
buttonGridWidget.Icon = [
"add_24.png"
"delete_24.png"
"play_24.png"
"pause_24.png"
"stop_24.png"
];
buttonGridWidget.Text = [
"Add"
"Delete"
"Play"
"Pause"
"Stop"
];

Checkbox List

The wt.CheckboxList widget provides an array of checkboxes within a scrollable panel.
% Create a figure with a grid layout
figObj = uifigure("Position",[100 100 250 200]);
gridObj = uigridlayout(figObj,[1 1],"BackgroundColor",[.6 .8 1]);
 
% Create the widget
cbListWidget = wt.CheckboxList(gridObj);
 
% Configure the widget
cbListWidget.Items = [
"Boston"
"Chicago"
"Houston"
"Los Angeles"
"Miami"
"New York"
"Seattle"
];
cbListWidget.Value(3) = false;
cbListWidget.ShowSelectAll = true;

Color Selector

The wt.ColorSelector widget enables you to have a color selection edit field along with a button that opens a color selection dialog.
% Create a figure with a grid layout
figObj = uifigure("Position",[100 100 250 45]);
gridObj = uigridlayout(figObj,[1 1],"BackgroundColor",[.6 .8 1]);
 
% Create the widget
colorWidget = wt.ColorSelector(gridObj);
colorWidget.Value = [1 0 0];
If you are using R2024a or later, consider if the built-in uicolorpicker meets your needs instead.

Contextual View

The wt.ContextualView widget is an advanced component that is visually like a tabpanel but without the tabs. It is intended for use in apps with a modular implementation using the model-view-controller (MVC) design pattern. The ContextualView provides a multiple-view container for recangular contents similar to a uitabgroup. However, unliike uitabgroup, the ContextualView provides only a programmatic interface for launching a component and placing it within the widget.
ContextualViewAnimated.gif

Date Slider

The wt.DateSlider widget provides a combined date picker and slider. Its Value uses a datetime object and is compatible with the toolbox support range.
% Create a figure with a grid layout
figObj = uifigure("Position",[100 100 400 60]);
gridObj = uigridlayout(figObj,[1 1],"BackgroundColor",[.6 .8 1]);
 
% Create the widget
dateSliderWidget = wt.DateSlider(gridObj);

Date Range Slider

The wt.DateRangeSlider widget provides a combined date picker and range slider. It requires MATLAB R2024b or later and is available for programmatic use only while the App Designer component registry targets R2022a. Its Value uses a two-element datetime row vector.
% Create a figure with a grid layout
figObj = uifigure("Position",[100 100 900 60]);
gridObj = uigridlayout(figObj,[1 1],"BackgroundColor",[.6 .8 1]);
 
% Create the widget
dateRangeSliderWidget = wt.DateRangeSlider(gridObj);

Datetime Selector

The wt.DatetimeSelector widget provides a combined date and time selection. You may optionally include seconds, time zone, and AM/PM selection. It's value uses a datetime object.
% Create a figure with a grid layout
figObj = uifigure("Position",[100 100 350 45]);
gridObj = uigridlayout(figObj,[1 1],"BackgroundColor",[.6 .8 1]);
 
% Create the widget
dateWidget = wt.DatetimeSelector(gridObj);

Drop Down List Manager

The wt.DropDownListManager widget provides the user to edit and select from a list of names, and it does so within a compact space. You can easily add, remove, or rename items from the dropdown control using the connected buttons.
% Create a figure with a grid layout
figObj = uifigure("Position",[100 100 350 50]);
gridObj = uigridlayout(figObj,[1 1],"BackgroundColor",[.6 .8 1]);
gridObj.RowHeight = {30};
 
% Create the widget
ddlWidget = wt.DropDownListManager(gridObj);
ddlWidget.AllowRemove = true;
ddlWidget.AllowRename = true;
ddlWidget.Items = [
"Initial Configuration"
"Revision 1"
"Revision 2"
];
ddlWidget.Value = "Revision 1";

File Selector

The wt.FileSelector widget provides a file path edit field along with a button that opens a file selection dialog.
% Create a figure with a grid layout
figObj = uifigure("Position",[100 100 400 45]);
gridObj = uigridlayout(figObj,[1 1],"BackgroundColor",[.6 .8 1]);
 
% Create the widget
fileWidget = wt.FileSelector(gridObj);
fileWidget.Value = matlabroot;
fileWidget.ShowHistory = true;

List Selector (single pane)

The wt.ListSelector widget is intended for adding items to a list from a known set. You can optionally enable reordering the items, and add custom buttons.
% Create a figure with a grid layout
figObj = uifigure("Position",[100 100 250 215]);
gridObj = uigridlayout(figObj,[1 1],"BackgroundColor",[.6 .8 1]);
 
% Load data
load("usastates.mat","usastates");
stateNames = string({usastates.Name});
 
% Create the widget
listWidget = wt.ListSelector(gridObj);
listWidget.Items = stateNames;
listWidget.Value = ["Massachusetts","Michigan"];

List Selector Two Pane

The wt.ListSelectorTwoPane widget is similar to the one-pane variant. It is intended for adding items to a list from a known set. In this case, the set of available items is displayed in a list on the left side, and items are selectively added to the right list.
Again you can optionally enable reordering the items and add custom buttons.
% Create a figure with a grid layout
figObj = uifigure("Position",[100 100 300 215]);
gridObj = uigridlayout(figObj,[1 1],"BackgroundColor",[.6 .8 1]);
 
% Load data
load("usastates.mat","usastates");
stateNames = string({usastates.Name});
 
% Create the widget
listWidget2 = wt.ListSelectorTwoPane(gridObj);
listWidget2.Items = stateNames;
listWidget2.Value = ["California","Massachusetts","Michigan","Texas"];

Menu Button

The wt.MenuButton widget implements a small button that provides a pop-up menu when pushed.
% Create a figure with a grid layout
figObj = uifigure("Position",[100 100 250 150]);
gridObj = uigridlayout(figObj,"BackgroundColor",[.6 .8 1]);
gridObj.RowHeight = {'fit','1x'};
gridObj.ColumnWidth = {'1x','fit'};
 
% Create the widget
mbWidget = wt.MenuButton(gridObj);
mbWidget.Layout.Column = 2;
mbWidget.MenuSelectedFcn = @(src,evt)disp(evt);
names = ["Item 1","Item 2","Item 3"];
menuItems = mbWidget.addMenuItems(names);

Numeric Array

The wt.NumericArray widget combines multiple numeric edit fields that form elements of a 1-dimensional array. It is intended for small arrays typically 2 to 5 values. Optionally you may impose restrictions on the numerical range. You can also force the array to be increasing or decreasing. You can also set orientation as horizontal or vertical.
% Create a figure with a grid layout
figObj = uifigure("Position",[100 100 250 50]);
gridObj = uigridlayout(figObj,[1 1],"BackgroundColor",[.6 .8 1]);
 
% Create the widget
naWidget = wt.NumericArray(gridObj);
naWidget.Value = [10 20 30 40];

Password Field

The wt.PasswordField widget provides a password field with text displayed as dots.
% Create a figure with a grid layout
figObj = uifigure("Position",[100 100 250 45]);
gridObj = uigridlayout(figObj,[1 1],"BackgroundColor",[.6 .8 1]);
 
% Create the widget
passwordWidget = wt.PasswordField(gridObj);
passwordWidget.Value = "MathWorks";
PasswordWidget.png

Progress Bar

The wt.ProgressBar widget provides a progress indicator that can be embedded within your app.
% Create a figure with a grid layout
figObj = uifigure("Position",[100 100 300 60]);
gridObj = uigridlayout(figObj,[1 1],"BackgroundColor",[.6 .8 1]);
 
% Create the widget
progressWidget = wt.ProgressBar(gridObj);
 
% Configure the widget
progressWidget.ShowCancel = true;
progressWidget.setProgress(0.5, "The task is running...");

Range Field

The wt.RangeField widget combines multiple numeric edit fields that form elements of a 1-dimensional array. It is intended for small arrays typically 2 to 5 values. Optionally you may impose restrictions on the numerical range. You can also force the array to be increasing or decreasing. You can also set orientation as horizontal or vertical.
% Create a figure with a grid layout
figObj = uifigure("Position",[100 100 250 50]);
gridObj = uigridlayout(figObj,[1 1],"BackgroundColor",[.6 .8 1]);
 
% Create the widget
rfWidget = wt.RangeField(gridObj);
rfWidget.Value = [100 500];

Row Entries Table

The wt.RowEntriesTable widget is intended to enable display and editing of simple lists when each item (row) has multiple attributes (columns) to configure. You can add or remove rows using the buttons. You may optionally enable ordering buttons to move items up and down the list.
% Create a figure with a grid layout
figObj = uifigure("Position",[100 100 250 220]);
gridObj = uigridlayout(figObj,[1 1],"BackgroundColor",[.6 .8 1]);
 
% Create the widget
rtWidget = wt.RowEntriesTable(gridObj);
rtWidget.Orderable = true; % Enables the up/down buttons to reorder items
fruitData = cell2table({
"Apple", 195
"Banana", 120
"Lemon", 100
"Lime", 50
"Orange", 130
"Pear", 180
}, "VariableNames", ["Name","Mass (g)"]);
meanMass = mean(fruitData.("Mass (g)"));
fruitData = [
{"Overall Average", meanMass}
fruitData
];
rtWidget.AllowItemRemove(1) = false;
rtWidget.AllowItemOrdering(1) = false;
firstRowStyle = uistyle("BackgroundColor",[.8 .9 .8],"FontWeight","bold","FontColor",[.5 0 1]);
rtWidget.addStyle(firstRowStyle, "row", 1)
pause(2); drawnow
rtWidget.Data = fruitData; drawnow

Search Dropdown

The wt.SliderCheckboxGroup widget provides a set of slider values with checkboxes that enable or disable them. This is useful for setting things like the visibilities of colors or layer transparency that varies between 0 and 1.
% Create a figure with a grid layout
figObj = uifigure("Position",[100 100 250 250]);
gridObj = uigridlayout(figObj,[1 1],"BackgroundColor",[.6 .8 1]);
gridObj.RowHeight = {'fit'};
 
% Create and configure the widget:
sdWidget = wt.SearchDropDown(gridObj);
sdWidget.Items = ["Primary Current","Secondary Current","Primary Voltage","Secondary Voltage"];
SearchDropdownExampleOpen.png SearchDropdownExample.png

Slider Checkbox Combination Group

The wt.SearchDropdown widget provides an edit field that will search the typed substring from a list of items that pops up in a listbox below.
% Create a figure with a grid layout
figObj = uifigure("Position",[100 100 350 150]);
gridObj = uigridlayout(figObj,[1 1],"BackgroundColor",[.6 .8 1]);
 
% Create the widget
sliderCheckboxWidget = wt.SliderCheckboxGroup(gridObj);
 
% Configure the widget
sliderCheckboxWidget.Name = ["Red", "Green", "Blue", "Alpha"];
sliderCheckboxWidget.CheckboxWidth = 100;
sliderCheckboxWidget.Value = [0.8, 0.3, 0, 1];
sliderCheckboxWidget.State = [true, true, false, true];

Slider Spinner Combination

The wt.SliderSpinner widget provides a slider alongside a numeric edit field with spinner buttons. It is useful if the user needs to be able to both slide the value or enter it as text.
% Create a figure with a grid layout
figObj = uifigure("Position",[100 100 250 70]);
gridObj = uigridlayout(figObj,[1 1],"BackgroundColor",[.6 .8 1]);
 
% Create the widget
sliderSpinnerWidget = wt.SliderSpinner(gridObj);
 
% Configure the widget
sliderSpinnerWidget.Limits = [-10 30];
sliderSpinnerWidget.RoundFractionalValues = "off";
sliderSpinnerWidget.Step = 0.5;
sliderSpinnerWidget.Value = 16.5;

Task Status Table

The wt.TaskStatusTable widget provides a list of process or workflow steps that should be performed in order. It enables stepping forward or back using the included buttons. You can also choose from standard status icons beside each item in the list.
% Create a figure with a grid layout
figObj = uifigure("Position",[100 100 250 225]);
gridObj = uigridlayout(figObj,[1 1],"BackgroundColor",[.6 .8 1]);
 
% Create the widget
taskStatusWidget = wt.TaskStatusTable(gridObj);
 
% Configure the widget
taskStatusWidget.Items = [
"Import Data"
"Preprocess Data"
"Analyze Data"
"Plot Results"
"Save Results"
];
taskStatusWidget.Status = [
"complete"
"warning"
"running"
"none"
"none"
];
taskStatusWidget.SelectedIndex = 3;

Toolbar

The wt.Toolbar widget enables you to construct a lightweight toolstrip or toolbar for controlling your app. You can add buttons with icons and text and create multiple sections.
% Create a figure with a grid layout
figObj = uifigure("Position",[100 100 660 120]);
gridObj = uigridlayout(figObj,[1 1],"BackgroundColor",[.6 .8 1]);
 
% Create the widget
toolbarWidget1 = wt.Toolbar(gridObj);
 
% Create a horizontal section
section1 = wt.toolbar.HorizontalSection();
section1.Title = "NORMAL BUTTONS";
section1.addButton("open_24.png", "Open");
section1.addButton("save_24.png", "Save");
 
% Create a horizontal section with state buttons
section2 = wt.toolbar.HorizontalSection();
section2.Title = "STATE BUTTONS";
stateButton1 = section2.addStateButton("","Mode 1");
stateButton2 = section2.addStateButton("","Mode 2");
stateButton3 = section2.addStateButton("","Mode 3");
 
% Set the state of the buttons
stateButton1.Value = true;
stateButton2.Value = false;
stateButton3.Value = false;
 
% Create a horizontal section with vertical subsection
section3 = wt.toolbar.HorizontalSection();
section3.Title = "MIXED HORIZONTAL / VERTICAL";
section3.addButton("left_24.png","");
section3.addButton("right_24.png","");
 
% Create the vertical subsection
section3v1 = section3.addVerticalSection();
section3v1.addButton("play_24.png","Play");
section3v1.addButton("stop_24.png","Stop");
 
% Add a manual component to the vertical section
sliderObj = uislider("Parent",[]);
section3v1.Component(end+1) = sliderObj;
 
% Attach a callback to the slider
% This is required for manually adding components
sliderObj.ValueChangedFcn = @(src,evt)disp(evt);
 
% Create more horizontal items
section3.addButton("pause_24.png","Pause");
 
% Add another basic section
section4 = wt.toolbar.HorizontalSection();
section4.Title = "HELP";
section4.addButton('help_24.png','Help');
 
% Attach the horizontal sections to the toolbar
toolbarWidget1.Section = [
section1
section2
section3
];
drawnow % to get it to render the width before snapshot occurs
toolbarWidget1.Section(end+1) = section4;
Copyright 2020-2026 The MathWorks, Inc.

\ No newline at end of file diff --git a/widgets/doc/WidgetsList.mlx b/widgets/doc/WidgetsList.mlx index 8721f108..cc0d6a9d 100644 Binary files a/widgets/doc/WidgetsList.mlx and b/widgets/doc/WidgetsList.mlx differ diff --git a/widgets/doc/custom_toolbox.json b/widgets/doc/custom_toolbox.json index bbe0cd9d..866994ab 100644 --- a/widgets/doc/custom_toolbox.json +++ b/widgets/doc/custom_toolbox.json @@ -1 +1 @@ -{"Name":"Widgets (App Designer and Advanced App Components) Toolbox","ShortName":"widgets__app_designer_and_advanced_app_components__toolbox","ID":"3ptoolbox::widgets__app_designer_and_advanced_app_components__toolbox","ToolboxHelpLocations":[{"ContentType":"doc","LocationOnDisk":"C:\\Work_Internal\\widgets-toolbox\\widgets\\doc","AbsolutePath":"C:\\Work_Internal\\widgets-toolbox\\widgets\\info.xml","HelpLocation":"3ptoolbox/widgetsappdesignerandadvancedappcomponentstoolbox/doc"},{"ContentType":"examples","LocationOnDisk":"C:\\Work_Internal\\widgets-toolbox\\widgets","AbsolutePath":"C:\\Work_Internal\\widgets-toolbox\\widgets\\demos.xml","HelpLocation":"3ptoolbox/widgetsappdesignerandadvancedappcomponentstoolbox/examples"}]} \ No newline at end of file +{"Name":"Widgets (App Designer and Advanced App Components) Toolbox","ShortName":"widgets__app_designer_and_advanced_app_components__toolbox","ID":"3ptoolbox::widgets__app_designer_and_advanced_app_components__toolbox","ToolboxHelpLocations":[{"ContentType":"doc","LocationOnDisk":"C:\\AI\\widgets-toolbox\\widgets\\doc","AbsolutePath":"C:\\AI\\widgets-toolbox\\widgets\\info.xml","HelpLocation":"3ptoolbox/widgetsappdesignerandadvancedappcomponentstoolbox/doc"},{"ContentType":"examples","LocationOnDisk":"C:\\AI\\widgets-toolbox\\widgets","AbsolutePath":"C:\\AI\\widgets-toolbox\\widgets\\demos.xml","HelpLocation":"3ptoolbox/widgetsappdesignerandadvancedappcomponentstoolbox/examples"}]} \ No newline at end of file diff --git a/widgets/doc/helpsearch-v4_en/store/000000000002.zap b/widgets/doc/helpsearch-v4_en/store/000000000002.zap index 8915a683..5d889ae2 100644 Binary files a/widgets/doc/helpsearch-v4_en/store/000000000002.zap and b/widgets/doc/helpsearch-v4_en/store/000000000002.zap differ diff --git a/widgets/icons/down_grey_12.png b/widgets/icons/down_grey_12.png new file mode 100644 index 00000000..68f2ad12 Binary files /dev/null and b/widgets/icons/down_grey_12.png differ diff --git a/widgets/icons/up_grey_12.png b/widgets/icons/up_grey_12.png new file mode 100644 index 00000000..d6feb7d3 Binary files /dev/null and b/widgets/icons/up_grey_12.png differ diff --git a/widgets/resources/appDesigner.json b/widgets/resources/appDesigner.json index b97b1ab8..5931f278 100644 --- a/widgets/resources/appDesigner.json +++ b/widgets/resources/appDesigner.json @@ -10,6 +10,7 @@ "authorEmail": "", "version": "3.0", "avatar": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAAAeCAIAAABVOSykAAAAB3RJTUUH5gIDEgIatEpCVwAABGhJREFUaIHtml9MW1Ucx+89veWf/CmUFi9QunJpibD+IcAGTpxrZoydxi24h8WZ6F40xif1xScf9cUYo1HjiyZq9jDNZnRNNKYmg4XqiJRLiVJ2gfJnF2gvLeNfb+895/jQBhDjxu69gyvZ96VtTvs7n3zPL+ee3++UTKfTxAPtTmC/Af5PorZ/iMfjHMcJgoAQ2ksIAIDZbGYYxm637xjSFdKWWZFIJJFI+Hy+uro6g8Gwl2QQwrm5uUgkkkqlfD6fbpHI3J4Vj8djsVggECBJEmO8l1h5DpLEGAeDQZfLlVtMHSLlM4vjOK/XSxDEXbEkSVI8vdFo/K+h3LxerzcajebIdIiU3+AFQaBpWvGUWommaUEQcu91iJTPLIQQAGqfjEOjHIFR22Gn4ggAgM2NXA0Sn1y+HolhKGNZwlDCsowQPH6svY6uUYNE3fmru1f81uJXl39BklhaUuRstGkVVpn4ZHr01rLDYc9kxA0xu4GyEyNDjMOmwKzt0uacdXt14+LV/gJTdZGZ/ua7YHJp/w+6lZUmB+NoYJi6xia60VlaaVYfUwOzREn+8drQwkq22+/39hxfWJEvXw2trW+oj6w3qTULIjTAcuzUYlfPsYLCQmNhYXNXz+CfU6G+32VZ1gRRP1Jr1vhM8tfhyVZPa83DVpIkCYKssNRYnO5gKByJjmmCqB+pMmtpZeNiaNhmt7mcjs0TNjBQ1sZmg6Xhsy8vzczyWkBqLISQLEMFP1T+NMzK8JMfBsuqKrs73aKMs3DrZGigjPWeo+w8/+4Hn7/3zhvl5WWKZ7knXQnf7B+ZkjLr4vpqS4trx+j7H38BAEkQ+MknHr1wvldBfOVmBQcn02uiJKQ++vTrlhZXW/tWTcfd6JsdDgNALhH40vc/XTjfS5Kk4ol2r0CHIzrBF1lNT584Isp4LbO1fkxnj7Wlg/35igmvnH3uKWU8ys063dV0uquJIIg//or3jyd2jL75+stH290EQSCEIEQUtRdlcAFleO3Zjg+/7R+4MeI+3Lx9CMrSLPtbiXz77bdeUZzp972fBQDYG6dyqiorPuf3zsRnYuOTEOY3JgTlxYkxmJh+9aWztnrlJdQBbP45bdUnvI5RdnRhfhFjTBB4ObGQGB8J+Lt8/0y3e9UBNMsAQLeH8RyyhvuuZ0VREsWxcF/HI4f8PUcoSlV5p1ltqCsVGqlnHm+bnpsfCIUIhGrKqDOn/A+VFKsMq01mpVLpSW5ymuPmJm7yE+OrKUGTsGpUXlp87tRj2XQyI/Av9Aaqq0zqY2qQWXS1qbW2Aq8ni2SpAkkYyA1ue32tVX1klbLXWl88c5LASKsuSN4sAACEUFmfm66ueP5kpyY0EMLNHpYapE21tTJaIuVezGYzz+9/acLzvNmc76XoECmfWQzDsCxL0/RdV/IOTWuVghCyLOtyuXSLRG7eSOfunTwez274NGfieZ5lWYvF8u+rMP0gkduv73V1o6lDJPLBfx12rwN4gr9/+huM30JqHa4lcgAAAABJRU5ErkJggg==", + "avatarDark": "", "defaultPosition": [ 1, 1, @@ -27,6 +28,7 @@ "authorEmail": "", "version": "3.1", "avatar": "data:image/png;base64,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", + "avatarDark": "", "defaultPosition": [ 1, 1, @@ -44,6 +46,7 @@ "authorEmail": "", "version": "3.0", "avatar": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAAAZCAIAAABIPBwcAAAAB3RJTUUH5gIDEgMWpOc/PQAAAtFJREFUWIXtmb9r8kAYxy+aIEqXErRDhojw1oJDSlonERzdXpxC0N2xf0EG0X/BwU4dKsWpdBTegiBOVZvXyXQIhpJCEVGKKAGbvMNB7FtTEj2LP+hn8bl438e7r8+dyYmNRiPwgzNcmx7ALoHDl0qlstlxrBeO48xYUZS23NYGGvaOOc9guA0P6WFDLE3T5kXcjBKJxDrGuXlqtZoZP/x9aPVb9+f3faqv47rzJK6Zy6/6X5uvZ6OzKBOFF+dmHR8fr2m0G8Y0S1GUVr919fvqBX8Zg7EOljELdx3QB8/UM7gDASUA6wu3le0ubbldPa8+4U9L2QTRgf4G3sb4uHpePXo8gmbt8wavDbQO1VnBKRMd6B2qow002Nxns7B3bIgPEZMM8aH5y2BtVqPRwDAsmUwOBgMAQD6fxzDs9PRUkiTL/pIk8TwPO3/ikxY2y+Uy4hwcglJWixm+rKxsNnt7e0uSZLlcVlV1MpkUi8WLi4tFRyRJ4jhuOLT4Dhe1giBcX18jTmBT2C9DWZbj8bjX6z05OQEAdLvdj+82Gg2O43K53OHh4bLancPGrOl0qqpqMBgEAPh8vmAw2Ov1PnaIxWKiKEIvltXuHDZmTSaTlWeIot1ObMyCFbFaahTtdmJjltfrpSgKFgisFOfzR9FuJ/YbfCgUqtfr0+kUbs+W29N3aLcQ+8eddDoty7LP52MYplKpkCTpPDuKdgtxdAcvCIJhGKIohsNhyw7hcPjm5sbSC1vtDvGlWaVSKZVKWd6Uo5DP5zOZzHpzfoXhNogZgZiEmBGG24Cx9TKMxWKGYSB+jCWCIAiC8B2ZF/GQnogaEWkRJUlEjXhID4z3+UGaDbF8k0cpLmJG8E2eDbGwuc9m0TTN+tnCXYFRmGUtI2YEozCFuwLrn58sz5fh5eXlOke6HUSZaEAJBB4D2p+VzuB//XcGj/38FeacfV6Ga+cfRrtOIunfglEAAAAASUVORK5CYII=", + "avatarDark": "", "defaultPosition": [ 1, 1, @@ -61,6 +64,7 @@ "authorEmail": "", "version": "3.0", "avatar": "data:image/png;base64,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", + "avatarDark": "", "defaultPosition": [ 1, 1, @@ -68,6 +72,42 @@ 25 ] }, + { + "className": "wt.DateSlider", + "componentName": "Date Slider (R2023b+)", + "description": "A date selection control with a date picker and slider.", + "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAANNSURBVEhLrZa5SyxBEMZr1vVYbxFvTMQj8B8wEET8DwQVDcTM2MTQQDARxEAwNVcE0cTNTRQRRQ1k1wu8xfsWdd78yu1xnJ334KEf9G7bXf1V1VfVvVoSgPf3d3tzc5NvKSsrk9LS0kA7sLOzY9/e3kp2drZUV1cn2YUS3y5sB9fX13J+fi43NzdyeHgoHx8fdmL7G7CNxWJqd3R0JI6jJLskB+D19VUuLy/l6upK7u7uxLL+moBrR1Bk7Ed4dXXVfnp6UhIM4vG45OTkaFSpqakSiUSUZGFhwQ6Hw3rICVzS0tJ0TgBvb28SCoWEfWOHDWdDx8fHSkYEDJyRwcXFhRKzBoGJksEc3cHp6anaYQ+pOQMn3CoRGwzARklJCbrrvLKyUp6fn10bY/f4+KijtrZWHebn5+u6386am5v7VhhkgrSmpkYdpKSkiCNjkr4QIEVjY6M6QNaNjQ2dI5eBNTMzow6MRwAZGkNO9NQnqNBkCRm1QkbOGXJjH2KRTaJhQGoKyDqHzbp/sMe3ITa2DOPQmpqasomyvb3dJf4paJLJyUnJyMj4yoB0fwtwuZLxwQjCw8ODHBwcaCt6a2RwdnYmJycngecNb5gPvPkRjUZleXlZ+5pU6ayuri7Jzc3VuzI7Oyvr6+v6d29vr+Tl5SVOfiIpA2+E+/v7Mj09resdHR1SUVGhLei0tEbd19cni4uLqjVy+DsMLsMbKFFxcbEMDQ1JW1ub9jedkp6eLlVVVZKZmSk9PT3S2dmpDoLaF3xz4JeIN6SoqEijHR8fl62tLX226+vr9VluaGiQgoKCfzaGKxFGfkMKu7Kyord5bGxMmpqadG1iYiJh8QUyIEM/DK9bZG8NlpaWtIh1dXXS3Nws29vbug4Zjxx7FJp7c39/r/Uiw5aWFr1wcLkZmIkXra2tasxjxmFeSvTv7u7WH6K1tTX9IeKB4/YiIUF4gzSBWyMjIzb93t/fr4X0gih5vFgvLCzUNUgorvdBY43scAZeXl5keHhYsrKyPosM0NgPil1eXu6SA4hwCJkZSGXIgeFSiSJO24H5+ajs7u7p/CeAAy4Atzbx4OCg7a2DV8v/gfdO0FkDAwNfK6Ojo/rvx2+A58O57Q63yB/WIHzYbbhnVwAAAABJRU5ErkJggg==", + "category": "Widgets Toolbox", + "authorName": "Robyn Jackey - MathWorks Consulting", + "authorEmail": "", + "version": "1.0", + "avatar": "data:image/png;base64,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", + "avatarDark": "", + "defaultPosition": [ + 1, + 1, + 600, + 40 + ] + }, + { + "className": "wt.DateRangeSlider", + "componentName": "Date Range Slider (R2024b+)", + "description": "A date range selection control with date pickers and a range slider.", + "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAACCUlEQVRIibVWz0sbQRT+ss0taf6BYKHQP8BTC4VevCpIIbT2sqJRijcRPImgSKGYHiIlJSUoRqGEeqwSsT14FdRAMCZ6Uf+BtpuckmVmZWY76Uyy7o6RDDx2ePPe9828HzMb+pzJbgIYR59GmIFPJycC0anjgBICSmlbiJiTTp1rV9w/gKGz717AmbARSOAFPr+cwtFp2RecrQUS3LXz9roPuLALC+NC4RssywIhBIS4Du6ctHViDjziPqurHxUb5uM4QCwWw2RyWiUwzXGEQiGdlGBqdoF/0+m057pt26jWLl0Ch1ECaLVs37DICRXj6vqmKyyyH8MOyw661bI4N+MZc+ohhiDQAS9Valj6lNECJ0Qi0N15Z/UEgTMJu4YEOzvfUa/7V9GLV0Oc4Gv2S1dlyVUUjT7G27F3EgGlME0ThuHfd3s/D/+VZ8rXrtls4vikpBJYVj2w/cU4r174hIUoITZE7HXulmdP4ph7b2qDM2xDF9wvoeVqDWu5LRW8fYIHgsuhU2yIUkUUxeIuGo2G1l30f83VDT5/yQm28xu8iiKRCIZHRtUkJxJveBWx9uYCB8za4R9Z5zYlJN2P/V+cYGXlA9c1Wy1+FykEv//8vVdY5ISKcVapduXB6CXmndXydCCOybHX3klmR3oIuJxQ1Y91teO+aP0AF08me3Dyudx6f35bgPwt/Fq7n0cYWXkAAAAASUVORK5CYII=", + "category": "Widgets Toolbox", + "authorName": "Robyn Jackey - MathWorks Consulting", + "authorEmail": "", + "version": "1.0", + "avatar": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAACCUlEQVRIibVWz0sbQRT+ss0taf6BYKHQP8BTC4VevCpIIbT2sqJRijcRPImgSKGYHiIlJSUoRqGEeqwSsT14FdRAMCZ6Uf+BtpuckmVmZWY76Uyy7o6RDDx2ePPe9828HzMb+pzJbgIYR59GmIFPJycC0anjgBICSmlbiJiTTp1rV9w/gKGz717AmbARSOAFPr+cwtFp2RecrQUS3LXz9roPuLALC+NC4RssywIhBIS4Du6ctHViDjziPqurHxUb5uM4QCwWw2RyWiUwzXGEQiGdlGBqdoF/0+m057pt26jWLl0Ch1ECaLVs37DICRXj6vqmKyyyH8MOyw661bI4N+MZc+ohhiDQAS9Valj6lNECJ0Qi0N15Z/UEgTMJu4YEOzvfUa/7V9GLV0Oc4Gv2S1dlyVUUjT7G27F3EgGlME0ThuHfd3s/D/+VZ8rXrtls4vikpBJYVj2w/cU4r174hIUoITZE7HXulmdP4ph7b2qDM2xDF9wvoeVqDWu5LRW8fYIHgsuhU2yIUkUUxeIuGo2G1l30f83VDT5/yQm28xu8iiKRCIZHRtUkJxJveBWx9uYCB8za4R9Z5zYlJN2P/V+cYGXlA9c1Wy1+FykEv//8vVdY5ISKcVapduXB6CXmndXydCCOybHX3klmR3oIuJxQ1Y91teO+aP0AF08me3Dyudx6f35bgPwt/Fq7n0cYWXkAAAAASUVORK5CYII=", + "avatarDark": "", + "defaultPosition": [ + 1, + 1, + 600, + 40 + ] + }, { "className": "wt.FileSelector", "componentName": "File Selector", @@ -78,6 +118,7 @@ "authorEmail": "", "version": "3.0", "avatar": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAAAZCAIAAAD7WPZDAAAAB3RJTUUH5gIDEgQQAsUMzwAAA05JREFUaIHt209vG0UUAPA3s8PacRuqdO3ajiOMsmoUKQdbFXBAPeTAKReO4ciJA2e+BN+BAzeIBFIlpEiuhJpD4dCT8SlF3VCXNTbdLuviP+2ud97jsGipyjRqS6Ko9P1kybOjfU9zeJoZe2fFeDwGxk6aPOsBsP8nlX3t7e2d7TjYWdnd3c3b/X7f87wwDBHx+TNIKR3HcV232WzmnSpvbW9vn8Q42avk4OAgb3e73SAI2u12o9GwLOv5k2itB4NBt9uNoqjdbmed/xTWxsbGCY2WvTLywur3+0EQ7OzsCCGIKE3TF8rTaDRWV1f39/f7/X42b/EeiwEAeJ7XarUAgIheIjyLarVanudlPVxYDAAgDMN6vf4fk9Tr9TAMs7Y6/lb2mkBEKZ+eZYhoNBr9+2YiIiIhRK1WezJKSpnv+rmwmIFePErjGRIGvx1tbW0BoL20IpUthACAKIoODw+JqFqtPisDL4XMIPJ/Gt2+Ho/vNquFSfDzr73vbt/8wpJCKaWUKpfLm5ubQggiJDL/McGFxQzs0opSxZr7fu3y1frlq9X19+4f/UD4909FIUS5XF6tVW59/Wlw9KMxAy+FzKBw/tLR4Nv1d0kKAUDFZScl6/svPxFCAhAAodZ25R1VXNHaPGNxYTEDe+nNBw9G8fxhaXkFKC0tX/jg489JTwCngA8Jx2kc3Lg+UIsE1JIxAxcWMxDSKl1cj+7/Ujp/jmgBkFgWggBAJETQGmWq9CAaz0iYS4j3WMzs0ttXoj98ojlkH5wBzghnQDOiOdCCJE5nsXqjZAznwmJmtbe2hoM7pP8EnICeAE4JJ0BTwCnpuU7xcRzf9YckzE8VeSlkZnahNJ8/0kmkLAJ6TDQHmgFOUU90EqcxEYl5QkvnLhjDubCYmWWp8SS59s1XUgJQCpACJUQJ4QIXmC7o3r3f41QKyTMWezYppdb6ydMy0lIffvRZukiOi7JUoVDML7XW+RMeLiwGAOA4znA4XFtby3uEELZdtO3iMVFPGQ6HjuNkbd68MwAA13V7vZ7W+qUzaK17vZ7rutklFxYDAGg2m5VKpdPp+L7/ouWltfZ9v9PpVCqV/HSyyN7S4TPvr61TOvMu+PUvdhp4KWSn4i9YBJAJLULm+gAAAABJRU5ErkJggg==", + "avatarDark": "", "defaultPosition": [ 1, 1, @@ -95,6 +136,7 @@ "authorEmail": "", "version": "3.1", "avatar": "data:image/png;base64,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", + "avatarDark": "", "defaultPosition": [ 1, 1, @@ -112,6 +154,7 @@ "authorEmail": "", "version": "3.1", "avatar": "data:image/png;base64,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", + "avatarDark": "", "defaultPosition": [ 1, 1, @@ -129,6 +172,7 @@ "authorEmail": "", "version": "3.1", "avatar": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAAAZCAIAAABIPBwcAAAAB3RJTUUH5gIDEgU2ydO4cwAAAJRJREFUWIXt2bEJwzAURVE7eJr3Kk2nibSWZtAIKRwSQ4r4VVLgnsYCN5/Lx4W8jzE23POYPcA/Oc5HKWXuHCuT1Frb2KyfJNk+z8f1Re99xjxLq7W+z2xWgFgBYgWIFSBWgFgBYgWIFSBWgFgBYgWIFSBWgFgBYgWIFfhc/tm+XnTh22uzbEuaO8r6dn6F3cc3K/AE7UoQuvgib+gAAAAASUVORK5CYII=", + "avatarDark": "", "defaultPosition": [ 1, 1, @@ -146,6 +190,7 @@ "authorEmail": "", "version": "3.0", "avatar": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAAAeCAIAAADmXcb7AAAAB3RJTUUH5QwOExwLwLcjXwAAAGxJREFUeJzt0kENACEQwMDj/ItdCZigISEzCvrompkPTvtvB/AmY5EwFgljkTAWCWORMBYJY5EwFgljkTAWCWORMBYJY5EwFgljkTAWCWORMBYJY5EwFgljkTAWCWORMBYJY5EwFgljkTAWiQ2jJAMMhsTjyAAAAABJRU5ErkJggg==", + "avatarDark": "", "defaultPosition": [ 1, 1, @@ -163,6 +208,7 @@ "authorEmail": "", "version": "3.1", "avatar": "data:image/png;base64,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", + "avatarDark": "", "defaultPosition": [ 1, 1, @@ -180,6 +226,7 @@ "authorEmail": "", "version": "3.1", "avatar": "data:image/png;base64,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", + "avatarDark": "", "defaultPosition": [ 1, 1, @@ -197,6 +244,7 @@ "authorEmail": "", "version": "3.1", "avatar": "data:image/png;base64,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", + "avatarDark": "", "defaultPosition": [ 1, 1, @@ -214,6 +262,7 @@ "authorEmail": "", "version": "3.1", "avatar": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAfQAAABaCAIAAADB1TP3AAAAB3RJTUUH5gIDEgYy5ZMvqQAAAVpJREFUeJzt1FEJACAUwEC1f9gXwRKCMO4S7Gt7ZhYALed3AADvmTtAkLkDBJk7QJC5AwSZO0CQuQMEmTtAkLkDBJk7QJC5AwSZO0CQuQMEmTtAkLkDBJk7QJC5AwSZO0CQuQMEmTtAkLkDBJk7QJC5AwSZO0CQuQMEmTtAkLkDBJk7QJC5AwSZO0CQuQMEmTtAkLkDBJk7QJC5AwSZO0CQuQMEmTtAkLkDBJk7QJC5AwSZO0CQuQMEmTtAkLkDBJk7QJC5AwSZO0CQuQMEmTtAkLkDBJk7QJC5AwSZO0CQuQMEmTtAkLkDBJk7QJC5AwSZO0CQuQMEmTtAkLkDBJk7QJC5AwSZO0CQuQMEmTtAkLkDBJk7QJC5AwSZO0CQuQMEmTtAkLkDBJk7QJC5AwSZO0CQuQMEmTtAkLkDBJk7QJC5AwSZO0CQuQMEmTtAkLkDBJk7QJC5AwRdFT4DhAhzaNMAAAAASUVORK5CYII=", + "avatarDark": "", "defaultPosition": [ 1, 1, @@ -231,6 +280,7 @@ "authorEmail": "", "version": "1.0", "avatar": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAAGQCAIAAAAP3aGbAAAAB3RJTUUH6QIHFSACe++vJgAABUVJREFUeJzt1EENAzEQwMBtdYADIRACuRCuv8jSDAK//Nl7D0DBMzNrrdsZAC/OOd/bDQD/Miwgw7CADMMCMgwLyDAsIMOwgAzDAjIMC8gwLCDDsIAMwwIyDAvIMCwgw7CADMMCMgwLyDAsIMOwgAzDAjIMC8gwLCDDsIAMwwIyDAvIMCwgw7CADMMCMgwLyDAsIMOwgAzDAjIMC8gwLCDDsIAMwwIyDAvIMCwgw7CADMMCMgwLyDAsIMOwgAzDAjIMC8gwLCDDsIAMwwIyDAvIMCwgw7CADMMCMgwLyDAsIMOwgAzDAjIMC8gwLCDDsIAMwwIyDAvIMCwgw7CADMMCMgwLyDAsIMOwgAzDAjIMC8gwLCDDsIAMwwIyDAvIMCwgw7CADMMCMgwLyDAsIMOwgAzDAjIMC8gwLCDDsIAMwwIyDAvIMCwgw7CADMMCMgwLyDAsIMOwgAzDAjIMC8gwLCDDsIAMwwIyDAvIMCwgw7CADMMCMgwLyDAsIMOwgAzDAjIMC8gwLCDDsIAMwwIyDAvIMCwgw7CADMMCMgwLyDAsIMOwgAzDAjIMC8gwLCDDsIAMwwIyDAvIMCwgw7CADMMCMgwLyDAsIMOwgAzDAjIMC8gwLCDDsIAMwwIyDAvIMCwgw7CADMMCMgwLyDAsIMOwgAzDAjIMC8gwLCDDsIAMwwIyDAvIMCwgw7CADMMCMgwLyDAsIMOwgAzDAjIMC8gwLCDDsIAMwwIyDAvIMCwgw7CADMMCMgwLyDAsIMOwgAzDAjIMC8gwLCDDsIAMwwIyDAvIMCwgw7CADMMCMgwLyDAsIMOwgAzDAjIMC8gwLCDDsIAMwwIyDAvIMCwgw7CADMMCMgwLyDAsIMOwgAzDAjIMC8gwLCDDsIAMwwIyDAvIMCwgw7CADMMCMgwLyDAsIMOwgAzDAjIMC8gwLCDDsIAMwwIyDAvIMCwgw7CADMMCMgwLyDAsIMOwgAzDAjIMC8gwLCDDsIAMwwIyDAvIMCwgw7CADMMCMgwLyDAsIMOwgAzDAjIMC8gwLCDDsIAMwwIyDAvIMCwgw7CADMMCMgwLyDAsIMOwgAzDAjIMC8gwLCDDsIAMwwIyDAvIMCwgw7CADMMCMgwLyDAsIMOwgAzDAjIMC8gwLCDDsIAMwwIyDAvIMCwgw7CADMMCMgwLyDAsIMOwgAzDAjIMC8gwLCDDsIAMwwIyDAvIMCwgw7CADMMCMgwLyDAsIMOwgAzDAjIMC8gwLCDDsIAMwwIyDAvIMCwgw7CADMMCMgwLyDAsIMOwgAzDAjIMC8gwLCDDsIAMwwIyDAvIMCwgw7CADMMCMgwLyDAsIMOwgAzDAjIMC8gwLCDDsIAMwwIyDAvIMCwgw7CADMMCMgwLyDAsIMOwgAzDAjIMC8gwLCDDsIAMwwIyDAvIMCwgw7CADMMCMgwLyDAsIMOwgAzDAjIMC8gwLCDDsIAMwwIyDAvIMCwgw7CADMMCMgwLyDAsIMOwgAzDAjIMC8gwLCDDsIAMwwIyDAvIMCwgw7CADMMCMgwLyDAsIMOwgAzDAjIMC8gwLCDDsIAMwwIyDAvIMCwgw7CADMMCMgwLyDAsIMOwgAzDAjIMC8gwLCDDsIAMwwIyDAvIMCwgw7CADMMCMgwLyDAsIMOwgAzDAjIMC8gwLCDDsIAMwwIyDAvIMCwgw7CADMMCMgwLyDAsIMOwgAzDAjIMC8gwLCDDsIAMwwIyDAvIMCwgw7CADMMCMgwLyDAsIMOwgAzDAjIMC8gwLCDDsIAMwwIyDAvIeGbmnHM7A+DdDyBFCVREf0ePAAAAAElFTkSuQmCC", + "avatarDark": "", "defaultPosition": [ 1, 1, @@ -248,6 +298,7 @@ "authorEmail": "", "version": "1.0", "avatar": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAPoAAAAeCAIAAACKW5T+AAAAB3RJTUUH6QIHFRoTNPZRrQAADHhJREFUeJztnG1QVFeax597b7/QDU1j327eQewRYVXQIPElyLyYmBCTyZrJTg3aJLtVU9mkarJj+WFqqyYbNovrVmZGK+tWjYmTycwmQsiaTKKJMYyJbwGVjA4KQxBUOkJABPo20E3f233vPefZD9f0ELAZaCAppX/VH04dzvmfA/fPc8997unDDA8PA0BXV5fb7RYEgRACMW4fOI7jed7pdC5cuHDcj2blmt5h+szw8HBzc7PH4ykqKsrMzNTpdFFLx/hGUFW1qakpGAyuWLEiXHnx4kWPx1NYWJiRkTGTa6qqam9vb0tLi91uX7ly5e2uzzQ3N1+9evWRRx6JGf225vjx43a7XYthXV1dV65cKSsrY1kWEWeozDAMpbSuri43N/d212fdbndRUVHM67c7a9eudbvdWtntdhcUFDAMM3OvAAAiMgxTUFAwi/ryn674f/UOSvIc6Ueav04QhMzMzJmLxvhmMZvNgiBoZUEQ0tPTJ2l87Nebbll/70+OROqSnp5eX18/Rf3Jkf90Raw9parE+5+1mT//ERcfN4k+09nKXKyHES9kLaZL74YMZ3SDavo6QkgstN8ZhB/pCCEsy07e+Ps/OwkoAUqAIlIJUDy8Z9sk7VmWnZZ+JP7qdZ/f29Pbve1/1v73v+gTTJH0mT+f5H63A4Iizc6Hh/+RbvgH1p4axbiafpSTjnFHQJH6UR1A9XOUP/saxpPOtIi1pzDeyHwrxevzeUf83Rcvv7313yN26LnK9HaCJCIC09UOB18ZPX4oeKM36gnE7D6f0QFwwLAA7NfgBE/bibamXwYTg/F3L7EuyXIU5Q35/F6f/+9c90fqwiTaqCNTtdgAABhgeq8Z696gJw+FIjgehwZo41H6/u+Zyxdv2SC2jJnPYITy7ONpO+H+6NeUYovx6F3BbAt1tH/a4vX5S3c8ufJH90WcX6KNFq7Dnk7aeJQdGmAA4zovSEchxOm5DX+v45O/0jgk4bkT6rE/0KFBQC5uycqJgjG7z2NQAVABVQACMIevFzWvmxMs2XnfOnL42FunXrD3FPZfHprc6xps4T1kQbKPNRrPHDEP9VEAw9WL7DsjIoCxdJMxNeNmO5+XvPtbevT/iNAv3bd1yJrJXR/OTEnUcV+5a42/hQmCsGXLlo6ODgDYsWNHTU3NrPzCp0+fZr5ktjRjRI2sEEAFUQIMAsqAMqAqK3Pi+LDXC9eucqQ4vrfx29cHB8/gx6uf3fI3va7BZS02PlShbHrC51xJkWEZ5K67dXXV5NShUP91AICuDvXAXjxSrQRG/cvWX1+8dnRBBsPc4n71dUT3mpqa/fv3ezwenuclSdq+fbvb7X7uuee+hqFjHPxVBEuhDzAAVPzS9MGQQm7ZePPPPo569LFe1xv0wVDoD4c+EnyB8p/uWvPQ5qnrxC1bRWypkoU3vOeD658jgPHqhSDDBoHTrbsXThxU3vkNFQOjd5cN3+caSclN4+0LEk0cy4zTiWj3mpqayspKrexyuU6fPr1+/XoAeOqpp1588UUA2L59e1FR0d69e5ubm6urq91ud2Vl5QMPPFBTU8PzfFhHEITDhw/v2bNHqzSZTDt37nzmmWe0G8jzzz9vtVr37dsHAA0NDSUlJQAwbiyTyTT1v8sdxo+feOLNCTfDcpfr1ddfn6KCrNJ/+o8PgdED6ABYYBhACqAi6QfqBxwNm37Lv+5EoiJRkChAFCQKEvmNl16KevITvf7Sb978S8e18p/u+s5DW6erFp+WYdj4qMSx7LEDhvbzDIDe3crV7lZOvs30daGijHxvy8CKe8WUJWmOpGRbfILJMFEk4vO4y+Wqqqqqrq52uVwdHR1VVVXt7e2iKALArl27tDZNTU1nz55taGioqKhwOp2iKObk5NTV1Y3VaW9vt1qt2dnZ4Rqz2Wy1Ws+fPw8Aly5dKi0tRcSqqqr9+/dLkhRprPnJL3bvtjscY2vsDscvdu+euoKsEFQ6UelGtQfJdVR7Ue1BtQvIENBhpD6gfsAAoEj8R8nIB6r3oDr4ttxfK19/PfTF76Je4cyu1zX09hRj6YO4+cngdx5TEmxAFGaoHz87pwRG/VnLBlfcqy5ekergk22J8XH6WypMKf10/vz5RYsWZWdnm0ymxx9/vLe3V5IkACgtLTWZTHa7vby8vKyszGQyZWRk/E21sc1SUlKKi4sBYMOGDZOPNT+ZaO6J/wCTE1IIUi9QAagAxAPEA9QDxIvUi3QY6AhQH1IfEAmJjERGoiDVQruKRA1FZfe58LqGMS1L993N6sZyTOIJACIwgCQuPpS6MODIdvD2ZJs5wWRgmPHLGI2pZlv37dtnNpsZhlm/fv21a9c0C+bk5Mxk6gBgs9nsdvvkY2lhft5S7nKVu1wTy1NEVggQLxIBieevH+oB6v0ywI8ADVA1hKqMqjx2MQNUiSK6z53XNYwMtXCEZQAQAQARdMODpu42m/9GnAETTMZJ+k7V7lVVVfgldXV1Npttih3z8/NHRka6u7vDNYIgtLW1aUF9KmONfRKYn2gRfbrLGA1ZJUC9QLW4Pgh0EKgHiABEQOpFOoTET5UgqiFUQ6jKoPn+5tpdldXp2X2uvY7CDTj5Lhx+jfP0EXOi5FgomyyUAeNAt/VYjbGlgU66OX5Kdi8uLm5oaAhnJ59++umpLzB4nn/44Ye3bdumbWCSJOnZZ59dunRpXl7erI91p6IZfbrLGA1ZIUgEJAJSz1cDvABkCJVRVIKoSKgEUQ2iGqKqjDcdLysymVZ0n3Ove/tp40fqwd+qDUdUMRDIXjZ0132BpWsVRxYjBeI//ZA5/pb4l3NEDERSmCwR6XQ6KyoqAMDlclVWVubn5wOAlnuZVrbE5XLl5OSEFy3a42+kxnl5eTMZ605lumuYMCGFPre9emJ91S83I1WRqEAUJDKqctXOT2cywzn3en8POXWIHnoVv+hEYAILlw8UPegv3mjjCD1WY6qr1odGsf59RRiAf66MW1as09/C2+OreJ6vra3Vyi6XK+zLkpKScZuPX375Za2Ql5cX7hIpmz6x+7iOJSUlWhYyUuMY0fFvey9MrKx8spDKIlBy86lUVZDIskKqXmmJbpS59jr97Bw9+0dytg6/6CTABHIKBtY9Glh+jyMlPc1uQVMF8vbgiYPs55e41rPyWy+Bf6txzQa9YXx+JraJYD4iqwRlEamKlIRT7NNdpoeZ87je00mO7KenP6TeAQAQFy4fWlU2snpTcnKK2QjxcSyTv1xelCsih+xBtrOFfvK+jIAJVnbpSs4YN1YqtiNyPhJSCJUDKIsoi1QWUZZQlqLLOd7o6pjbuN76qVq7h56uQ+8AAhNKSvXcdb+n9IcWa2JSgiHdYdVyjgajMbniJ8ZHfxxctJwBpPXvhV79L6n/xji1mN3nI7JCqCxqH1QkqohUkaJ7o3Ty7MVP+vnFxXfPSVz33KBNn5BT76O3HwBCSckD9/zAX/Bt3mZP461Jlrix+XXWYIi/537zD5+WizcqjI42N4jvvCJe+spO4NhiZj4iKwRDAUQKlCBVgaiURJNiB4CPjx1LdyRc8vFL6OBrv59NrwMA9ndjTyf6hhAYOSnVl79GWPdIfMYig57yVjM7YUuM3pGa+NDWoMmKHMf8+UTovddEs4XNWBSXaNUaxOw+H5EVSmURkCIllBCiUlWhskKnq3PlypVLbW35m0reOnSq+3KrJTSbXgcAhk9Fe7pqWRCy2D2ryoZWb0KrY4ElLpW3TPS6Bssyyd8tG02yjtpSdH98Y/Ryq3D+XN6Gm1vfdBzHqaoa+7rqHQDHceHC5Nc0pJKKn1+elriqqhP1Dxw4MDAwsO9/301NTS4re/D7m3YV3rU6usnfUp9Z4GDXPYDWlJEQihn51uzFFrPelmjS67hJpFidzrx0FeiNclqWbOKHGVNYX8fzfE9Pz8y3A8T4ZhFFMfz6mef5vr6+rKysSI3f/PCL6er39fVN1D9z5sxjj/3A5XIVFBREN+3J9cFo0hWssSxf09/j1YVCBh2k2BIMt0qoj0NnNieuWC0vXe0bGKLekbC+zul0NjU1xc4Pu91pbGx0Om8eSuF0OltbW9PS0mbrmqqq2trampubO07/gw8+mFN9bf4MA7lZU920MhaDHpwZC5wZC8L63AsvvOD3+y9cuGAymRISEqI+UCHGN4UoivX19TqdLrwvIykpyefztba2xsXFxcfHz+SaaofONTY22u127VX3ba3PxI5Eva25w44snWv9/weOf2O+kVWvIAAAAABJRU5ErkJggg==", + "avatarDark": "", "defaultPosition": [ 1, 1, @@ -265,6 +316,7 @@ "authorEmail": "", "version": "1.0", "avatar": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAAAZCAIAAABIPBwcAAAAB3RJTUUH6QIHFCsOkG9Q8QAAAeBJREFUWIXtmDGr6jAYhr9eXFoQkbi5uKWjQzdxd3UKpdb/4FwUpZuLqwpODtJ/oHsLDg4dzaR0bhAXM/YOcuBS5UCsEXvpszUN35s+CUmJslgsQA6EkH8fPc/Le1AJAEzTfHvdzWbz2Jj3oBIAlMvlt5d+St6D/sgo+r9SyBKgkCVAIUuAQpYAWWVRSk3TZIy9ZTRPCYJA+SEIAnlBlNJms6koSqfTefpFmWRRSgkhl8slS5HfYYyt1+s4jpMk8X3fdV1JE8M5X61WnuclSWLbtuM4nPNUn9dlBUFACJlMJtVqNds4fwMhNJ/PEUIAoOs6AByPRxlBqqpOp1OMMQAYhnG9Xm+3W6pP6eXqrVYrDENKaaYxinBfX7VaTXbQ4XCoVCqapqXaX5f1YTjns9ms3+/fJ18SjDHLsna7ne/7qqqm3ubjNOScDwaDer1uWZbUIITQdruN49h13cfDJAeyGGPdbrfdbg+Hw88kaprWaDTO53Oq/dtlcc4dx7FtW/aaYoyZpnnfgqMoOp1OhmGk+ny7rCiK9vt9r9eT/auFEBqPx4QQRVF0XR+NRo+bY9YNHmP89EbpXWCMwzCUV18o69tX1ldRyBKgkCVAIUuAQpYAhSwBSgCwXC4/E5b3oL+EBb73WogB0AAAAABJRU5ErkJggg==", + "avatarDark": "", "defaultPosition": [ 1, 1, @@ -282,6 +334,7 @@ "authorEmail": "", "version": "1.0", "avatar": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAAAZCAIAAABIPBwcAAAAB3RJTUUH6QIHFCwjmvGaQwAAAWdJREFUWIXtmL+qgzAYRz8vLhGkiN26ZDNj9+JzBPEROvQZpE/QoYPuIj6HdHdstizdKqFLXDv0DhfslYaWTws5m3/gdziQQZ08z2FOcM7/XtZ1PZXJEBcAkiSZWuOXqqqGN2eiV1WVCwC+709tMsZ89H6mFvgmbCwDbCwDbCwDbCwDsGMJIdbrteM4+/0eefpFhBBJknRdN3yEGqvrut1udzwetdaXy6UsS8z1VxBCcM6VUk+fosa6Xq9BEDDGCCFxHDdN0/c9psA4p9OJc55lWRAET1/AjrVYLDzPAwBKqZRSa40pMM5ms2nbljH23wuosaSUmHMfBzUWpRRz7uOgxloul7fb7XH0pJSU0seR/BawYymlzudz3/dN08RxTAjBFHgT1FhhGB4Oh+1263nearVK0xRz/X1c5L0oitq2RR41Ioqip7/VwH7uGGFjGWBjGWBjGWBjGWBjGeACQFEUU2uMMR+9O+4bakIWLAJNAAAAAElFTkSuQmCC", + "avatarDark": "", "defaultPosition": [ 1, 1, @@ -299,6 +352,7 @@ "authorEmail": "", "version": "1.0", "avatar": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAADICAIAAADdvUsCAAAAB3RJTUUH6QIHFRkeYWp+0wAADtlJREFUeJzt3W9oVPeex/Hv75yZOTMTJ9MxsfnTuOnmbp4kpmSN9IFIqawPglS8IHlwaykruAjdwoJwUeqy0LJXdBcKK+5eZB8URHRLsGzo4i1bF5fWJxXDStW03ODcRk0nk0wyTiY5M+d3zu/33QfHpmkd7eRP+yv188IH8+fMjzOGd37nX2bE+fPnCQAeb/fu3Uu3JyYmstns7OysUmrVA9q23dTU1NXV1dnZSUQRItq7d+/aVxTWy7Vr11588UXTawEPjYyMLN2+ceNGoVB44YUXnnvuuUgksuoxgyCYnJz8/PPPi8Vif3//6gcCeKpMTEwUCoXBwUHLspg5CIK1jNbR0dHe3v7RRx9NTExY67WKAL9s2Wy2r69PCMHMax+NmYUQfX192WwWMyFAXWZnZ9vb25+wwP/86+6aj//V31563Eva29s//fRTRAhQF6WUZf3AluOe3/4vcYW4QuyyrhC7//Uvf/eE5S3LUkohQoD1pVmXSc2yLpAq1PMC7BMCrK8IkU3CIrLq7AsRAqwvfsztx/pOhOVy+dixY8eOHSuXy0t3cTbfoGPHjp06dcrzvPDu/fv333jjjZo/kbGxscHBwbGxsZ92BeER7BMFxAGRIqrrhH6NmXB0dPT69evrvWqwGr29vV9++eXMzEx4d3x8PJvN9vf3m10rqEn6ithnrhBXiSWxJA6k/8Md1j4wMzw83N3dnU6nlz94/vz5s2fPEtHAwMDRo0eJ6MSJE11dXYuLi5cuXRoYGHjllVfefvttIjpy5MjOnTuJaGxs7PDhw0svSaVSa36nT5f+/v6zZ8+Oj493dHQQUS6XGxgY2Lx585UrV06ePElEXV1db731Vvhs6P79+8ePHx8aGtq5c2f4///uu+/29PR4nnfmzJlLly7Rsh8QrM5//vOu2k/wPPEiafebFKuer2ou/OvfXl66XSPCrVu3Pnjw4JNPPtmzZ8/Sg2NjY4VCYWRkZGZm5vjx4x9++GH4bDabPXr06K5duw4fPtzS0jIyMnLx4sWwYSI6ffr0kSNHtm/ffubMmffee+/QoUOO46zt7T9dNm/ePDAwcPPmze3bt0spb9++3dvbWyqVPvvss+HhYSI6ceLEBx98cOjQoR8c6uLFi/l8fnh4+N69e+EPq6en58d/B79MMtB//fYfSESJIkQWCUGsiQJWedJl4oWlFH9z5HesAlY+K5+Uz8pnJc///vfLR6sR4ZYtW4jo6tWryzd7enp6enp6libDHTt2hI/39vamUqnGxsaurq6+vj7Hcdra2sKnxsfHiai7u9txnL6+vsuXL0spEeGKpFKp3t7eq1evzszMzM/Pj46O7t+/v6Oj4+jRo0uT4fLLix+nXC6HAadSqTDsfD6PCFdN+or9OySSJGIkIkRErIg80hXiBdZl0mXiRWJXlf+b/YqWLsuK9l2Wrpbu97ZRax8d3bNnTyaTOXfuXLFYDB+5cuXK4OAgEQ0PDw8MDNSzorlcLpvNHjx4cHBw8OTJk8VisVQqreWdP536+/uz2ez8/PyNGzfCbdHwGMzNmzeHh4frKZCISqVSsVg8e/bs4ODg0NDQ6OhoLpf7sdf8F8zzFes50rOkZ0kVSBVIF0jNsZ5j/YB0ifQ863lSFVaSlWTlsw6nwYBV4H03wtr7hKlUateuXeEv2h07dnied/Pmzd27d+/bt09KWeeKtrW1PbrHAisVTlyXL1/O5/PhVBbWeODAgVgsVucg6XQ6k8m8/vrrr7766o+6tk8J6StScyziJKJENhERKSKfOJwMF1mXSS/qwONAciCXb46S9uuaCYlo27ZtSzOe4zjNzc35fF5Kef369dHR0XpWNNwtHB8f9zzv1KlTyw+1Q/3CLdJLly6Njo6GOwhtbW3hZsWdO3fCAy3Lhb2FE92NGzeWD3L79u1yuYyTGWsnA0V6jsJrYvQM6RnSBVKzpGZZz7Eusiprv8qBx4HHgaSwxof7hIEM6oswlUrt379/6e5LL71ULBaHhoaGh4e3bt1aKBR+sKiOjo4333zz5MmTe/fuzefzBw4cwA7h6oTthduiRLRt27ZMJnPw4MHTp09v3bo1/OW4tHC4FRNueS4fZN++fS0tLUNDQ4cPHz5y5Ah2CNdC+orVLKtZ1gVWy/7pWVJF9hfYr7JfYb/KQZUDTweSH3Yofam+NxOK8+fP4496f1bwR70/KyMjI+GO9/vvv//aa6+FD/7jG39Zc+F3/unXrANWASmfleRAvvO7z2ou+ff/9n/hjXPnzuECboAVW0pouX/4mxe0dEmrh8dgAp+VlL56598/f/JoiBBgfchAsXRZB6zV0inB7+3+1YQIAdaH5ystF0kr1oqVTypgJb1VX7YGACslffXN5qgi/fAqmdVfOwoAKyV9xd4isyatWAekAo0IAX5K0tdausSatdJKqUAHvpa+/sEXIkKAuti2HQTBEz5u1AvUa2/9cUVjBkFg2zYiBKhLU1NTLpcLr5eo6T/+cG+lY+ZyuaampggRXbt2bU1rB+sNP5Gfoa6urlu3brW1ta3ls7eXC4Lg1q1b3d3dESJ6+eWX12VQgF+eCxcuhDc6OzuLxeLHH3+8ZcuWNaYYBEEul7t161Zzc3NnZyc2RwHq1d/fPzEx8cUXX1y9enXtXwjT3d397RfCAECdOjs7w3LWET7yEMAwRAhgGCIEMAwRAhiGAzMAK+C6brlc9jxvLd9SKIRwHCeVSiWTSUKEAPUrFotSynQ6nUwmhRCrHoeZXdctlUqe52UyGUQIUBfXdaWUra2tYX5r/L7eZDKZSCSmpqZc18U+IUBdyuVyOp1eywT4PUKIdDpdLpcxEwLUxfO8RCLx8E75AT2YISkpuYHSTZTcsLoxE4nE9PQ0IgSoCzN/Ow3OTYvsLVKKGhq5/c+5dbNwEk98dW1CCGbG5ijACrllUVmg8NrRxXmavBNMTaqKu+rxMBMCrFA0xk6CojHhSyIid8HKfcXE6tnn7ETy0cVZVmm+SF5FpDLUmHl0AUQIsEJRh59p5sqCKEwJWSUie6GocqSFEC0dlhNfviyrgGbzOn+PpEftwkaEAOtCZDZxLO6TbRe+tmWFiayFIt3zFRFvav92PvQ9vneHp+6yV1WtnTKWEK5MxqOW9Z1DrIgQYDWshpT93PM65uhCzl4oWkSisiByXzF/s126OK+n7tLUXVY6SG+qppo5mozVOsGBCAFWyU5v5FhcRWLWpK8rC0RklYuahCIhmlsof1/fu0NB4De1+a3P+/FUwnFiUfvRE42IEGD1Iomk1dahhKD8XWt+ThCLhQf2hMvTd6mySFrLlk4v0xLEUwkn5sTsaMSuMchPv94AvySWk+Bn23Q0qgsNYi4vAkmyytJjyw6SaS/TojdkErGYE7Mjdu0zgogQYK3sRANH4yoSsxeKHEhBJIi1HdGJhsBpSDpOLGrVnANDOFkPsA5swVERXtLNRMRMlvTsxZLjVyyLn1AgYSYEWDv2KjSbp5lJ4VW1HdWRGAVSqMD23Gh+wraJN7UJ67ETHiIEWBP2KlyY4sksz88RiaCxSSXTlrdou2Wr6kZmv2bbCmKOnXrGesxHlSJCgNXjisvT93kyS26ZSAQNaS/T7m9sdQRT/ivKfWWpgKYn2auqv9hC6Y1WrfkQEQKsEpdmuZDjQo7cMpMIGtLV5s3BM83xeCLuRCjyPDmOyt8XC/OiVNB3x6nteWpqsR45RooIAVaD3TJ//RXPfM2ySkRBQ1pubPeb2uNO3LYpYgvR+IxqSAUsWNy3yg9oelITUSRK6Yxlf+c4DY6OAqwYPyjwxB955muSVSKhowkv0+Zt2hyJRqJRK+FEw788tG07/ny33fErtSFNRDQ9qbO3VbXyvdEQIcDKsFfh4gxPT5KsEpGOOtVNHUF6kxNzEk4sFrGWfwSGsO1Ic2vkz7p1U6sWgosz6t6doFRcPiA2RwFWqOqSW2ZfEgkdjfuNTbK5w040WBY5UfvRD6Gx4oloe6eyoyQEzU3ryT+pSFQkG+xoLFwAEQKsUCzOTpIjMR11vI1tsqmdo04sYiecyOM+BkoIEX+2LYhG/Vhc5Cb88gNvdq6xtTV8FhEC1EUIobW2LEvEHNHcStG41BwkGqPJDRHbcqJ2zdMP377csuz0RrJsnUj6tiOFTURaayEEIgSoi+M4lUqloaGB7Ij1TLNIN5PrCa0tQXEnYj+xwJAViViZJpVu8quSpCSiSqXiOA4iBKhLKpUqlUqJRCKc8YSgVIOzinFsizYkYxuSMa11qVRqbGzE0VGAuiSTScdxpqamFhcXtdZrGUprvbi4ODU15ThOMpnETAhQr0wm47ru/Pz8zMzM2r8QprGxEV8IA7BiyWQyLGcdYXMUwDBECGAYIgQwDBECGIYIAQxDhACGIUIAwxAhgGGIEMAwRAhgGCIEMAwRAhiGCAEMQ4QAhiFCAMMQIYBhiBDAMEQIYBgiBDAMEQIYhggBDEOEAIYhQgDDECGAYYgQwDBECGAYIgQwDBECGIYIAQxDhACGIUIAwxAhgGGIEMAwRAhgGCIEMAwRAhiGCAEMQ4QAhiFCAMMQIYBhiBDAMEQIYBgiBDAMEQIYhggBDEOEAIYhQgDDECGAYYgQwDBECGAYIgQwDBECGIYIAQxDhACGIUIAwxAhgGGIEMAwRAhgGCIEMAwRAhiGCAEMQ4QAhiFCAMMQIYBhiBDAMEQIYBgiBDAMEQIYhggBDEOEAIYhQgDDECGAYYgQwDBECGAYIgQwDBECGIYIAQxDhACGIUIAwxAhgGGIEMAwRAhgGCIEMAwRAhiGCAEMQ4QAhiFCAMMQIYBhiBDAMEQIYBgiBDAMEQIYhggBDEOEAIYhQgDDECGAYYgQwDBECGAYIgQwDBECGIYIAQxDhACGIUIAwxAhgGGIEMAwRAhgGCIEMAwRAhiGCAEMQ4QAhiFCAMMQIYBhiBDAMEQIYBgiBDAMEQIYhggBDEOEAIYhQgDDECGAYYgQwDBECGAYIgQwDBECGIYIAQxDhACGIUIAwxAhgGGIEMAwRAhgGCIEMAwRAhiGCAEMixDRhQsXTK8GwNPr/wGc33wd8tvS/QAAAABJRU5ErkJggg==", + "avatarDark": "", "defaultPosition": [ 1, 1, @@ -316,6 +370,7 @@ "authorEmail": "", "version": "1.0", "avatar": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAAAeCAIAAABVOSykAAAAB3RJTUUH6QkLFAI2wS35LQAAAGtJREFUaIHt2rENgDAAA8EEsXZSZHJW4CuEdDeB9bXnWmvwzj3GOOd8PeMH9t7X1xv+RKxArECsQKxArECsQKxArECsQKxArECsQKxArECsQKxArECsQKxArECsQKxArECsQKxArGA6hrz3AHo+BisZkRSqAAAAAElFTkSuQmCC", + "avatarDark": "", "defaultPosition": [ 1, 1, @@ -333,6 +388,7 @@ "authorEmail": "", "version": "1.0", "avatar": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAIAAAC0Ujn1AAAAB3RJTUUH6QsNEDYeFxAGQAAAAllJREFUSIm1lkFvokAYhmdYmhBqwYx1LSQIEpPWAxeNW73o3+j/2x76B7Z347ZysDSNB230QNI0JBxkGgmgTGAPZHvYpDJ07Xsako+Hjzffl3cgxhgAMJ/PZ7OZ4ziEEPBZsSwrSZJhGK1WCwDAAgBGo9HLy0uv19M0jWXZT6MJIbZtm6bpuu5wOISTyeTh4eHq6up/oP984ObmptPpfLu8vOx2u6enpx+VJkmyXC6vr3/e3v4KgrBWq3EctwfNMAzP809PT4zjOJqm7Sl1Xff+/t627c1mc3f3e7FY5DauaZrjOAwhZL8VcRyHYZCdt9ttFEVJkuxHsyxLCGFyWyiXy42Gfnx8zHGcqmpnZ2cMk/8WyCZkv05OTvr9fqWCMH5rNpv1ep2GS4UGAIii2O3+oCS+i+rXPqcvRFMZQkiM8dt2uxUEoVQqQQgPgyYkXq1WlmX5vq/rervdQQgdBu152LKs6XSapunr66solinR+V7vdjvf99M0BQCEYRhFYXY+AFoQBF3XBUE4OjpSVVWW5YN5XSqV2u2OKJajKJRlWVEOtzIQQoQQQihNU8p+MxWY60LcYuiiokKnaer7vud5hZIz3+s4jp+fn8fjcRAE5+fn/X6vUvkwkoqh1+v146O1Wi2TJMHYq9W+I1Sh8Z12ZbJkCYIgCEKalqnQCKFGQ89CTlGUAiuT5dieeOR5fjAYqGp9s9nU62q1Ws2FZkBWkiTbtpvN5kd1EEKe5y8uWu+PuWjbtiVJYgzDME0zd6rgX9G0bJqmYRgQY3zwi5miKMPhEH7ddfIPc/we6t+lCqkAAAAASUVORK5CYII=", + "avatarDark": "", "defaultPosition": [ 1, 1,