Code covered by the BSD License  

Highlights from
Add an ROI toolbar to your figure

image thumbnail

Add an ROI toolbar to your figure

by

 

04 Apr 2013 (Updated )

This function adds an ROI toolbar to your figure.

draggable(h,varargin)
function draggable(h,varargin)
% DRAGGABLE - Make it so that a graphics object can be dragged in a figure.
%   This function makes an object interactive by allowing it to be dragged
%   accross a set of axes, following or not certain constraints. This
%   allows for intuitive control elements which are not buttons or other
%   standard GUI objects, and which reside inside an axis. Typical use
%   involve markers on an axis, whose position alters the output of a
%   computation or display
% 
%   >> draggable(h);
%   
%   makes the object with handle "h" draggable. Use the "Position" property
%   of the object to retrieve its position, by issuing a get(h,'Position')
%   command.
% 
%   If h is a vector of handles, then draggable is called on each handle
%   using the same following arguments, if any.
%
%   >> draggable(h,...,motionfcn)
%
%   where "motionfcn" is a function handle, executes the given function
%   while the object is dragged. Handle h is passed to motionfcn as an
%   argument. Argument "motionfcn" can be put anywhere after handle "h".
%
%   >> draggable(h,...,constraint,p);
%
%   enables the object with handle "h" to be dragged, with a constraint.
%   Arguments "constraint" (a string) and "p" (a vector) can be put
%   anywhere after handle "h".
%
%   >> draggable(h,...,'endfcn',endfcn);
%
%   where "endfcn" is a function handle, executes the given function AFTER
%   the object is dragged (more specifically, on the next WindowButtonUp 
%   event). The function handle must come after the string 'endfcn', to 
%   avoid ambiguity with the "motionfcn" argument (above). Handle h is 
%   passed to endfcn as an argument.
%
%   >> draggable(h,'off')
%
%   returns object h to its original, non-draggable state.
%
%   CONSTRAINTS
%
%   The argument "constraint" may be one of the following strings:
%
%       'n' or 'none':          The object is unconstrained (default).
%       'h' or 'horizontal':    The object can only be moved horizontally.
%       'v' or 'vertical':      The object can only be moved vertically.
%       'd' or 'diagonal':      The object can only be moved along an
%                               arbitrary line of a given slope.
%
%   The argument "p" is an optional parameter which depends upon the
%   constraint type:
%
%   Constraint      p                   Description
%   -----------------------------------------------------------------------
%
%   'none'          [x1 x2 y1 y2]       Drag range (for the object's outer
%                                       limits, from x1 to x2 on the x-axis
%                                       and from y1 to y2 on the y-axis).
%                                       Default is the current axes range.
%                                       Use "inf" if no limit is desired.
%
%   'horizontal'    [xmin xmax]         Drag range (for the object's outer
%                                       limits). Default is the x-axis
%                                       range. Use "inf" if no limit is
%                                       desired. Note that full limits of
%                                       the form [x1 x2 y1 y2] can also be
%                                       used.
%
%   'vertical'      [ymin ymax]         Drag range (for the object's outer
%                                       limits). Default is the y-axis
%                                       range. Use "inf" if no limit is
%                                       desired. Note that full limits of
%                                       the form [x1 x2 y1 y2] can also be
%                                       used.
%
%   'diagonal'      [m x1 x2 y1 y2]     Slope m of the line along which the
%                                       movement is constrained (default is
%                                       1); x1 x2 y1 y2 as in 'none'.
%
%   -----------------------------------------------------------------------
%



