Confusing interaction between "Callback" and "KeyPressFcn" for "edit"-style UIControl

2 ビュー (過去 30 日間)
Aaron
Aaron 2024 年 9 月 16 日
編集済み: Aaron 2024 年 9 月 18 日
Hello --
I'm having difficulty understanding an interaction between a "Callback" and "KeyPressFcn" in my figure- and uicontrol-based tool.
I have an "edit"-style uicontrol with both a "KeyPressFcn" and "Callback" defined. The "Callback" initiates an input validation sequence and is working as intended. I want the "KeyPressFcn" to perform a pseudo-autocomplete as defined here. Here's a minimal version of what I have:
% Initial setup ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
% Set allowed strings
str = ["FOO", "BAR", "BAZ", "QUX", "FOOBAR", "BAZQUX", "FARBOO"];
% Set up figure
f = figure;
e = uicontrol('Style', 'edit', 'String', str(3), 'Callback', @cb, ...
'KeyPressFcn', @kp, 'Interruptible', 'off', 'BusyAction', 'queue');
% Set up guidata
data.AllowedStrings = str; % Store allowed strings
data.EditBoxString = e.String; % Store a copy of the current string
data.AutoComplete.String = []; % Autocomplete: initial string
data.AutoComplete.Counter = 1; % Autocomplete: which completion are we on?
data.AutoComplete.Options = []; % Autocomplete: List of possible completions
guidata(f,data)
% Functions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
% Callback: Data Validation
function cb(src,~)
% Get guidata
data = guidata(src);
% Check supplied string against valid strings
val = erase(lower(src.String), [" ", ":", "-", "(", ")"]) == ...
lower(data.AllowedStrings);
% If invalid, reset the string to its previous value and return
if ~nnz(val)
src.String = data.EditBoxString;
return
end
% Set editbox string to validated string and repack data
src.String = data.AllowedStrings(val);
data.EditBoxString = data.AllowedStrings(val);
guidata(src,data)
end
% KeyPressFcn: "Autocomplete" on leftbracket push
function kp(src,k)
% Get guidata
data = guidata(src);
% Autocomplete if "leftbracket" was pressed
if k.Key == "leftbracket"
% Get autocomplete string if none exists
if isempty(data.AutoComplete.String)
data.AutoComplete.String = erase(lower(src.String), ...
["["," ", ":", "-", "(", ")"]);
end
% Get possible completions using "startsWith"
data.AutoComplete.Options = find(startsWith(...
lower(data.AllowedStrings), data.AutoComplete.String));
% Fill text with next possibility
if ~isempty(data.AutoComplete.Options)
% Set string
src.String = data.AllowedStrings(...
data.AutoComplete.Options(data.AutoComplete.Counter));
% Iterate counter
data.AutoComplete.Counter = mod(data.AutoComplete.Counter, ...
length(data.AutoComplete.Options)) + 1;
end
else
% Set autocomplete string to empty & reset counter
data.AutoComplete.String = [];
data.AutoComplete.Counter = 1;
data.AutoComplete.Options = [];
end
% Repack guidata
guidata(src,data)
end
This does not seem to work. However, if I place a breakpoint on lines 25 (data = guidata(src) in the cb function) and 54 (if isempty(data.AutoComplete.String) in the kp function) and use F5 / F10 to sequentially complete each function, it works exactly as intended! I'm able to supply an input, say "b", then repeatedly press "[" to cycle through the relevant options: "BAR", "BAZ", and "BAZQUX". I just need to roll forward through all the breakpoints before I press "[" again.
This "almost-right" behavior is dependent on the "Interruptible" property being set to "off", so I imagine it's due to some subtlety in when the "Callback" is executed; it's just baffling to me that everything is (or appears to be) working in the right order when I include breakpoints but not without them. I think I need my "Callback" never to execute while I'm cycling through my autocomplete options.
Any suggestions? I'd prefer not to migrate the edit box into the more modern UI elements if possible.
Thanks!
  4 件のコメント
Stephen23
Stephen23 2024 年 9 月 17 日
編集済み: Stephen23 2024 年 9 月 18 日
"...particularly the "normalized" units"
I know exactly where you are coming from, I had exactly the same fight with the function IREGEXP: specifying UI element sizes was a complete pain as everything is in terms of pixels. Resizing all of the UI elements was an awkward, complex, fiddly task...
Until I discovered UIGRIDLAYOUT.
Only then did UIFIGURE resizing make sense.
Forget about normalized units. Forget about setting the UI element sizes yourself. With UIFIGUREs trying to set the UI element sizes yourself only causes code bloat and loss of hair. In contrast, with UIGRIDLAYOUT it is simple and quite intuitive: it lets you specify absolute sizes, normalized sizes, and lots more. I recommend giving it a try.
When I rewrote IREGEXP (motivated by the irreplaceable VALUECHANGINGFCN) I made a false start by trying to specify the element sizes myself (i.e. as a minimal rewrite of the original FIGURE-based GUI). Bad idea. Finding UIGRIDLAYOUT meant that I very quickly rewrote the relevant code and bingo! Easier and better.
"...a lack of visual customization options"
I have not tried such color properties, so cannot comment.
Aaron
Aaron 2024 年 9 月 18 日
Hi Stephen,
Thanks again for your insights. I'll likely be returning to your iregexp code as an example for uifigure in the future.
I also chanced on uigridlayout when I was planning my current project, but have already written my own bespoke grid layout function which allows a "paint-by-numbers"-style Excel sheet to define a grid layout in "normalized" units. This is of course probably a worse implementation of whatever uigridlayout does behind the scenes, but it's fun to use and lets me visually edit the grid in familiar software.
I actually was able to get my autocomplete working exactly as intended with "KeyPressFcn"; see my answer for the functional code. I just needed to add a step to unfocus and focus the text box every time it autocompletes.

