diff --git a/deploy/images/SearchDropdownExampleOpen.png b/deploy/images/SearchDropdownExampleOpen.png
new file mode 100644
index 00000000..e671afc6
Binary files /dev/null and b/deploy/images/SearchDropdownExampleOpen.png differ
diff --git a/deploy/images/SearchDropdownExampleSearching.png b/deploy/images/SearchDropdownExampleSearching.png
new file mode 100644
index 00000000..d6f70136
Binary files /dev/null and b/deploy/images/SearchDropdownExampleSearching.png differ
diff --git a/deploy/widget_icons/SearchDropdown.png b/deploy/widget_icons/SearchDropdown.png
new file mode 100644
index 00000000..9ee04e0a
Binary files /dev/null and b/deploy/widget_icons/SearchDropdown.png differ
diff --git a/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/JMJb_dCLyH7X_iHp2DLoMYNEdokd.xml b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/JMJb_dCLyH7X_iHp2DLoMYNEdokd.xml
index 99772b42..378b6137 100644
--- a/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/JMJb_dCLyH7X_iHp2DLoMYNEdokd.xml
+++ b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/JMJb_dCLyH7X_iHp2DLoMYNEdokd.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/J_q8ERiDV2uofqHvpCu2mo8uB9Ad.xml b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/J_q8ERiDV2uofqHvpCu2mo8uB9Ad.xml
index 99772b42..378b6137 100644
--- a/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/J_q8ERiDV2uofqHvpCu2mo8uB9Ad.xml
+++ b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/J_q8ERiDV2uofqHvpCu2mo8uB9Ad.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/S9wGd_pDd1D_9FNVqjfXodz45sod.xml b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/S9wGd_pDd1D_9FNVqjfXodz45sod.xml
index 99772b42..378b6137 100644
--- a/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/S9wGd_pDd1D_9FNVqjfXodz45sod.xml
+++ b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/S9wGd_pDd1D_9FNVqjfXodz45sod.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/V0mMK9LtsBu9P5LKljXbNE-C5qwd.xml b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/V0mMK9LtsBu9P5LKljXbNE-C5qwd.xml
index 99772b42..378b6137 100644
--- a/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/V0mMK9LtsBu9P5LKljXbNE-C5qwd.xml
+++ b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/V0mMK9LtsBu9P5LKljXbNE-C5qwd.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/YjX3mbPy2HN8LR96rq4Fzb372VUd.xml b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/YjX3mbPy2HN8LR96rq4Fzb372VUd.xml
index 99772b42..378b6137 100644
--- a/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/YjX3mbPy2HN8LR96rq4Fzb372VUd.xml
+++ b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/YjX3mbPy2HN8LR96rq4Fzb372VUd.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/fgSmFlkgej181VWOoxvdiZJ1K9gd.xml b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/fgSmFlkgej181VWOoxvdiZJ1K9gd.xml
index 7a6326b9..378b6137 100644
--- a/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/fgSmFlkgej181VWOoxvdiZJ1K9gd.xml
+++ b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/fgSmFlkgej181VWOoxvdiZJ1K9gd.xml
@@ -1,6 +1,6 @@
-
+
-
+
\ No newline at end of file
diff --git a/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/qR28PGzPsksFVChmMp9R0Zbo0uYd.xml b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/qR28PGzPsksFVChmMp9R0Zbo0uYd.xml
index 99772b42..378b6137 100644
--- a/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/qR28PGzPsksFVChmMp9R0Zbo0uYd.xml
+++ b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/qR28PGzPsksFVChmMp9R0Zbo0uYd.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/xTX_9QKVz_sRcYaTywWZA4aZm4kd.xml b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/xTX_9QKVz_sRcYaTywWZA4aZm4kd.xml
new file mode 100644
index 00000000..378b6137
--- /dev/null
+++ b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/xTX_9QKVz_sRcYaTywWZA4aZm4kd.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/xTX_9QKVz_sRcYaTywWZA4aZm4kp.xml b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/xTX_9QKVz_sRcYaTywWZA4aZm4kp.xml
new file mode 100644
index 00000000..fb20dac4
--- /dev/null
+++ b/resources/project/Xc4-tOl6vkwzCeVKSWRahPrXJiQ/xTX_9QKVz_sRcYaTywWZA4aZm4kp.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/resources/project/elO4D6tOV7Lp-Jfo7ptgr2NlB30/mBLWFebZLKEYjmQwRCNBG-dcvOEd.xml b/resources/project/elO4D6tOV7Lp-Jfo7ptgr2NlB30/mBLWFebZLKEYjmQwRCNBG-dcvOEd.xml
new file mode 100644
index 00000000..99772b42
--- /dev/null
+++ b/resources/project/elO4D6tOV7Lp-Jfo7ptgr2NlB30/mBLWFebZLKEYjmQwRCNBG-dcvOEd.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/resources/project/elO4D6tOV7Lp-Jfo7ptgr2NlB30/mBLWFebZLKEYjmQwRCNBG-dcvOEp.xml b/resources/project/elO4D6tOV7Lp-Jfo7ptgr2NlB30/mBLWFebZLKEYjmQwRCNBG-dcvOEp.xml
new file mode 100644
index 00000000..fb20dac4
--- /dev/null
+++ b/resources/project/elO4D6tOV7Lp-Jfo7ptgr2NlB30/mBLWFebZLKEYjmQwRCNBG-dcvOEp.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/resources/project/f1CasPtPWEKkULO6lQ_WTjau2Nw/thSBeYSSaW6FYs_WYLbsJvWC9Acd.xml b/resources/project/f1CasPtPWEKkULO6lQ_WTjau2Nw/thSBeYSSaW6FYs_WYLbsJvWC9Acd.xml
new file mode 100644
index 00000000..99772b42
--- /dev/null
+++ b/resources/project/f1CasPtPWEKkULO6lQ_WTjau2Nw/thSBeYSSaW6FYs_WYLbsJvWC9Acd.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/resources/project/f1CasPtPWEKkULO6lQ_WTjau2Nw/thSBeYSSaW6FYs_WYLbsJvWC9Acp.xml b/resources/project/f1CasPtPWEKkULO6lQ_WTjau2Nw/thSBeYSSaW6FYs_WYLbsJvWC9Acp.xml
new file mode 100644
index 00000000..8222afbe
--- /dev/null
+++ b/resources/project/f1CasPtPWEKkULO6lQ_WTjau2Nw/thSBeYSSaW6FYs_WYLbsJvWC9Acp.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/resources/project/qPaYXYIWInHoCm9ctG8-uEqdxlk/Kc8NyOxBHteHzaAs2Svjccxs8jsd.xml b/resources/project/qPaYXYIWInHoCm9ctG8-uEqdxlk/Kc8NyOxBHteHzaAs2Svjccxs8jsd.xml
new file mode 100644
index 00000000..a75f7a81
--- /dev/null
+++ b/resources/project/qPaYXYIWInHoCm9ctG8-uEqdxlk/Kc8NyOxBHteHzaAs2Svjccxs8jsd.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/resources/project/qPaYXYIWInHoCm9ctG8-uEqdxlk/Kc8NyOxBHteHzaAs2Svjccxs8jsp.xml b/resources/project/qPaYXYIWInHoCm9ctG8-uEqdxlk/Kc8NyOxBHteHzaAs2Svjccxs8jsp.xml
new file mode 100644
index 00000000..f1a9071d
--- /dev/null
+++ b/resources/project/qPaYXYIWInHoCm9ctG8-uEqdxlk/Kc8NyOxBHteHzaAs2Svjccxs8jsp.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/resources/project/sSpxFEkGAHXZFJ64I8GyrijBl1s/_08KYHmkQrwI4wltgg7WZLZLxR4d.xml b/resources/project/sSpxFEkGAHXZFJ64I8GyrijBl1s/_08KYHmkQrwI4wltgg7WZLZLxR4d.xml
new file mode 100644
index 00000000..a75f7a81
--- /dev/null
+++ b/resources/project/sSpxFEkGAHXZFJ64I8GyrijBl1s/_08KYHmkQrwI4wltgg7WZLZLxR4d.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/resources/project/sSpxFEkGAHXZFJ64I8GyrijBl1s/_08KYHmkQrwI4wltgg7WZLZLxR4p.xml b/resources/project/sSpxFEkGAHXZFJ64I8GyrijBl1s/_08KYHmkQrwI4wltgg7WZLZLxR4p.xml
new file mode 100644
index 00000000..08a2d7b2
--- /dev/null
+++ b/resources/project/sSpxFEkGAHXZFJ64I8GyrijBl1s/_08KYHmkQrwI4wltgg7WZLZLxR4p.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/resources/project/sSpxFEkGAHXZFJ64I8GyrijBl1s/rVbGKXEYIRCxCGbb4rKJ4OqWDVod.xml b/resources/project/sSpxFEkGAHXZFJ64I8GyrijBl1s/rVbGKXEYIRCxCGbb4rKJ4OqWDVod.xml
new file mode 100644
index 00000000..4356a6ae
--- /dev/null
+++ b/resources/project/sSpxFEkGAHXZFJ64I8GyrijBl1s/rVbGKXEYIRCxCGbb4rKJ4OqWDVod.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/resources/project/sSpxFEkGAHXZFJ64I8GyrijBl1s/rVbGKXEYIRCxCGbb4rKJ4OqWDVop.xml b/resources/project/sSpxFEkGAHXZFJ64I8GyrijBl1s/rVbGKXEYIRCxCGbb4rKJ4OqWDVop.xml
new file mode 100644
index 00000000..c2b58c46
--- /dev/null
+++ b/resources/project/sSpxFEkGAHXZFJ64I8GyrijBl1s/rVbGKXEYIRCxCGbb4rKJ4OqWDVop.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/test/+wt/+test/BaseTest.m b/test/+wt/+test/BaseTest.m
index 292fa722..4567a861 100644
--- a/test/+wt/+test/BaseTest.m
+++ b/test/+wt/+test/BaseTest.m
@@ -191,13 +191,34 @@ function verifyPropertyValue(testCase, component, property, expValue)
expValue
end
- import matlab.unittest.constraints.Eventually
- import matlab.unittest.constraints.IsEqualTo
+ import matlab.unittest.constraints.*
% Verify values
+ if isStringScalar(expValue)
+
+ constraint = IsEqualTo(expValue,...
+ "Using", StringComparator);
+
+ elseif islogical(expValue)
+
+ constraint = IsEqualTo(expValue,...
+ "Using", LogicalComparator);
+
+ elseif isnumeric(expValue)
+
+ constraint = IsEqualTo(expValue,...
+ "Using", NumericComparator);
+
+ else
+
+ constraint = IsEqualTo(expValue);
+
+ end
+
+ % Perform the verification
testCase.verifyThat(...
@()get(component, property),...
- Eventually(IsEqualTo(expValue), "WithTimeoutOf", 5));
+ Eventually(constraint, "WithTimeoutOf", 5));
end %function
diff --git a/test/+wt/+test/BaseWidgetTest.m b/test/+wt/+test/BaseWidgetTest.m
index c56c6008..ecb5707e 100644
--- a/test/+wt/+test/BaseWidgetTest.m
+++ b/test/+wt/+test/BaseWidgetTest.m
@@ -114,6 +114,10 @@ function verifySetProperty(testCase,propName,newValue,expValue)
% Verify new property value
actualValue = testCase.Widget.(propName);
testCase.verifyEquality(actualValue, expValue);
+
+ % Tried to use this one, but the above works better because
+ % strings can equate to enumeration strings, etc.
+ % testCase.verifyPropertyValue(testCase.Widget, propName, expValue);
end %function
@@ -134,7 +138,7 @@ function verifySetPropertyError(testCase,propName,newValue,errorId)
end %function
- function verifyTypeAction(testCase,control,newValue,propName,expValue)
+ function verifyTypeAction(testCase,component,newValue,propName,expValue)
% If no expected new value after set was given, assume it
% matches the newValue provided as usual
@@ -143,12 +147,16 @@ function verifyTypeAction(testCase,control,newValue,propName,expValue)
end
% Type the new value into the control
- testCase.type(control, newValue);
+ testCase.type(component, newValue);
% Verify new property value
actualValue = testCase.Widget.(propName);
testCase.verifyEquality(actualValue, expValue);
-
+
+ % Tried to use this one, but the above works better because
+ % strings can equate to enumeration strings, etc.
+ % testCase.verifyPropertyValue(component, propName, expValue);
+
end %function
diff --git a/test/+wt/+test/SearchDropDown.m b/test/+wt/+test/SearchDropDown.m
new file mode 100644
index 00000000..3120f1ae
--- /dev/null
+++ b/test/+wt/+test/SearchDropDown.m
@@ -0,0 +1,129 @@
+classdef SearchDropDown < wt.test.BaseWidgetTest
+ % Implements a unit test for a widget or component
+
+ % Copyright 2025 The MathWorks Inc.
+
+ %% Properties
+ properties
+ ItemNames = [
+ "Voltage A"
+ "Voltage B"
+ "Current A"
+ "Current B"
+ "Power A"
+ "Power B"
+ ];
+ ItemData = [
+ 11
+ 12
+ 21
+ 22
+ 31
+ 32
+ ];
+ end
+
+
+ %% Class Setup
+ methods (TestClassSetup)
+
+ function createFigure(testCase)
+
+ % Start with superclass
+ testCase.createFigure@wt.test.BaseWidgetTest();
+
+ % Adjust grid size
+ testCase.Figure.Position(3:4) = [1200 700];
+ testCase.Grid.RowHeight = repmat({'fit'},1,4);
+ testCase.Grid.ColumnWidth = repmat({'1x'},1,6);
+
+ end %function
+
+ end %methods
+
+
+ %% Test Method Setup
+ methods (TestMethodSetup)
+
+ function setup(testCase)
+
+ fcn = @()wt.SearchDropDown(testCase.Grid);
+ testCase.Widget = verifyWarningFree(testCase,fcn);
+ drawnow
+
+ % Set the initial items
+ testCase.verifySetProperty("Items", testCase.ItemNames);
+
+ % Verify empty default
+ expValue = "";
+ testCase.verifyPropertyValue(testCase.Widget, "Value", expValue)
+ testCase.verifyPropertyValue(testCase.Widget.EditField, "Value", char(expValue))
+
+ % Set callback
+ testCase.Widget.ValueChangedFcn = @(s,e)onCallbackTriggered(testCase,e);
+
+ end %function
+
+ end %methods
+
+
+ %% Unit Test
+ methods (Test)
+
+ function testValueProperty(testCase)
+
+ % Get the component
+ comp = testCase.Widget;
+
+ % Set a value from the list
+ expValue = testCase.ItemNames(3);
+ testCase.Widget.Value = expValue;
+ testCase.verifyPropertyValue(comp, "Value", expValue)
+ testCase.verifyPropertyValue(comp.EditField, "Value", char(expValue))
+
+ % Clear the value
+ expValue = "";
+ testCase.Widget.Value = expValue;
+ testCase.verifyPropertyValue(comp, "Value", expValue)
+ testCase.verifyPropertyValue(comp.EditField, "Value", char(expValue))
+
+ % Verify number of callbacks so far
+ testCase.verifyCallbackCount(0)
+
+ end %function
+
+
+
+ function testInteractivePartialSearch(testCase)
+
+ % Get the component
+ comp = testCase.Widget;
+
+ % Simulate typing to filter the dropdown items
+ testCase.type(comp.EditField, "urren")
+ testCase.verifyPropertyValue(comp.ListBox, "Items", cellstr(testCase.ItemNames(3:4)'));
+
+ % Verify number of callbacks so far
+ testCase.verifyCallbackCount(1)
+
+ % Ensure the listbox is showing
+ % It might not be due to the nature of the type command
+ comp.SearchPanel.Visible = true;
+
+ % Simulate selecting the second item from the filtered list
+ testCase.choose(comp.ListBox, 2);
+
+ testCase.verifyPropertyValue(comp, "Value", testCase.ItemNames(4));
+ testCase.verifyPropertyValue(comp.EditField, "Value", char(testCase.ItemNames(4)));
+
+ % Verify number of callbacks so far
+ testCase.verifyCallbackCount(2)
+
+ % Verify the list is no longer visible
+ testCase.verifyNotVisible(comp.SearchPanel)
+
+ end %function
+
+ end %methods (Test)
+
+end %classdef
\ No newline at end of file
diff --git a/widgets/+wt/SearchDropDown.m b/widgets/+wt/SearchDropDown.m
new file mode 100644
index 00000000..0add4a62
--- /dev/null
+++ b/widgets/+wt/SearchDropDown.m
@@ -0,0 +1,893 @@
+classdef SearchDropDown < wt.abstract.BaseWidget &...
+ wt.mixin.Enableable & ...
+ wt.mixin.FieldColorable & ...
+ wt.mixin.FontStyled & ...
+ wt.mixin.Tooltipable
+ % Searchable text field with drop down list
+
+ % Copyright 2025 The MathWorks Inc.
+
+ % This is a prototype widget that is like an editable dropdown, but it
+ % enables you to search/filter better as you type. It combines an edit
+ % field with a listbox below. Space beneath is required for it to work.
+
+ % Known Issues
+ % 1. If you are typing in edit field and hit down arrow, it should go
+ % into the list below, but instead it completes the edit. But if you
+ % tab or enter to finish editing, it should complete the edit.
+
+
+ %% Events
+ events (HasCallbackProperty, NotifyAccess = protected)
+
+ % Triggered on value changed, has companion callback
+ ValueChanged
+
+ end %events
+
+
+ %% Public properties
+ properties (AbortSet)
+
+ % Placeholder
+ Placeholder (1,1) string = ""
+
+ % List of items to choose from
+ Items (:,1) string
+
+ % Confirmed value, selected by the user
+ Value (1,1) string
+
+ end %properties
+
+
+ %% Read-only properties
+ properties (SetAccess = protected, UsedInUpdate = false)
+
+ % Current typed value, displayed in the edit field
+ EditingValue (1,1) string
+
+ end %properties
+
+
+
+ %% Internal Properties
+ properties (Hidden)
+
+ Debug (1,1) logical = false
+
+ end %properties
+
+
+ properties (Dependent, Hidden, SetAccess = immutable)
+
+ % Indicates search panel exists
+ SearchExists (1,1) logical
+
+ % Indicates search is open
+ SearchOpen (1,1) logical
+
+ end %properties
+
+
+ properties (Transient, NonCopyable, Hidden, ...
+ SetAccess = protected, UsedInUpdate = false)
+
+ % Edit field
+ EditField matlab.ui.control.EditField
+
+ % Search panel for drop down
+ SearchPanel matlab.ui.container.Panel
+
+ % Grid for drop down
+ SearchGrid matlab.ui.container.GridLayout
+
+ % List selection
+ ListBox matlab.ui.control.ListBox
+
+ % Event listeners
+ LocationChangedListener event.listener
+ FigureCurrentObjectListener event.proplistener
+ FigureActivatedListener event.listener
+ FigureDeactivatedListener event.listener
+ WindowKeyPressListener event.listener
+
+ ListButtonDownListener event.listener
+ WindowMouseDownListener event.listener
+ WindowMouseReleaseListener event.listener
+
+
+ LastClick (1,1) string
+
+ % Figure ancestor
+ Figure matlab.ui.Figure
+
+ end %properties
+
+ % Accessors
+ methods
+ function tf = get.SearchOpen(obj)
+ tf = obj.SearchExists && obj.SearchPanel.Visible;
+ end
+ function tf = get.SearchExists(obj)
+ tf = isscalar(obj.SearchPanel) && isvalid(obj.SearchPanel);
+ end
+ end
+
+
+ %% Protected methods
+ methods (Access = protected)
+
+ function setup(obj)
+
+ % Call superclass method
+ obj.setup@wt.abstract.BaseWidget()
+
+ % Set default size
+ obj.Position(3:4) = [100 30];
+
+ % Configure Main Grid
+ % obj.Grid.Padding = 2;
+
+ % Create the edit control for user input
+ obj.EditField = uieditfield(obj.Grid, 'text');
+ obj.EditField.ValueChangingFcn = @(~,evt)onEditValueChanging(obj,evt);
+ obj.EditField.ValueChangedFcn = @(~,evt)onEditValueChanged(obj,evt);
+
+ % Create the search pannel (hidden)
+ obj.createSearchPanel()
+
+ % Attach figure
+ obj.Figure = ancestor(obj,'figure');
+
+ % Configure listeners
+ obj.LocationChangedListener = listener(obj.EditField, ...
+ "LocationChanged", @(~,evt)onLocationChanged(obj,evt));
+
+ % Listen to figure change
+ obj.createFigureListeners();
+
+ % Update the internal component lists
+ % obj.BackgroundColorableComponents = obj.Grid;
+
+ end %function
+
+
+ function update(obj)
+
+ if obj.Debug
+ disp('update');
+ end
+
+ % Update the contents
+ obj.EditField.Placeholder = obj.Placeholder;
+ obj.EditField.Value = obj.Value;
+ % obj.EditField.Value = obj.EditingValue;
+
+ % Update filtered list
+ % obj.updateFilteredList()
+
+ % Close the search panel
+ % (without giving focus to the edit field)
+ obj.closeSearchPanel(false);
+
+ end %function
+
+
+ function createSearchPanel(obj)
+
+ if obj.Debug
+ disp('createSearchPanel');
+ end
+
+ % Create the search panel content
+ obj.SearchPanel = uipanel("Parent",[]);
+
+ % Create the grid layout for the search panel
+ obj.SearchGrid = uigridlayout(obj.SearchPanel);
+ obj.SearchGrid.RowHeight = {'1x'};
+ obj.SearchGrid.ColumnWidth = {'1x'};
+ obj.SearchGrid.Padding = 0;
+
+ % Create the list box for displaying search results
+ obj.ListBox = uilistbox(obj.SearchGrid);
+ obj.ListBox.ValueChangedFcn = @(~,evt)onListValueChanged(obj,evt);
+
+
+ % Check for clicks
+ if isMATLABReleaseOlderThan("R2022b")
+
+ else
+ % R2022b and later can use this
+ obj.ListBox.ClickedFcn = @(~,evt)onListClicked(obj,evt);
+ end
+
+ % Listen for button down on list
+ obj.ListButtonDownListener = listener(obj.ListBox, ...
+ "ButtonDown", @(~,evt)onListButtonDown(obj,evt));
+
+ % Later: Add more controls for case sensitive, etc.
+
+ % Update the internal component lists
+ obj.FontStyledComponents = [obj.EditField, obj.ListBox];
+ obj.FieldColorableComponents = [obj.EditField, obj.ListBox];
+ obj.EnableableComponents = [obj.EditField];
+ obj.TooltipableComponents = [obj.EditField];
+ obj.BackgroundColorableComponents = [obj.Grid, obj.SearchPanel, obj.SearchGrid];
+
+ end %function
+
+
+ function openSearchPanel(obj)
+
+ if obj.Debug
+ disp('openSearchPanel');
+ end
+
+ % Has figure changed?
+ currentFigure = ancestor(obj,'figure');
+ if ~isequal(currentFigure, obj.Figure)
+ obj.onLocationChanged();
+ end
+
+ % Does the search panel exist?
+ if ~obj.SearchExists
+ obj.createSearchPanel();
+ end
+
+ % Show the complete list initially
+ items = obj.Items;
+ obj.ListBox.Items = items;
+
+ % What to select as default in list?
+ if isempty(items)
+ % Empty list selection
+ obj.ListBox.Value = {};
+ else
+ % Default to first item
+ obj.ListBox.Value = items(1);
+ end
+ idxInit = find(matches(items, obj.Value), 1);
+ if isscalar(idxInit)
+ % Default to matching item
+ obj.ListBox.Value = items(idxInit);
+ else
+ % Empty list selection
+ obj.ListBox.Value = {};
+ end
+
+ % Show the search panel
+ obj.SearchPanel.Parent = obj.Figure;
+
+ % Update position
+ obj.updateSearchPanelPosition()
+
+ % Toggle visible
+ obj.SearchPanel.Visible = 'on';
+
+ % Focus back on edit field
+ % disp(" openSearchPanel: Focus EditField");
+ % focus(obj.EditField)
+
+ end %function
+
+
+ function updateSearchPanelPosition(obj)
+
+ if obj.Debug
+ disp('updateSearchPanelPosition');
+ end
+
+ % if obj.SearchOpen
+ if obj.SearchExists
+
+ % Calculate position
+ edPos = getpixelposition(obj.EditField, true);
+ maxHeight = 300;
+ x = edPos(1);
+ w = edPos(3);
+ h = edPos(2) - 10;
+ h = min(h, maxHeight);
+ y = edPos(2) - h;
+ searchPosition = [x y w h];
+
+ % Set position
+ obj.SearchPanel.Position = searchPosition;
+
+ end
+
+ end %function
+
+
+ function closeSearchPanel(obj, focusEdit)
+
+ arguments
+ obj
+ focusEdit (1,1) logical = false;
+ end
+
+ if obj.Debug
+ disp('closeSearchPanel');
+ end
+
+ % Is the search panel open?
+ if obj.SearchOpen
+
+ % Hide the search panel
+ obj.SearchPanel.Visible = 'off';
+
+ end %if
+
+ % Is it in focus? If so, focus to the edit field instead
+ if obj.SearchExists
+
+ if obj.Debug
+ disp(' closeSearchPanel: check current focus object');
+ end
+
+ curObj = obj.Figure.CurrentObject;
+
+ searchObjs = [
+ obj.SearchGrid
+ obj.SearchPanel
+ obj.ListBox
+ ];
+
+ if isscalar(curObj) && any(curObj == searchObjs) && focusEdit
+ if obj.Debug
+ disp(" closeSearchPanel: Focus EditField");
+ end
+ focus(obj.EditField)
+ end
+
+ end
+
+ % curObjNew = obj.Figure.CurrentObject;
+
+ end %function
+
+
+ function updateFilteredList(obj)
+ % display the filtered items in the listbox
+
+ if obj.Debug
+ disp('updateFilteredList');
+ end
+
+ if obj.SearchExists
+
+ allItems = obj.Items;
+ isMatch = contains(allItems, obj.EditingValue, 'IgnoreCase', true);
+ items = allItems(isMatch);
+
+ % Update the list contents
+ obj.ListBox.Items = items;
+
+ % What to select as default in list?
+ % if isempty(items)
+ % % Empty list selection
+ % obj.ListBox.Value = {};
+ % elseif isempty(obj.ListBox.Value)
+ % % Default to first item
+ % obj.ListBox.Value = items(1);
+ % else
+ % % Leave selection as-is
+ % end
+
+ end %if
+
+ end %function
+
+
+ function onListValueChanged(obj,evt)
+ % Triggered after an item in the list has been selected
+
+ if obj.Debug
+ fprintf("onListValueChanged EventValue: %s ListboxValue: %s LastClick: %s\n", evt.Value, string(obj.ListBox.Value), obj.LastClick);
+ end
+
+ newValue = evt.Value;
+ obj.EditingValue = newValue;
+
+ switch obj.LastClick
+
+ case "ListBox"
+
+ % Clear state
+ obj.LastClick = "";
+
+ if isMATLABReleaseOlderThan("R2022b")
+
+ % Accept the value and close the
+ obj.acceptEditValue();
+
+ % Close the search panel
+ % and give focus to the edit field
+ obj.closeSearchPanel(true);
+
+ end
+
+ end %switch
+
+ end %function
+
+
+ function onListClicked(obj,evt)
+ % Triggered after an item in the list has been clicked
+ % (R2022b and later only)
+
+ % Get the changed value
+ % newValue = string(obj.ListBox.Value)
+ idx = evt.InteractionInformation.Item;
+ newValue = string(obj.ListBox.Items(idx));
+
+ if obj.Debug
+ % disp("onListClicked");
+ fprintf("onListClicked Index: %d ItemValue: %s ListboxValue: %s\n", idx, newValue, string(obj.ListBox.Value));
+ end
+
+ % Was an item clicked (rather than empty space)?
+ if isscalar(newValue)
+
+ % Complete the action
+ obj.EditingValue = newValue;
+
+ % Accept the value
+ obj.acceptEditValue();
+
+ % Close the search panel
+ % Do not give focus to the edit field
+ obj.closeSearchPanel(false);
+
+ end %if
+
+ end %function
+
+
+ function onFigureButtonDown(obj,evt)
+ % Triggered on any click within the figure
+
+
+ if obj.Debug
+ fprintf("onFigureButtonDown ClickOn: %s ListboxValue: %s\n", class(evt.HitObject), string(obj.ListBox.Value));
+ end
+
+
+ switch evt.HitObject
+
+ case obj.ListBox
+
+ obj.LastClick = "ListBox";
+
+ if isMATLABReleaseOlderThan("R2022b")
+ % For R2022a
+
+ % % Get the changed value
+ % newValue = string(obj.ListBox.Value);
+ %
+ %
+ % fprintf(" onFigureButtonDown (R2022a) - Accepting ListboxValue: %s\n", newValue);
+ %
+ % % Complete the action
+ % obj.EditingValue = newValue;
+ %
+ % % Accept the value and close the
+ % obj.acceptEditValue();
+ %
+ % % Close the search panel
+ % obj.closeSearchPanel(true);
+
+
+ else
+ % R2022b and later can use this
+
+ end
+
+ case obj.EditField
+
+ obj.LastClick = "EditField";
+
+ % Do nothing
+ % The dropdown is activated elsewhere?
+
+ otherwise
+ % Clicked any other figure object
+
+ obj.LastClick = "";
+
+ % Close the search panel
+ % Do not give focus to the edit field
+ obj.closeSearchPanel(false);
+
+ end %switch
+
+ end %function
+
+
+ function onFigureButtonUp(obj,evt)
+ % Triggered after
+
+ if obj.Debug
+ fprintf("onFigureButtonUp Hit: %s ListboxValue: %s\n", class(evt.HitObject), string(obj.ListBox.Value));
+ end
+
+ % newValue = string(obj.ListBox.Value)
+
+ % % Close the search panel
+ % obj.closeSearchPanel();
+
+ end %function
+
+
+ function onListButtonDown(obj,evt)
+ % Triggered after a key press in the list field
+
+ % Get the changed value
+ % newValue = string(obj.ListBox.Value)
+ idx = evt.InteractionInformation.Item;
+ newValue = string(obj.ListBox.Items(idx));
+
+ if obj.Debug
+ fprintf("onListButtonDown Index: %d ItemValue: %s ListboxValue: %s\n", idx, newValue, string(obj.ListBox.Value));
+ end
+
+ % Complete the action
+ obj.EditingValue = newValue;
+
+ % Accept the value
+ obj.acceptEditValue();
+
+ % Close the search panel
+ % and give focus to the edit field
+ obj.closeSearchPanel(true);
+
+ end %function
+
+
+ function onEditValueChanging(obj,evt)
+ % Triggered on edit field typing
+
+ if obj.Debug
+ fprintf("onEditValueChanging New EditingValue: %s Existing Value: %s\n", evt.Value, obj.Value);
+ end
+
+ % Get the editing value
+ newValue = evt.Value;
+
+ % Update the value property
+ obj.EditingValue = newValue;
+
+ % Verify search is open
+ if ~obj.SearchOpen
+ obj.openSearchPanel();
+ end
+
+ % Update filtered list
+ obj.updateFilteredList()
+
+ end %function
+
+
+ function onEditValueChanged(obj,evt)
+ % Triggered on edit field enter or loss of focus
+
+ if obj.Debug
+ fprintf("onEditValueChanged New EditingValue: %s Existing Value: %s LastClick: %s\n", evt.Value, obj.Value, obj.LastClick);
+ end
+
+ switch obj.LastClick
+
+ case "EditField"
+
+ % Clear state
+ obj.LastClick = "";
+
+ % Are there items available?
+ if isempty(obj.ListBox.Items)
+ % Get the editing value
+ newValue = evt.Value;
+ else
+ % Get the first match in items
+ newValue = obj.ListBox.Items(1);
+ end
+
+ % Update the value property
+ obj.EditingValue = newValue;
+
+ % Accept the value
+ obj.acceptEditValue();
+
+ % Close the search panel
+ %obj.closeSearchPanel(true);
+ % Disabled this in favor of closing in the update method. The
+ % problem is in 22a if we start typing in the edit, then click
+ % an item in the list, this one fires first and hides the list
+ % before the list's selection callback can occur.
+
+ end %switch
+
+ end %function
+
+
+ function onLocationChanged(obj,~)
+ % Triggered on edit field location (figure) changed
+
+ if obj.Debug
+ disp("onLoctionChanged");
+ end
+
+ % Attach figure
+ obj.Figure = ancestor(obj,'figure');
+
+ % Move SearchPanel
+ if obj.SearchExists
+ obj.SearchPanel.Parent = obj.Figure;
+ end
+
+ % Listen to figure events
+ obj.createFigureListeners();
+
+ end %function
+
+
+ function createFigureListeners(obj)
+ % Create listeners to figure changes
+
+ if obj.Debug
+ disp("createFigureListeners");
+ end
+
+ % Get the figure ancestor
+ obj.Figure = ancestor(obj,'figure');
+
+ % Listen to current object (focus) changes
+ obj.FigureCurrentObjectListener = listener(obj.Figure, ...
+ "CurrentObject", "PostSet", ...
+ @(~,evt)onFigureCurrentObjectChanged(obj,evt));
+
+ % Listen to figure activation
+ obj.FigureActivatedListener = listener(obj.Figure, ...
+ "FigureActivated", ...
+ @(~,evt)onFigureActivated(obj,evt));
+
+ % Listen to figure deactivation
+ obj.FigureDeactivatedListener = listener(obj.Figure, ...
+ "FigureDeactivated", ...
+ @(~,evt)onFigureDeactivated(obj,evt));
+
+
+ obj.WindowMouseDownListener = listener(obj.Figure, ...
+ "WindowMousePress", ...
+ @(~,evt)onFigureButtonDown(obj,evt));
+
+ obj.WindowMouseReleaseListener = listener(obj.Figure, ...
+ "WindowMouseRelease", ...
+ @(~,evt)onFigureButtonUp(obj,evt));
+
+ % Listen to key presses
+ obj.WindowKeyPressListener = listener(obj.Figure, ...
+ "WindowKeyPress", ...
+ @(~,evt)onWindowKeyPress(obj,evt));
+
+
+ end %function
+
+
+ function onFigureCurrentObjectChanged(obj,~)
+ % Triggered after figure's current object (focus) changed
+
+
+ if obj.Debug
+ fprintf("onFigureCurrentObjectChanged CurrentObject: %s\n", class(obj.Figure.CurrentObject));
+ end
+
+ obj.checkForSearchActivation();
+
+ end %function
+
+
+ function onFigureActivated(obj,~)
+ % Triggered after figure is activated
+
+ if obj.Debug
+ disp("onFigureActivated");
+ end
+
+ % Close list
+ % obj.closeSearchPanel(false)
+
+ end %function
+
+
+ function onFigureDeactivated(obj,~)
+ % Triggered after figure is deactivated
+
+ if obj.Debug
+ disp("onFigureDeactivated");
+ end
+
+ % Close list
+ % obj.closeSearchPanel(false)
+
+ end %function
+
+
+ function onWindowKeyPress(obj,evt)
+ % Triggered after a key is pressed in the figure
+
+
+ if obj.Debug
+ fprintf("onWindowKeyPress Key: %s CurrentObject: %s LastClick: %s\n", evt.Key, class(obj.Figure.CurrentObject), obj.LastClick);
+ end
+
+ obj.LastClick = "";
+
+ % Button presses
+ escPressed = evt.Key == "escape" && isempty(evt.Modifier);
+ downArrowPressed = evt.Key == "downarrow" && isempty(evt.Modifier);
+ % upArrowPressed = evt.Key == "uparrow" && isempty(evt.Modifier);
+ enterPressed = evt.Key == "return" && isempty(evt.Modifier);
+
+ % Search state
+ searchOpen = obj.SearchOpen;
+ searchExists = obj.SearchExists;
+
+ % Edit state
+ curObj = obj.Figure.CurrentObject;
+
+ % Edit focus?
+ editFocus = isequal(curObj, obj.EditField);
+
+ % Search focus?
+ if searchExists
+ searchObjs = [
+ obj.SearchGrid
+ obj.SearchPanel
+ obj.ListBox
+ ];
+ searchFocus = isscalar(curObj) && any(curObj == searchObjs);
+ % searchIndexOne = isequal(obj.ListBox.ValueIndex, 1);
+ else
+ searchFocus = false;
+ % searchIndexOne = false;
+ end
+
+ if editFocus && enterPressed
+ % If enter pressed, accept the entered value
+
+ obj.acceptEditValue()
+
+ % Close list
+ % Do not give focus to the edit field
+ obj.closeSearchPanel(false);
+
+ elseif searchOpen && escPressed
+ % If ESC is pressed while list is open, close the list
+
+ % Close list
+ % and give focus to the edit field
+ obj.closeSearchPanel(true);
+
+ elseif searchOpen && editFocus && downArrowPressed
+ % Focus the search list
+
+ if obj.Debug
+ disp(" onWindowKeyPress: Focus ListBox 1");
+ end
+ focus(obj.ListBox)
+
+ % elseif searchOpen && searchFocus && upArrowPressed && searchIndexOne
+ %
+ % focus(obj.EditField)
+
+ elseif searchExists && editFocus && downArrowPressed
+ % Open and focus the search list
+
+ obj.openSearchPanel();
+
+ if obj.Debug
+ disp(" onWindowKeyPress: Focus ListBox 3");
+ end
+ focus(obj.ListBox)
+
+ elseif searchExists && ~searchOpen && editFocus
+ % Open the search panel
+
+ obj.openSearchPanel();
+
+ elseif searchExists && ~searchOpen && searchFocus
+ % If search is hidden but still focused, change to the edit
+ % field focus
+
+ % Don't know why search would be closed here
+
+ if obj.Debug
+ disp(" onWindowKeyPress: Focus ListBox 4");
+ end
+
+ if isMATLABReleaseOlderThan("R2022b")
+ % Do nothing
+ % The edit field might actually be focused and selected,
+ % causing typing to delete the existing text in the edit
+ % field.
+ else
+ focus(obj.EditField)
+ end
+
+ end %if
+
+ end %function
+
+
+ function checkForSearchActivation(obj)
+ % Check current object and activate search if conditions met
+
+ % What is in focus?
+ focusObject = obj.Figure.CurrentObject;
+
+ if focusObject == obj.EditField
+ % Open list
+ obj.openSearchPanel();
+ elseif isequal(focusObject, obj.ListBox)
+ % Ignore - do nothing
+ else
+ % Close list
+ % Do not give focus to the edit field
+ obj.closeSearchPanel(false)
+ end
+
+ end %function
+
+
+ function acceptEditValue(obj)
+ % Accept the currently selected value
+
+
+ % Get typed value
+ editValue = obj.EditingValue;
+ newValue = editValue;
+
+ if obj.Debug
+ fprintf("acceptEditValue NewValue: %s OldValue: %s\n", newValue, obj.Value);
+ end
+
+ % % Get list items
+ % if obj.SearchExists
+ % items = obj.ListBox.Items;
+ % else
+ % items = obj.Items;
+ % end
+ %
+ % % Select the new value intelligently
+ % if matches(editValue, items)
+ % newValue = editValue;
+ % elseif isempty(items)
+ % newValue = "";
+ % else
+ % newValue = items(1);
+ % end
+
+ % Prepare event data
+ oldValue = obj.Value;
+ evtOut = wt.eventdata.PropertyChangedData('Value', ...
+ newValue, oldValue);
+
+ % Update the value
+ obj.Value = newValue;
+
+ % Request update
+ obj.requestUpdate();
+
+ % Close the search panel
+ % obj.closeSearchPanel(false);
+
+ % Trigger event
+ notify(obj,"ValueChanged",evtOut);
+
+ end %function
+
+ end %methods
+
+
+end %classdef
+
diff --git a/widgets/doc/WidgetsList.mlx b/widgets/doc/WidgetsList.mlx
index 25b9206f..f1efaad6 100644
Binary files a/widgets/doc/WidgetsList.mlx and b/widgets/doc/WidgetsList.mlx differ
diff --git a/widgets/examples/SearchDropDownExample.mlx b/widgets/examples/SearchDropDownExample.mlx
new file mode 100644
index 00000000..25b93dbb
Binary files /dev/null and b/widgets/examples/SearchDropDownExample.mlx differ
diff --git a/widgets/resources/appDesigner.json b/widgets/resources/appDesigner.json
index 9af5f45b..3b1b603d 100644
--- a/widgets/resources/appDesigner.json
+++ b/widgets/resources/appDesigner.json
@@ -305,8 +305,25 @@
300,
200
]
+ },
+ {
+ "className": "wt.SearchDropDown",
+ "componentName": "Search Drop Down",
+ "description": "Editable test field that drops down a list of search matches",
+ "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAB3RJTUUH6QkLFAIxX0lsjgAAAjVJREFUSIm1lT9P40AQxX+7HieOFVmkoqCgoEhBh0BpEEgI0dHQIFFSUfF1+Ap0JF2gRaJBQqSkBhoaaAKOxuu9gnN0vpyI7/C9zn/03r55M7PGe+/5j5Dn52f6/T516njvaTQaHB4eImma8vb2xtraGsaYWgRUldFohKoi3nuSJGFrawtr7bedGGMYj8c8PDwAIPBpSVVrE8iybPpsv8VWAVKohmFYmwMRKQu8vr5yeXmJMaYWAVVlPB5/CnQ6HdbX13HOfYv4V4gIvV6POI6RZrPJ0tJSbeQFilLJy8sL5+fnRFH0z2S/l9V7T57nnJ6eInmes7i4yP7+Pt77v84gCIJSqMYYPj4+GAwGwM+QW60Ww+GQq6srGo1GZfLJZMLu7i4nJydMJpOpQLPZnG4FAXDOsbKywvb2NkEQVBbIsoxut4u1FhFBVaflKTD15pxDVUsf58EYw/39Pbe3t0RRxNHREUmSlP4RAGstT09P3N3dEYZhJfLitAB5ntNut8mybGZhSmF1c3OT5eVlrP16e3jvCYKA1dXVmbxEZGaeBCAMQ4bDIYPBYG7IeZ4TxzFnZ2ckSVIi/FMHSmH34OCAnZ2duXdC4WBhYQHn3Ny2lsLazc0N19fXlTLw3pOmKcfHx2xsbKCqXwt474njmE6nUxqar6CqtFotrLWl1jbGlHIUgPf3d/b29uj1epUnubhYHh8fZ96naTrNRsIwRFW5uLioRFwFRU7WWoxzzhdjXjeiKOIHJNsIDj55zxIAAAAASUVORK5CYII=",
+ "category": "Widgets Toolbox",
+ "authorName": "Robyn Jackey - MathWorks Consulting",
+ "authorEmail": "",
+ "version": "1.0",
+ "avatar": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAAAeCAIAAABVOSykAAAAB3RJTUUH6QkLFAI2wS35LQAAAGtJREFUaIHt2rENgDAAA8EEsXZSZHJW4CuEdDeB9bXnWmvwzj3GOOd8PeMH9t7X1xv+RKxArECsQKxArECsQKxArECsQKxArECsQKxArECsQKxArECsQKxArECsQKxArECsQKxArGA6hrz3AHo+BisZkRSqAAAAAElFTkSuQmCC",
+ "defaultPosition": [
+ 1,
+ 1,
+ 100,
+ 30
+ ]
}
],
- "schema": 1,
- "MATLABRelease": "R2021a"
+ "schema": 2,
+ "MATLABRelease": "R2022a"
}
\ No newline at end of file