% VERSION INFORMATION:
% 2003-11-20:   Initially submitted to MatlabCentral.Com
% 2004-01-06:   Addition of the renderer option, as proposed by Ohad Gal
%               as a feedback on MatlabCentral.Com.
% 2004-02-18:   Bugfix: now works with 1-element plots and line objects
% 2004-03-04:   Bugfix: sanitized the way the object's new position is
%               computed; it now always follow the mouse even after the
%               mouse pointer was out of the axes.
% 2004-03-05:   Bugfix: movement when mouse is out of the axes is now
%               definitely correct ;)
% 2006-05-23:   Bugfix: fix a rendering issue using Matlab 7 & +
%               Deprecated the rendering options: rendering seems ok with
%               every renderer.
% 2010-01-11:   Bugfix by Gilles Fortin (odd jumping on limits caused by
%               round-off error due to successive addition then subtraction
%               of the same value)
% 2010-02-26:   endfcn code by Steven Bierer included.
%               Some suggested M-Lint fixes performed.
% 2012-01-18:   - Refactoring
%               - Limits of the form [x1 x2 y1 y2] can be used for 'h' and
%                 'v' constraint types;
%               - Support for text objects;
%               - Added diagonal constraint type
% 2012-01-20:   - Tested
%               - Added support for h as a vector of handles
%               - 'sliders' demo added in dragdemo
% 2013-01-10:   Bugfix: finding the figure's handle through gcbf in order 
%               to fix a bug when axes are embedded into a Panel. 
%               (Bug found by Esmerald Aliai)


% IMPLEMENTATION NOTES:
%
% This function uses the dragged object's "ButtonDownFcn" function and set
% it so that the objec becomes draggable. Any previous "ButtonDownFcn" is 
% thus lost during operation, but is retrieved after issuing the 
% draggable(h,'off') command.
%
% Information about the object's behavior is also stored in the object's
% 'UserData' property, using setappdata() and getappdata(). The original
% 'UserData' property is restored after issuing the draggable(h,'off')
% command.
%
% The corresponding figure's "WindowButtonDownFcn", "WindowButtonUpFcn" and
% "WindowButtonMotionFcn" functions.  During operation, those functions are
% set by DRAGGABLE; however, the original ones are restored after the user
% stops dragging the object.
%
% By default, DRAGGABLE also switches the figure's renderer to 'zbuffer'
% during operation: 'painters' is not fast enough and 'opengl' sometimes
% produce curious results. However there may be a need to switch to another
% renderer, so the user can now specify a specific figure renderer during
% object drag (thanks to Ohad Gal for the suggestion).
%
% The "motionfcn" function handle is called at each displacement, after the
% object's position is updated, using "feval(motionfcn,h)", where h is the
% object's handle.

% =========================================================================
% Copyright (C) 2003-2012
% Francois Bouffard
% fbouffard@gmail.com
% =========================================================================

% =========================================================================
% Input arguments management
% =========================================================================

% If h is a vector of handle, applying draggable on each object and
% returning.
if length(h) > 1
    for k = 1:length(h)
        draggable(h(k),varargin{:});
    end
    return
end

% Initialization of some default arguments
user_renderer = 'zbuffer';
user_movefcn = [];
constraint = 'none';
p = [];
user_endfcn = [];       % added by SMB (see 'for k' loop below)
endinput = 0;

% At least the handle to the object must be given
Narg = nargin;
if Narg == 0
    error('Not engough input arguments');
elseif numel(h)>1
    error('Only one object at a time can be made draggable');
end;

% Fetching informations about the parent axes
axh = get(h,'Parent');
if iscell(axh)
    axh = axh{1};
end;
%fgh = get(axh,'Parent'); % This fails if the axes are embedded in a Panel
fgh = gcbf; % This should always work
ax_xlim = get(axh,'XLim');
ax_ylim = get(axh,'YLim');

% Assigning optional arguments
Noptarg = Narg - 1;
for k = 1:Noptarg
   current_arg = varargin{k};
   if isa(current_arg,'function_handle') && endinput
       user_endfcn = current_arg; % added by SMB
       endinput = 0;              % 'movefcn' can still be a later argument
   elseif isa(current_arg,'function_handle')
       user_movefcn = current_arg;
   end;
   if ischar(current_arg);
       switch lower(current_arg)
           case {'off'}
               set_initial_state(h);
               return;
           case {'painters','zbuffer','opengl'}
               warning('DRAGGABLE:DEPRECATED_OPTION', ...
                       'The renderer option is deprecated and will not be taken into account');
               user_renderer = current_arg;
           case {'endfcn'} % added by SMB
               endinput = 1;
           otherwise
               constraint = current_arg;
       end;
   end;
   if isnumeric(current_arg);
       p = current_arg;
   end;
end;