サインインしてコメントする。

採用された回答

Aaron
Aaron 2024 年 9 月 18 日
編集済み: Aaron 2024 年 9 月 18 日
With @Walter Roberson's note that the "String" property of an "edit"-style uicontrol can't be visually updated without the box losing focus, I was able to modify the code to properly autocomplete.
I did this by adding an additional dummy uicontrol and toggling focusing between the two at the beginning of the autocomplete procedure.
Working autocomplete code:
% Initial setup ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
% Set allowed strings
str = ["FOO", "BAR", "BAZ", "QUX", "FOOBAR", "BAZQUX", "FARBOO", "QUAIL", "QIT"];
% Set up figure
f = figure;
e1 = uicontrol('Style', 'edit', 'String', str(3), 'Callback', @cb, ...
'KeyPressFcn', @kp, 'Interruptible', 'off', 'BusyAction', 'cancel', 'Tag', 'e1');
e2 = uicontrol('Style', 'edit', 'Position', [200, 200, 100, 100], 'Tag', 'e2');
% Set up guidata
data.AllowedStrings = str; % Store allowed strings
data.EditBoxString = e1.String; % Store a copy of the current string
data.AutoComplete.String = []; % Autocomplete: initial string
data.AutoComplete.Counter = 1; % Autocomplete: which completion are we on?
guidata(f, data);
% Functions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
% Callback: Data Validation
function cb(src, ~)
% Get guidata
data = guidata(src);
% Check supplied string against valid strings
val = erase(lower(src.String), [" ", ":", "-", "(", ")"]) == ...
lower(data.AllowedStrings);
% If invalid, reset the string to its previous value and return
if ~nnz(val)
src.String = data.EditBoxString;
return;
end
% Set editbox string to validated string and repack data
src.String = data.AllowedStrings(val);
data.EditBoxString = data.AllowedStrings(val);
guidata(src, data);
end
% KeyPressFcn: "Autocomplete" on leftbracket push
function kp(src, k)
% Get guidata & handles
data = guidata(src);
hndl = guihandles(src);
% Autocomplete if "leftbracket" was pressed
if k.Key == "leftbracket"
% Unfocus and focus to fix the issue!
uicontrol(hndl.e2)
uicontrol(hndl.e1)
% Get autocomplete string if none exists
if isempty(data.AutoComplete.String)
data.AutoComplete.String = erase(lower(src.String), ...
["[", " ", ":", "-", "(", ")"]);
end
% Get possible completions using "startsWith"
data.AutoComplete.Options = find(startsWith(...
lower(data.AllowedStrings), data.AutoComplete.String));
% Set string
if ~isempty(data.AutoComplete.Options)
% Set string
src.String = data.AllowedStrings(...
data.AutoComplete.Options(data.AutoComplete.Counter));
% Iterate counter
data.AutoComplete.Counter = mod(data.AutoComplete.Counter, ...
length(data.AutoComplete.Options)) + 1;
end
else
% Set autocomplete string to empty & reset counter
data.AutoComplete.String = [];
data.AutoComplete.Counter = 1;
end
% Repack guidata
guidata(src, data);
end

その他の回答 (1 件)

Walter Roberson
Walter Roberson 2024 年 9 月 17 日
編集済み: Walter Roberson 2024 年 9 月 17 日
Changing the String property of a uicontrol is not reflected on the display until the user presses Enter or moves the focus away from the control. The String property changes, but the display is not updated. Therefore, you will not be able to do pseudo autocomplete without the user continually pressing enter or clicking elsewhere.
  3 件のコメント
Walter Roberson
Walter Roberson 2024 年 9 月 17 日
If you unfocus and refocus then you lose the internal positioning of the cursor for the cases where the user edits in the middle of a string.
Aaron
Aaron 2024 年 9 月 17 日
If I'm cycling through autocomplete options, I don't particularly care if my cursor has been reset, since the text has been edited upon autocompletion.
I would be happy enough to make that concession, but it seems like the behavior is still not functioning as expected.

サインインしてコメントする。

カテゴリ

Help Center および File ExchangeMigrate GUIDE Apps についてさらに検索

製品


リリース

R2024a

Community Treasure Hunt

Find the treasures in MATLAB Central and discover how the community can help you!

Start Hunting!

Translated by