Code covered by the BSD License  

Highlights from
Brick Toolbox

image thumbnail
from Brick Toolbox by Thomas Deneux
The Brick Toolbox is a set of utility functions for Matlab.

fn_control
classdef fn_control < hgsetget
    % function X = fn_control(s[,fun][,spec][,hparent][,'okbutton|nobutton')
    % function fn_control('demo')
    %---
    % cluster of controls representing the structure s (changing the
    % control changes the values in X, and changing the values in X
    % automatically updates the control display)
    %
    % Input:
    % - s           structure to intialize X
    % - fun         function with prototype @(s)fun, which will be called
    %               by X
    % - spec        structure with additional information on the aspect and
    %               behavior of the controls (see below); it should have
    %               the same fields as s (though some fields can be
    %               omitted)
    % - hp          parent figure or uipanel where to create the controls
    %               (a new figure is created if not specified)
    % - 'okbutton' or 'nobutton'
    %               specifically specify to have an ok button or no button
    %
    % Output:
    % - X           a fn_control object, which can be manipulated using
    %               usual structure syntax
    % 
    % Possible values for the fields of spec:
    % []            automatic guess how to display the control
    % 'logical'     check box
    % 'multcheck [n]' array of n check boxes
    % {'str1','str2',...}       
    %               list box with specified string values
    % {'list|radio|button' 'str1','str2',...}       
    %               specification of the type of display [default: list]
    %               for a choice between string values
    % 'char [n [nlin]]'    
    %               input for string, if n is specified, sets the minimal
    %               length of the input in number of characters, otherwise,
    %               minimal length is set according to the value in s
    %               if nlin is specified, control occupies nlin lines
    %               instead of 1
    % 'double [n]'  input for numerical array
    % 'single [n]'  input for numerical array
    % 'slider min max [step] [format]'
    %               slider, specify min, max, step (optional) and format of the
    %               string representation (optional)
    % 'logslider min max [step] [format]'
    %               logarithmic scale slider (min and max should be the log of
    %               the effective min and max)
    % 'loglogslider min max [step] [format]'
    %               logarithmic scale slider, with possibility to select
    %               also a negative number
    % 'stepper [n [min [max [step [format]]]]]'
    %               input for n double
    %               if n>1, it is possible to define n values for min, max,
    %               step, separated by commas, for example: 0,-Inf,-1
    % 'clip'        input for 2-elements vector (usually, min and max);
    %               move the mouse in the control area to change the value
    % 'xdouble, xsingle, xchar [n], x[log[log]]slider min max [..],
    % xstepper, xclip'
    %               additional display of a check box: value will be empty
    %               if the box is not checked
    %               it is possible to specify a default value inside
    %               brackets at the end of the flag, for example:
    %               'xchar 12 [yes we can]' (here the brackets do not mean
    %               that this default value is optional, but they must
    %               appear in the string)
    % 'file|dir'    button for selecting file name / directory name
    % 
    % See also fn_structedit
    
    % Thomas Deneux
    % Copyright 2007-2012
    
    properties
        fun
        immediateupdate
    end
    properties (SetAccess='private')
        mode              % which special buttons: 'none', 'ok', 'execfun' 
        controls
        names
        entries
        hp
        himupd
        fignew
        changedfields
    end
    
    properties (Dependent)
        s
    end
    
    events
        OK
    end
    
    % Constructor
    methods
        function X = fn_control(s,varargin)
            
            % demo
            if ischar(s) && strcmp(s,'demo')
                [s varargin] = demo;
            end
            
            % Input
            X.hp = []; spec = struct;
            % (original structure)
            if ~isscalar(s)
                if length(s)==2
                    spec = s(2); s = s(1);
                else
                    error('input structure must be scalar or have 2 elements')
                end
            end
            % (other inputs)
            for i=1:length(varargin)
                a = varargin{i};
                if ischar(a)
                    % flag for the presence of an 'ok' button
                    X.mode = fn_switch(a, ...
                        {'ok' 'okbutton'},  'ok', ...
                        'nobutton',         'none');
                elseif isa(a,'function_handle')
                    % callback function
                    X.fun = a;
                elseif isstruct(a)
                    % specifications
                    spec = a;
                elseif ishandle(a) || fn_isfigurehandle(a)
                    % parent object (figure or panel)
                    X.hp = a;
                else 
                    error argument
                end
            end
            % (some initializations)
            if isempty(X.mode)
                X.mode = fn_switch(isempty(X.fun),'none','execfun');
            end
            X.fignew = isempty(X.hp);
            X.names = fieldnames(s)';
            X.entries = struct;
            nf = length(X.names);
            
            % Make a general structure with a lot of information
            X.controls = struct('name',{},'type',{},'style',{},'value',{}, ...
                'default',{},'defaultcheck',{},'defaultstring',{}, ...
                'n_name',{},'n_val',{},'n_line',{},'hname',{},'hval',{}, ...
                'check',{},'log',{},'min',{},'max',{},'step',{},'shift',{},'format',{}, ...
                'mode',{},'values',{} ...
                );
            
            % Loop on fields: fill the structure with all necessary
            % parameters but do not display the controls yet
            for k=1:nf
                f = X.names{k};
                X.entries.(f) = k;
                X.controls(k).name = f;
                xk = X.controls(k);
                xk.value = s.(f);
                if isfield(spec,f), opt = spec.(f); else opt = []; end

                % field name
                xk.name = f;
                xk.n_name = length(f);
                
                % default specification when not specified
                if isempty(opt)
                    opt = fn_switch(class(xk.value), ...
                        'logical',  'logical', ...
                        'double',   'double', ...
                        'single',   'single', ...
                        'char',     'char');
                end
                
                % value type and control style
                if iscell(opt)
                    % (list of strings)
                    xk.type = 'char';
                    xk.check = false;
                    if ismember(opt{1},{'list' 'radio' 'button'})
                        % format {'style','str1','str2',...}
                        xk.style = fn_switch(opt{1}, ...
                            'list',     'popupmenu', ...
                            'radio',    'radiobutton', ...
                            'button',   'togglebutton');
                        opt(1) = []; spec.(f) = opt;
                    else
                        % format {'str1','str2',...}, default style is 'list'
                        xk.style = 'popupmenu';
                    end
                    xk.defaultcheck = true;
                else
                    % (string describing the control)
                    % check first flag
                    tmp = regexp(opt,'^[^ ]*','match'); tmp = tmp{1};
                    % check box?
                    if tmp(1) == 'x'
                        xk.check = true;
                        tmp(1) = [];
                        % initial checking of the box
                        xk.defaultcheck = ~isempty(xk.value);
                    else
                        xk.check = false;
                        xk.defaultcheck = true;
                    end
                    % define value type and display style
                    switch tmp
                        case 'logical'
                            xk.type = 'logical';
                            xk.check = true;
                            xk.style = 'checkbox';
                        case 'multcheck'
                            xk.type = 'logical';
                            xk.style = 'multcheck';
                        case {'slider' 'logslider' 'loglogslider'}
                            xk.type = 'double';
                            xk.log = sum(logical(findstr(tmp,'log'))); % count how many 'log'
                            xk.style = 'slider';
                        case 'stepper'
                            xk.type = 'double';
                            xk.style = 'stepper';
                        case 'clip'
                            xk.type = 'double';
                            xk.style = 'sensor';
                            xk.mode = 'clip';
                        case {'file','dir'}
                            xk.type = 'char';
                            xk.style = 'file';
                            xk.mode = fn_switch(tmp,'file','SAVE','dir','DIR');
                        case {'char' 'double' 'single'}
                            xk.type = tmp;
                            xk.style = 'edit';
                        otherwise
                            error('unknown control flag: ''%s''',tmp)
                    end
                    % initial (default) value for unchecked check box
                    idx = find(opt=='[');
                    if ~xk.defaultcheck && ~isempty(idx)
                        % for the moment, we keep a string for the
                        % default value, which will be converted later
                        % to the appropriate type
                        str = opt(idx+1:end-1);
                        xk.default = str2val(str,xk.type);
                    end
                    opt(idx:end) = [];
                end
                
                % initial value, control width, and some other
                % style-specific parameters
                xk.n_line = 1;
                switch xk.style
                    case 'checkbox'
                        % logical value - only check box
                        xk.n_val = 0;
                        if isempty(xk.value)
                            xk.default = false;
                        else
                            xk.default = logical(xk.value);
                        end
                    case 'multcheck'
                        % read options
                        answer = regexp(opt,'([^ ]*)','tokens');
                        if isempty(xk.value)
                            if length(answer)==2
                                xk.mode = str2double(answer{2}); % number of check boxes
                            else
                                xk.mode = 1;
                            end
                            xk.default = false(1,xk.mode);
                        else
                            if length(answer)==2 && str2double(answer{2})~=length(xk.value)
                                error('multcheck length specification does not match with value')
                            end
                            xk.mode = length(xk.value);
                            xk.default = xk.value;
                        end
                        % width
                        xk.n_val = 3*xk.mode; % not clear what is the minimum width
                        xk.format = '%i';
                    case {'popupmenu' 'radiobutton' 'togglebutton'}
                        % (list of strings)
                        if isempty(xk.value)
                            xk.default = 1;
                        else
                            tmp = find(strcmp(xk.value,opt));
                            if isempty(tmp)
                                xk.default = 1;
                            else
                                xk.default = tmp;
                            end
                        end
                        xk.values = opt;
                        tmp = strvcat(opt{:}); %#ok<VCAT>
                        switch xk.style
                            case 'popupmenu'
                                xk.n_val = size(tmp,2)*.7;
                            case 'togglebutton'
                                xk.n_val = length(opt) + numel(tmp)*.7;
                            case 'radiobutton'
                                xk.n_val = 4*length(opt) + numel(tmp)*.7;
                        end
                    case 'slider'
                        % read options
                        answer = regexp(opt,'([^ ]*)','tokens');
                        answer = [answer{:}];
                        if length(answer)>=3
                            xk.min = str2double(answer{2});
                            xk.max = str2double(answer{3});
                        else
                            xk.min = fn_switch(xk.value>0,0,2*xk.value);
                            xk.max = fn_switch(xk.value==0,1,2*abs(xk.value));
                        end
                        xk.step = 0;
                        xk.format = [];
                        for i=4:length(answer)
                            str = answer{i};
                            if findstr(str,'%')
                                xk.format = str;
                            else
                                xk.step = str2double(str);
                            end
                        end
                        if isempty(xk.format)
                            if xk.step>0 && ~mod(xk.step,1)
                                xk.format = '%.0f';
                            else
                                xk.format = '%.1f';
                            end
                        end
                        if xk.log==2
                            xk.shift = xk.min;
                            xk.max = xk.max-xk.min;
                            xk.min = -xk.max;
                        end
                        % initial value
                        if xk.defaultcheck
                            if ~isnumeric(xk.value) || ~isscalar(xk.value)
                                error('wrong default value in the context of  slider style')
                            end
                            switch xk.log
                                case 0
                                    xk.default = xk.value;
                                case 1
                                    xk.default = log10(xk.value);
                                case 2
                                    % complicate
                                    xk.default = x2log(xk.value,xk.shift);
                            end
                        elseif isempty(xk.default)
                            if xk.log==2
                                xk.default = 0;
                            else
                                xk.default = xk.min;
                            end
                        end
                        % increase the field length according to format
                        test1 = num2str(xk.max+rand,xk.format);
                        test2 = num2str(xk.min+rand,xk.format);
                        xk.n_name = xk.n_name + 2 + max(length(test1),length(test2)) + 1;
                        % second column
                        xk.n_val = 6;
                    case 'stepper'
                        % read options
                        defans = {'stepper' '1' '-Inf' 'Inf' '1' '%.2g'};
                        answer = regexp(opt,'([^ ]*)','tokens');
                        answer = [answer{:}];
                        missing = (length(answer)+1:length(defans));
                        answer(missing) = defans(missing);
                        xk.mode = str2double(answer{2}); % number of numeric values
                        xk.min  = str2num(['[' strrep(answer{3},',',' ') ']']); %#ok<ST2NM>
                        xk.max  = str2num(['[' strrep(answer{4},',',' ') ']']); %#ok<ST2NM>
                        xk.step = str2num(['[' strrep(answer{5},',',' ') ']']); %#ok<ST2NM>
                        xk.format = answer{6};
                        % initial value
                        if xk.defaultcheck
                            xk.default = xk.value; 
                        elseif isempty(xk.default)
                            xk.default = zeros(1,xk.mode);
                        end
                        % width
                        xk.n_val = 5*xk.mode; % not clear what is the minimum width
                    case 'sensor'
                        if xk.defaultcheck
                            xk.default = xk.value; 
                        elseif isempty(xk.default)
                            xk.default = [0 1]; % TODO: check this
                        end
                        xk.n_val = 10; % not clear what is the minimum width
                    case 'edit'
                        if xk.defaultcheck
                            xk.default = val2str(xk.value); 
                        elseif isempty(xk.default)
                            xk.default = '';
                        end
                        tmp = regexp(opt,'[^ ]*','match');
                        if length(tmp)>=2 % length is specified
                            xk.n_val = str2double(tmp{2});
                        else
                            xk.n_val = max(4,length(xk.default));
                        end
                        if strcmp(xk.type,'char') && length(tmp)>=3
                            nlin = str2double(tmp{3});
                            if nlin<=0 || mod(nlin,1), error('number of lines must be a positive integer'), end
                            xk.n_line = nlin;
                            xk.n_val = xk.n_val+8;
                        end
                    case 'file'
                        if ~ischar(xk.value)
                            xk.value = '';
                        end
                        xk.default = xk.value;
                        xk.n_val = 20;
                end
                X.controls(k) = xk;
            end
            
            % Width of the two columns
            idx = logical([X.controls.n_val]); % ignore fields which do not have two columns
            if ~X.fignew, fsz = get(X.hp,'defaultuicontrolfontsize'); else fsz=10; end
            if fsz==8 || (fsz==10 && strcmp(computer,'MACI64'))
                n1 = [X.controls(idx).check]*20 + 5 + [X.controls(idx).n_name]*5;
                n2 = 20 + [X.controls(idx).n_val]*3;
            elseif fsz==10
                n1 = [X.controls(idx).check]*20 + [X.controls(idx).n_name]*8;
                n2 = 20 + [X.controls(idx).n_val]*8;
            end
            A = max(n1);
            B = max(n2);
            
            % Position parameters 
            D = 5; E = 10;
            G = 45; L = 80;
            K = 20; T = 17;
            nbut = sum([X.controls.n_line]) + ~strcmp(X.mode,'none');
            if X.fignew
                % figure
                ss = get(0,'screensize');
                W = max(D+A+D+B+D,D+G+D+L+D);
                H = E+nbut*(K+E);
                H = H+20; % BUG with Exceed
                p = get(0,'pointerLocation');
                LEFT   = min(max(p(1)-W/2,20),ss(3)-W-20);
                BOTTOM = min(max(p(2)-H/2,50),ss(4)-H-20);
                posp = [LEFT BOTTOM W H];
                X.hp = figure('numbertitle','off','name','Edit parameters', ...
                    'menubar','none', ...
                    'position',posp, ...
                    'defaultuicontrolhorizontalalignment','left');
                ncol = 1;
                nlin = nbut;
            else
                % check size of parent
                switch get(X.hp,'type')
                    case 'figure'
                        posp = get(X.hp,'position');
                    case 'uipanel'
                        oldunit = get(X.hp,'units');
                        set(X.hp,'units','pixel')
                        posp = get(X.hp,'position');
                        set(X.hp,'units',oldunit);
                    otherwise
                        error('containter must be a figure or a uipanel object')
                end
                Z = max(A+D+B+D, ...                                        % maximal width of normal buttons
                    fn_switch(X.mode,'execfun',G+D+L+D,'ok',G+D,'none',0)); % width of special button
                Y = K+E;
                xrep = (posp(3)-D)/Z;
                yrep = (posp(4)-D)/Y;
                if floor(xrep)*floor(yrep)>=nbut
                    % it fits
                    ncol = ceil(nbut/floor(yrep));
                    nlin = ceil(nbut/ncol);
                    B = (posp(3)-D-ncol*(A+D+D))/ncol;
                elseif floor(xrep)*floor(yrep/.7)>=nbut
                    % it fits when squeezed a bit vertically
                    ncol = floor(xrep);
                    nlin = ceil(nbut/ncol);
                    B = (posp(3)-D-ncol*(A+D+D))/ncol;
                    K = 17;
                    E = (posp(4)-nlin*K)/(nlin+1);
                else
                    error('cannot fit the buttons inside the container')
                end
            end
            delete(get(X.hp,'children'))
            set(X.hp,'DeleteFcn',@(hp,evnt)delete(X))
            
            bgcol = get(X.hp,fn_switch(get(X.hp,'type'),'figure','color','uipanel','backgroundcolor'));
            set(X.hp,'defaultuicontrolunits','pixel')
            X.changedfields = {};
            H = posp(4);
            Z = max(A+D+B+D, ...                                        % maximal width of normal buttons
                fn_switch(X.mode,'execfun',G+D+L+D,'ok',G+D,'none',0)); % width of special button
            Y = K+E;
            
            % Controls
            ipos = 1;
            for k=1:nf
                xk = X.controls(k);
                icol = 1+floor((ipos-1)/nlin);
                ilin = ipos-(icol-1)*nlin;
                ipos = ipos + xk.n_line; % position for next control
                if ~xk.n_val
                    % first column only (checkbox)
                    xk.hname = uicontrol('style','checkbox','string',xk.name, ...
                        'value',xk.default, ...
                        'parent',X.hp,'backgroundcolor',bgcol, ...
                        'position',[D+(icol-1)*Z H-ilin*Y A+D+B T], ...
                        'callback',@(hu,evnt)chgvalue(X,k,logical(get(hu,'value'))));
                else
                    % first column
                    xk.hname = uicontrol('style','text','string',xk.name, ...
                        'horizontalalignment','left', ...
                        'parent',X.hp,'backgroundcolor',bgcol, ...
                        'position',[D+(icol-1)*Z H-ilin*Y A T]);
                    if xk.check
                        set(xk.hname,'style','checkbox','value',xk.defaultcheck, ...
                            'callback',@(hu,evnt)chgvalue(X,k,logical(get(hu,'value'))));
                    end
                    if strcmp(xk.style,'slider') && (~xk.check || xk.defaultcheck)
                        set(xk.hname,'string',[xk.name ' (' num2str(xk.value,xk.format), ')'])
                    end
                    
                    % second column
                    switch xk.style
                        case 'popupmenu'
                            xk.hval = uicontrol('parent',X.hp,'style',xk.style, ...
                                'position',[D+(icol-1)*Z+A+D H-ilin*Y B K], ...
                                'string',xk.values,'value',xk.default, ...
                                'callback',@(hu,evnt)chgvalue(X,k,xk.values{get(hu,'value')}));
                        case {'radiobutton' 'togglebutton'}
                            compactstyle = fn_switch(xk.style, ...
                                'radiobutton',  'radio', ...
                                'togglebutton', 'toggle');
                            xk.hval = fn_buttongroup(compactstyle,xk.values, ...
                                @(x)chgvalue(X,k,x), ...
                                'parent',X.hp, ...
                                'units','pixel','position',[D+(icol-1)*Z+A+D H-ilin*Y B K], ...
                                'value',xk.default);
                        case 'multcheck'
                            xk.hval = fn_multcheck('parent',X.hp, ...
                                'position',[D+(icol-1)*Z+A+D H-ilin*Y B K], ...
                                'value',xk.default,'callback', ...
                                @(hu,evnt)chgvalue(X,k,get(hu,'value')));
                        case 'slider'
                            xk.hval = fn_slider('parent',X.hp,'mode','point', ...
                                'position',[D+(icol-1)*Z+A+D H-ilin*Y B K], ...
                                'min',xk.min,'max',xk.max,'width',.1, ...
                                'value',xk.default,'callback', ...
                                @(hu,evnt)chgvalue(X,k,get(hu,'value')));
                            if xk.step
                                % note that 'inc' and 'width are relative values (betw.
                                % 0 and 1, not betw. min and max)
                                inc = xk.step/(xk.max-xk.min);
                                set(xk.hval,'inc',inc,'width',inc)
                            end
                        case 'stepper'
                            xk.hval = fn_stepper('parent',X.hp, ...
                                'position',[D+(icol-1)*Z+A+D H-ilin*Y B K], ...
                                'min',xk.min,'max',xk.max,'step',xk.step, ...
                                'format',xk.format, ...
                                'value',xk.default,'callback', ...
                                @(hu,evnt)chgvalue(X,k,get(hu,'value')));
                        case 'sensor'
                            xk.hval = fn_sensor('parent',X.hp,'mode',xk.mode, ...
                                'backgroundcolor',[.5 .6 .6], ... 
                                'position',[D+(icol-1)*Z+A+D H-ilin*Y B K], ...
                                'value',xk.default, ...
                                'format','%.4g', ...
                                'callback',@(hu,evnt)chgvalue(X,k,get(hu,'value')));
                        case 'edit'
                            xk.hval = uicontrol('parent',X.hp,'style',xk.style, ...
                                'position',[D+(icol-1)*Z+A+D H-(ilin+xk.n_line-1)*Y B K+(xk.n_line-1)*Y], ...
                                'horizontalalignment','left', ...
                                'max',xk.n_line, ... % allow multiple lines if n_line>1
                                'string',val2str(xk.default),'backgroundcolor','w', ...
                                'callback',@(hu,evnt)chgvalue(X,k,str2val(get(hu,'string'),xk.type)));
                        case 'file'
                            xk.hval = uicontrol('parent',X.hp,'style','edit', ...
                                'position',[D+(icol-1)*Z+A+D H-ilin*Y B K], ...
                                'string',xk.default,'enable','inactive', ...
                                'buttondownfcn',@(hu,evnt)chgvalue(X,k,fn_getfile(xk.mode)));
                        otherwise
                            error programming
                    end
                end
                X.controls(k) = xk;
            end
            
            % Special action buttons
            switch X.mode
                case 'execfun'
                    X.immediateupdate = true;
                    uicontrol('style','pushbutton','string','Update','parent',X.hp, ...
                        'position',[D+ncol*Z-D-L-D-G H-nlin*Y G K], ...
                        'callback',@(hu,evnt)evalfun(X))
                    X.himupd = uicontrol('style','radiobutton','string','immediate', ...
                        'value',X.immediateupdate, ...
                        'parent',X.hp,'backgroundcolor',bgcol, ...
                        'position',[D+ncol*Z-D-L H-nlin*Y L K], ...
                        'callback',@(hu,evnt)set(X,'immediateupdate',get(hu,'value')));
                case 'ok'
                    uicontrol('style','pushbutton','string','OK', ...
                        'position',[W/2-G/2 D G K], ...
                        'callback',@(u,evnt)okpress);
                case 'none'
                    % no special buttons
                    X.immediateupdate = ~isempty(X.fun);
            end
            assignin('base','X',X)
            
            % nested function for when the OK button is pressed
            function okpress
                notify(X,'OK')
                % delete the controls now
                if X.fignew
                    close(X.hp)
                else
                    delete(get(X.hp,'children'))
                end
            end
            
        end
    end
    
    % Get/Set
    methods
        function s = get.s(X)
            c = [X.names; {X.controls.value}];
            for k=1:length(X.controls), if iscell(c{2,k}), c{2,k} = {c{2,k}}; end, end %#ok<CCAT1>
            s = struct(c{:});
        end
        function set.immediateupdate(X,value)
            set(X.himupd,'value',value) %#ok<*MCSUP>
            X.immediateupdate = value;
        end        
    end
    
    % Referencing, assignment, display
    methods
        function x = subsref(X,f)
            switch f(1).type
                case '()'
                    x = X(f(1).subs{:});
                case '.'
                    k = strcmp(X.names,f(1).subs);
                    if ~any(k)
                        x = X.(f(1).subs);
                    else
                        x = X.controls(k).value;
                    end
                otherwise
                    error('wrong referencing of fn_control object')
            end
            if length(f)>1, x = subsref(x,f(2:end)); end
        end
        function X = subsasgn(X,f,x)
            switch f(1).type
                case '()'
                    if length(f)>1
                        subsassgn(X(f(1).sub{:}),f(2:end),x)
                    else
                        X(f(1).subs{:}) = x;
                    end
                case '.'
                    k = strcmp(X.names,f(1).subs);
                    if ~any(k)
                        if length(f)>1
                            X.(f(1).subs) = subsassgn(X.(f(1).subs),f(2:end),x);
                        else
                            X.(f(1).subs) = x;
                        end
                    else
                        if length(f)>1
                            X.controls(k).value = subsassgn(X.controls(k).value,f(2:end),x);
                        else
                            X.controls(k).value = x;
                        end
                        updatecontrol(X,k)
                    end
                otherwise
                    error('wrong referencing of fn_control object')
            end
        end
        function disp(X)
            disp(X.s)
        end
    end
    
    % Routines
    methods
        function evalfun(X)
            if ~isempty(X.fun), feval(X.fun,X.s), end
            if isvalid(X) && ishandle(X.hp), X.changedfields = {}; end
        end
        function chgvalue(X,k,val)
            % callback function executed when control k has been moved
            xk = X.controls(k);
            % check the box if we moved the adjacent control
            if xk.check && ~strcmp(xk.style,'checkbox')
                if islogical(val)
                    if val
                        if strcmp(xk.style,'edit')
                            val = str2val(get(xk.hval,'string'),xk.type);
                        else
                            val = get(xk.hval,'value');
                        end
                    else
                        if strcmp(xk.type,'char')
                            val = '';
                        else
                            val = [];
                        end
                    end
                else
                    % check the box
                    set(xk.hname,'value',true)
                end
            end
            % special action
            switch xk.style
                case 'slider'
                    % logarithmic value
                    switch xk.log
                        case 0
                            % value is ok
                        case 1
                            val = 10^val;
                        case 2
                            % complicate
                            val = log2x(val,xk.shift);
                    end
                    % text update
                    if isempty(val)
                        set(xk.hname,'string',xk.name)
                    else
                        set(xk.hname,'string', ...
                            [xk.name ' (' num2str(val,xk.format) ')'])
                    end
                case 'file'
                    if isequal(val,0), return, end
                    set(xk.hval,'string',val)
            end
            % store the information on which fields were changed
            X.changedfields = union(X.changedfields,xk.name);
            % update value and eval function
            X.controls(k).value = val;
            if X.immediateupdate
                evalfun(X)
            end
        end
        function updatecontrol(X,k)
            % update display of control k (normally, upon a change of the
            % related value)
            xk = X.controls(k);
            % check the box according to whether the value is empty
            if xk.check && ~strcmp(xk.style,'checkbox')
                set(xk.hname,'value',~isempty(xk.value))
            end
            % change value display
            switch xk.style
                case 'checkbox'
                    % logical
                    set(xk.hname,'value',xk.value)
                case {'popupmenu' 'radiobutton' 'togglebutton'}
                    val = xk.value;
                    if ischar(val), val = find(strcmp(xk.values,xk.value)); end
                    if isempty(val), error('value ''%s'' does not exist',xk.value), end
                    set(xk.hval,'value',val);
                case 'multcheck'
                    set(xk.hval,'value',xk.value)
                case 'slider'
                    if isempty(xk.value)
                        set(xk.hname,'string',xk.name)
                    else
                        set(xk.hname,'string',[xk.name ...
                            ' (' num2str(xk.value,xk.format), ')'])
                        switch xk.log
                            case 0
                                val = xk.value;
                            case 1
                                val = log10(xk.value);
                            case 2
                                % complicate
                                val = x2log(xk.value,xk.shift);
                        end
                        set(xk.hval,'value',val);
                        % update in case of coercing actions!
                        val = get(xk.hval,'value');
                        switch xk.log
                            case 0
                                X.controls(k).value = val;
                            case 1
                                X.controls(k).value = 10^val;
                            case 2
                                % complicate
                                X.controls(k).value = log2x(val,xk.shift);
                        end
                    end
                case {'stepper','sensor'}
                    if ~isempty(xk.value)
                        set(xk.hval,'value',xk.value)
                    end
                case 'edit'
                    set(xk.hval,'string',val2str(xk.value));
                case 'file'
                    set(xk.hval,'string',xk.value);
                otherwise
                    error programming
            end
        end
    end
        
    % Misc
    methods
        function access(X) %#ok<MANU>
            keyboard
        end
    end