% Assigning defaults for constraint parameter
switch lower(constraint)
    case {'n','none'}
        constraint = 'n';
        if isempty(p); p = [ax_xlim ax_ylim]; end;
    case {'h','horizontal'}
        constraint = 'h';
        if isempty(p) 
            p = ax_xlim;
        elseif length(p) == 4
            p = p(1:2);
        end
    case {'v','vertical'}
        constraint = 'v';
        if isempty(p)
            p = ax_ylim; 
        elseif length(p) == 4
            p = p(3:4);
        end
    case {'d','diagonal','l','locked'}
        constraint = 'd';
        if isempty(p)
            p = [1 ax_xlim ax_ylim]; 
        elseif length(p) == 1
            p = [p ax_xlim ax_ylim];
        end;
    otherwise
        error('Unknown constraint type');
end;

% =========================================================================
% Saving initial state and parameters, setting up the object callback
% =========================================================================

% Saving object's and parent figure's initial state
setappdata(h,'initial_userdata',get(h,'UserData'));
setappdata(h,'initial_objbdfcn',get(h,'ButtonDownFcn'));
setappdata(h,'initial_renderer',get(fgh,'Renderer'));
setappdata(h,'initial_wbdfcn',get(fgh,'WindowButtonDownFcn'));
setappdata(h,'initial_wbufcn',get(fgh,'WindowButtonUpFcn'));
setappdata(h,'initial_wbmfcn',get(fgh,'WindowButtonMotionFcn'));

% Saving parameters
setappdata(h,'constraint_type',constraint);
setappdata(h,'constraint_parameters',p);
setappdata(h,'user_movefcn',user_movefcn);
setappdata(h,'user_endfcn',user_endfcn);        % added by SMB
setappdata(h,'user_renderer',user_renderer);

% Setting the object's ButtonDownFcn
set(h,'ButtonDownFcn',@click_object);

% =========================================================================
% FUNCTION click_object
%   Executed when the object is clicked
% =========================================================================

function click_object(obj,eventdata)
% obj here is the object to be dragged and gcf is the object's parent
% figure since the user clicked on the object
setappdata(obj,'initial_position',get_position(obj));
setappdata(obj,'initial_extent',compute_extent(obj));
setappdata(obj,'initial_point',get(gca,'CurrentPoint'));
set(gcf,'WindowButtonDownFcn',{@activate_movefcn,obj});
set(gcf,'WindowButtonUpFcn',{@deactivate_movefcn,obj});
activate_movefcn(gcf,eventdata,obj);

% =========================================================================
% FUNCTION activate_movefcn
%   Activates the WindowButtonMotionFcn for the figure
% =========================================================================

function activate_movefcn(obj,eventdata,h)
% We were once setting up renderers here. Now we only set the movefcn
set(obj,'WindowButtonMotionFcn',{@movefcn,h});

% =========================================================================
% FUNCTION deactivate_movefcn
%   Deactivates the WindowButtonMotionFcn for the figure
% =========================================================================

function deactivate_movefcn(obj,eventdata,h)
% obj here is the figure containing the object
% Setting the original MotionFcn, DuttonDownFcn and ButtonUpFcn back
set(obj,'WindowButtonMotionFcn',getappdata(h,'initial_wbmfcn'));
set(obj,'WindowButtonDownFcn',getappdata(h,'initial_wbdfcn'));
set(obj,'WindowButtonUpFcn',getappdata(h,'initial_wbufcn'));
% Executing the user's drag end function
user_endfcn = getappdata(h,'user_endfcn');
if ~isempty(user_endfcn)
    feval(user_endfcn,h);           % added by SMB, modified by FB
end

% =========================================================================
% FUNCTION set_initial_state
%   Returns the object to its initial state
% =========================================================================

function set_initial_state(h)
initial_objbdfcn = getappdata(h,'initial_objbdfcn');
initial_userdata = getappdata(h,'initial_userdata');
set(h,'ButtonDownFcn',initial_objbdfcn);
set(h,'UserData',initial_userdata);

% =========================================================================
% FUNCTION movefcn
%   Actual code for dragging the object
% =========================================================================

function movefcn(obj,eventdata,h)
% obj here is the *figure* containing the object

