Matlab GUI: Numeric input for serious work

Design goals

Need GUI element with the following properties:

editfloat in action

Why a JSpinner doesn't cut it

You can easily create Java Swing objects in your GUI, and the JSpinner is pretty close to what I need. But: To my knowledge, it does not support incrementing/decrementing the value based on the current caret position. The caret always jumps to the beginning of the string.

Example:

f = figure;
jSpinner = javaObjectEDT('javax.swing.JSpinner');
javacomponent(jSpinner);

JSpinner

About "editfloats"

I hacked on edittext fields until they became a bit like JSpinner, but without the superfluous arrows on the right-hand side. Arrow up/arrow down increments/decrements the digit to the right of the caret position. Because the hack is based on edittexts, I call these fields editfloats. The format, minimum, maximum, default number, and default string properties are stored inside the 'UserData' property of the edittext.

The callbacks I needed are not accessible with the Matlab handles. Instead, I use the handles of the underlying Java swing components. Yair Altman wrote an excellent FindJObj function for finding those handles.

I use these edifloat elements in all my GUIs that I had made earlier (using GUIDE). It's just as easy to put them into GUIs that were created programmatically (without GUIDE). Caveat: Using java handles often needs the GUI to be visible. Therefore I placed the FindJObj function in GUIDE's autocreated ..._OutputFcn, which is called after the GUI is made visible. If you instead use the ..._OpeningFcn (called when the figure is invisible), the figure will be made visible/invisible multiple times, and figure creation will take a long time.

Preparations for creating editfloats

I moved the meaty stuff into external function files, because many of my GUIs and GUI elements will share this code.

The function editfloat_setup.m takes the handle of an edittext and sets up the appropriate Callbacks to make it an editfloat.

    function floatstr_edit_setup(h, options)
    % Takes the handle of an edit_text, and sets up callbacks such that it
    % becomes a float_edit box, with arrowup/arrowdown functionality.
    % Needs an options struct, which is attached to h as user_data. Must
    % contain fields:
    %  - .format       Format of the floating point number
    %  - .min          Minimum number
    %  - .max          Maximum Number
    %  - .default_num  Default Number (for bad input)
    %  - .default_str  Default String (for bad input)

    set(h, 'UserData', options) 
    set(h, 'FontName', 'Courier', 'FontSize', 10)
    % Sets callback, calls it to make sure string is in suitable format
    set(h,'Callback',@editfloat_Callback);
    editfloat_Callback(h, 0)

    % Note: 'persistent' fails when the GUI is closed and started again. You need to
    % restart Matlab in this case. I stopped using 'persistent'.
    % Sets up Callback of underlying Java Swing object.
    jh = findjobj(h, 'nomenu'); % external function by Yair Altman.
    set(jh,'KeyPressedCallback', ...
        {@editfloat_KeyPressedCallback_Java, h});

Because the user is explicity allowed to enter any text into the editfloat field, we need a function editfloat_Callback to make sure the user input is valid.

    function editfloat_Callback(hObject,~)
    % Called on "commit" event ("Enter" and "LostFocus" or so). 
    % Parses the element's input and updates the string with parsed input.

    edit_str = get(hObject, 'String');
    user_data = get(hObject, 'UserData');
    [~, clean_str] = editfloat_str_parser(edit_str, user_data);
    set(hObject, 'String', clean_str)