end


%---
function [str type] = val2str(val)

type = class(val);
switch type
    case 'char'
        str = val;
    case {'double','single','logical'}
        if ndims(val)>2 || numel(val)>15
            error('cannot display array with more than 2 dimensions or 15 elements')
        end
        str = num2str(val);
        str(:,end+1) = ';'; str(:,end+1) = ' ';
        str = reshape(str',1,numel(str));
        str(end-1:end) = [];
        str = regexprep(str,' *',' ');
    case 'cell'
        % special! cell array of strings
        type = 'char';
        str = val;
    otherwise
        error('cannot display object of class ''%s''',type)
end

end

%---
function val = str2val(str,type)

switch type
    case 'char'
        val = str;
    case {'double','logical'}
        val = eval(['[' str ']']);
    otherwise
        error programming
end

end

%---
function val = x2log(x,shift)

if x==0
    val=0;
elseif x>0
    val = max(0,log(x)-shift);
else
    val = min(0,-(log(-x)-shift));
end

end

%---
function x = log2x(val,shift)

if val==0
    x = 0;
elseif val>0
    x = 10^(val+shift);
else
    x = -10^(-val+shift);
end

end

%---
function [s args] = demo %#ok<STOUT>

C = {'s = struct(''a'',false,''b'',1,''c'',2,''d'',''hello'',''e'',[0 1],''f'',pwd);'
    'spec = struct(''c'',''xslider 0 10 1'',''d'',{{''hello'',''yo''}},''e'',''clip'',''f'',''dir'');'
    'myfun = @disp;'
    'fn_control(s,spec,myfun);'};
for k=1:4, disp(C{k}), end
for k=1:3, evalin('base',C{k}), eval(C{k}), end
args = {spec myfun};

end

Contact us