% Retrieving data saved in the figure
% Reminder: "position" refers to the object position in the axes
%           "point" refers to the location of the mouse pointer
initial_point = getappdata(h,'initial_point');
constraint = getappdata(h,'constraint_type');
p = getappdata(h,'constraint_parameters');
user_movefcn = getappdata(h,'user_movefcn');

% Getting current mouse position
current_point = get(gca,'CurrentPoint');

% Computing mouse movement (dpt is [dx dy])
cpt = current_point(1,1:2);
ipt = initial_point(1,1:2);
dpt = cpt - ipt;

% Dealing with the pathetic cases of zero or infinite slopes
if strcmpi(constraint,'d')
    if p(1) == 0
        constraint = 'h';
        p = p(2:end);
    elseif isinf(p(1))
        constraint = 'v';
        p = p(2:end);
    end
end

% Computing movement range and imposing movement constraints
% (p is always [xmin xmax ymin ymax])
switch lower(constraint)
    case 'n'
        range = p;
    case 'h'
        dpt(2) = 0;
        range = [p -inf inf];
    case 'v'
        dpt(1) = 0;
        range = [-inf inf p];
    case 'd'
        % Multiple options here as to how we use dpt to move the object
        % along a diagonal. 
        % We could use the largest of abs(dpt) for judging movement, but
        % this causes weird behavior in some cases. E.g. when the slope
        % is gentle (<1) and dy is the largest, the object will move
        % rapidly far away from the mouse pointer.
        % Another option (see below) is to follow dx when the 
        % slope is <1 and dy when the slope is >= 1.
 
        %if abs(p(1)) >=1
        %    dpt = [dpt(2)/p(1) dpt(2)];
        %else
        %    dpt = [dpt(1) p(1)*dpt(1)];
        %end
        
        % Projecting dpt along the diagonal seems to work really well.
        v = [1; p(1)];
        Pv = v*v'/(v'*v);
        dpt = dpt*Pv;
        
        range = p(2:5);
end

% Computing new position.
% What we want is actually a bit complex: we want the object to adopt the 
% new position, unless it gets out of range. If it gets out of range in a 
% direction, we want it to stick to the limit in that direction. Also, if 
% the object is out of range at the beginning of the movement, we want to 
% be able to move it back into range; movement must then be allowed.

% For debugging purposes only; setting debug to 1 shows range, extents,
% dpt, corrected dpt and in-range status of the object in the command
% window. Note: this will clear the command window.
debug = 0;
idpt = dpt;

% Computing object extent in the [x y w h] format before and after moving
initial_extent = getappdata(h,'initial_extent');
new_extent = initial_extent + [dpt 0 0];

% Verifying if old and new objects breach the allowed range in any
% direction (see the function is_inside_range below)
initial_inrange = is_inside_range(initial_extent,range);
new_inrange = is_inside_range(new_extent,range);

% Modifying dpt to stick to range limit if range violation occured,
% but the movement won't get restricted if the object was out of
% range to begin with.
%
% We use if/ends and no elseif's because once an object hits a range limit,
% it is still free to move along the other axis, and another range limit
% could be hit aftwards. That is, except for diagonal constraints, in 
% which a first limit hit must completely lock the object until the mouse
% is inside the range.

% In-line correction functions to dpt due to range violations
xminc = @(dpt) [range(1) - initial_extent(1) dpt(2)];
xmaxc = @(dpt) [range(2) - (initial_extent(1) + initial_extent(3)) dpt(2)];
yminc = @(dpt) [dpt(1) range(3) - initial_extent(2)];
ymaxc = @(dpt) [dpt(1) range(4) - (initial_extent(2) + initial_extent(4))];

% We build a list of corrections to apply
corrections = {};
if initial_inrange(1) && ~new_inrange(1)
    % was within, now out of xmin range -- add xminc
    corrections = [corrections {xminc}];
end
if initial_inrange(2) && ~new_inrange(2)
    % was within, now out of xmax range -- add xmaxc
    corrections = [corrections {xmaxc}];
end
if initial_inrange(3) && ~new_inrange(3)
    % was within, now out of ymin range -- add yminc
    corrections = [corrections {yminc}];