Arrow up/arrow down functionality happens inside the editfloat_KeyPressedCallback_Java Callback. This checks if the key pressed was an arrow up or arrow down key, and then increments/decrements the current value by a value that depends on the caret position.

    function editfloat_KeyPressedCallback_Java(jhObject, event, hObject)
    % Called on each button press inside edit text. 
    % Increments/decrements the value at the current caret position.
    % Needs the following fields in the "user_data" struct:
    %  - .format       Format of the floating point number
    %  - .min          Minimum number
    %  - .max          Maximum Number
    %  - .default_num  Default Number (for bad input)
    %  - .default_str  Default String (for bad input)

    keynum = get(event, 'keyCode'); 

    if keynum==38 || keynum==40  % arrow up or arrow down
        user_data = get(hObject, 'UserData');
        % format = user_data.format;
        caret_position = get(jhObject, 'CaretPosition') + 1; %matlab indexing
        current_str = get(jhObject, 'Text'); %using jhObject, because during edition, hObject contains old stuff
        [old_num, parsed_str] = editfloat_str_parser(current_str, user_data);
        
        if caret_position > length(parsed_str)
            % we're at the end of the string. do nothing
            return
        end
        
        if length(parsed_str) ~= length(current_str)
            % User was during input. Update field, return
            % Update: In the current version, this is impossible, I think
            set(hObject, 'String', parsed_str) 
            drawnow;
            return
        end
        
        % Prepare a delta_str -> delta_num from the given string and the caret
        % position. 
        numeric_period = int16('.');
        delta_str = parsed_str;
        delta_str(parsed_str ~= numeric_period) = '0';  % -> 00000.00000 or so
        
        new_num = old_num;
        if ~strcmp(delta_str(caret_position), '.')
            delta_str(caret_position) = '1';          % -> 00100.00000 or so
            delta_num = str2double(delta_str);
            if keynum == 38 % up
                new_num = new_num + delta_num;
            elseif keynum == 40 %down
                new_num = new_num - delta_num;
            end
        end
        
        new_str = sprintf(user_data.format, new_num);
        [~, new_str] = editfloat_str_parser(new_str, user_data);
        set(hObject, 'String', new_str) %using hObject here, because jhObject led to weird errors
        drawnow;   % otherwise set() takes place after the setCaretPosition!
        
        % Set correct caret position again. Adjust for possibly new string
        % length.
        length_difference = length(new_str) - length(current_str);
        jhObject.setCaretPosition(caret_position - 1+length_difference);
        drawnow;  % I was getting more errors. Maybe this helps?
    end

The fourth function you need is one that parses a string (usually the content of editfloat) and returns the number that the string represents together with a clean version of that string.

function [num, clean_str] = editfloat_str_parser(float_str, options)
% Converts float_str to a number. Returns this number as
% numeric and as a string.
%
% The input "options" must be a struct that contains the fields:
%  - .format       Output format of the number string.
%  - .min          Minimum number
%  - .max          Maximum Number
%  - .default_num  Default Number (for bad input)
%  - .default_str  Default String (for bad input)

% Example output_format: '%016.6f'
%   - 0 padded to field length
%   - 16 field length (including decimal point! -> 15 digits)
%   - 6 digits after the decimal points
%   - floating point


no_commas = strrep(float_str, ',', '.'); % having "," as decimal is fine
num = str2double(no_commas);
if isnan(num)  % if contained text
    num = options.default_num;
    clean_str = options.default_str;
    return
end
num = real(num); % only keep the real part
if num < options.min     num = options.min; elseif num > options.max
    num = options.max;
end
clean_str = sprintf(options.format, num);

Adding an editfloat to a GUI

% --- Outputs from this function are returned to the command line.
function varargout = Advanced_THEs_Plot_GUI_OutputFcn(hObject, eventdata, handles) 
% varargout  cell array for returning output args (see VARARGOUT);

% This function is executed just after the GUI is made visible. All the
% java swing object stuff needs the object to be visible, which is why the
% following code bits are here.
% Note: 
% Makes edittext into editfloats by setting up the right callbacks (for
% arrowup/arrowdown functionality)

% FreqOffset field
editfloat_options.format = '%12.3f';
editfloat_options.min = 0;
editfloat_options.max = 100e6-1e-3;
editfloat_options.default_num = 0;
editfloat_options.default_str = '0.000';
editfloat_setup(handles.FreqOffset_edittext, editfloat_options)

You're done!