end
if initial_inrange(4) && ~new_inrange(4)
    % was within, now out of ymax range -- add ymaxc
    corrections = [corrections {ymaxc}];
end

% Applying all corrections, except for objects following a diagonal
% constraint, which must stop at the first one
if ~isempty(corrections)
    if strcmpi(constraint,'d')
        c = corrections{1};
        dpt = c(dpt);
        % Forcing the object to remain on the diagonal constraint
        if isequal(c,xminc) || isequal(c,xmaxc) % horizontal correction
            dpt(2) = p(1)*dpt(1);
        elseif isequal(c,yminc) || isequal(c,ymaxc) % vertical correction
            dpt(1) = dpt(2)/p(1);
        end
    else
        % Just applying all corrections
        for c = corrections
            dpt = c{1}(dpt);
        end
    end
end

% Debug messages
if debug
    if all(new_inrange)
        status = 'OK';
    else
        status = 'RANGE VIOLATION';
    end
    clc
    disp(sprintf('          range: %0.3f %0.3f %0.3f %0.3f', range));
    disp(sprintf(' initial extent: %0.3f %0.3f %0.3f %0.3f', initial_extent))
    disp(sprintf('     new extent: %0.3f %0.3f %0.3f %0.3f', new_extent))
    disp(sprintf('initial inrange: %d %d %d %d', initial_inrange))
    disp(sprintf('    new inrange: %d %d %d %d [%s]', new_inrange, status))
    disp(sprintf('    initial dpt: %0.3f %0.3f', idpt))
    disp(sprintf('  corrected dpt: %0.3f %0.3f', dpt))
end

% Re-computing new position with modified dpt
newpos = update_position(getappdata(h,'initial_position'),dpt);

% Setting the new position which actually moves the object
set_position(h,newpos);

% Calling user-provided function handle
if ~isempty(user_movefcn)
    feval(user_movefcn,h);
end;

% =========================================================================
% FUNCTION get_position
%   Return an object's position: [x y [z / w h]] or [xdata; ydata]
% =========================================================================
function pos = get_position(obj)
props = get(obj);
if isfield(props,'Position')
    pos = props.Position;
elseif isfield(props,'XData')
    pos = [props.XData(:)'; props.YData(:)'];
else
    error('Unable to find position');
end

% =========================================================================
% FUNCTION update_position
%   Adds dpt to a position specification as returned by get_position
% =========================================================================
function newpos = update_position(pos,dpt)
newpos = pos;
if size(pos,1) == 1 % [x y [z / w h]]
    newpos(1:2) = newpos(1:2) + dpt;
else                % [xdata; ydata]
    newpos(1,:) = newpos(1,:) + dpt(1);
    newpos(2,:) = newpos(2,:) + dpt(2);
end

% =========================================================================
% FUNCTION set_position
%   Sets the position of an object obj using get_position's format
% =========================================================================
function set_position(obj,pos)
if size(pos,1) == 1 % 'Position' property
    set(obj,'Position',pos);
else                % 'XData/YData' properties
    set(obj,'XData',pos(1,:),'YData',pos(2,:));
end

% =========================================================================
% FUNCTION compute_extent
%   Computes an object's extent for different object types;
%   extent is [x y w h]
% =========================================================================

function extent = compute_extent(obj)
props = get(obj);
if isfield(props,'Extent')
    extent = props.Extent;
elseif isfield(props,'Position')
    extent = props.Position;
elseif isfield(props,'XData')
    minx = min(props.XData);
    miny = min(props.YData);
    w = max(props.XData) - minx;
    h = max(props.YData) - miny;
    extent = [minx miny w h];
else
    error('Unable to compute extent');
end
    
% =========================================================================
% FUNCTION is_inside_range
%   Checks if a rectangular object is entirely inside a rectangular range
% =========================================================================

function inrange = is_inside_range(extent,range)
% extent is in the [x y w h] format
% range is in the [xmin xmax ymin ymax] format
% inrange is a 4x1 vector of boolean values corresponding to range limits
inrange = [extent(1) >= range(1) ...
           extent(1) + extent(3) <= range(2) ...
           extent(2) >= range(3) ...
           extent(2) + extent(4) <= range(4)];

Contact us