- Undocumented Matlab - https://undocumentedmatlab.com -

Enabling user callbacks during zoom/pan

Posted By Yair Altman On October 28, 2015 | 14 Comments

An annoying side-effect of Matlab figure uimodes (zoom, pan and rotate3d) is that they disable the user’s figure-wide callbacks (KeyPressFcn, KeyReleaseFcn, WindowKeyPressFcn, WindowKeyReleaseFcn, WindowButtonUpFcn, WindowButtonDownFcn and WindowScrollWheelFcn). In most cases, users will indeed expect the mouse and keyboard behavior to change when these modes are active (selecting a zoom region with the mouse for instance). However, in certain cases we may need to retain our custom callback behavior even when the modes are active. This is particularly relevant for the keyboard callbacks, which are not typically used to control the modes and yet may be very important for our figure’s interactive functionality. Unfortunately, Matlab’s mode manager installs property listeners that prevent users from modifying these callback when any mode is active:

>> hFig=figure; plot(1:5); zoom on
>> set(hFig, 'KeyPressFcn', @myKeyPressCallback)
Warning: Setting the "KeyPressFcn" property is not permitted while this mode is active.
(Type "warning off MATLAB:modes:mode:InvalidPropertySet" to suppress this warning.)
> In matlab.uitools.internal.uimodemanager>localModeWarn (line 211)
  In matlab.uitools.internaluimodemanager>@(obj,evd)(localModeWarn(obj,evd,hThis)) (line 86)
>> get(hFig, 'KeyPressFcn')  % the KeyPressFcn callback set by the mode manager
ans =
    [1x1 matlab.uitools.internal.uimode]
    {1x2 cell                          }

The question of how to override this limitation has appeared multiple times over the years in the CSSM newsgroup (example1 [1], example2 [2]) and the Answers forum, most recently yesterday [3], so I decided to dedicate today’s article to this issue.

In Matlab GUI, a “mode” (aka uimode) is a set of defined behaviors that may be set for any figure window and activated/deactivated via the figure’s menu, toolbar or programmatically. Examples of predefined modes are plot edit, zoom, pan, rotate3d and data-cursor. Only one mode can be active in a figure at any one time – this is managed via a figure’s ModeManager. Activating a figure mode automatically deactivates any other active mode, as can be seen when switching between plot edit, zoom, pan, rotate3d and data-cursor modes.
This mode manager is implemented in the uimodemanager class in %matlabroot%/toolbox/matlab/uitools/+matlab/+uitools/internal/@uimodemanager/uimodemanager.m and its sibling functions in the same folder. Until recently it used to be implemented in Matlab’s old class system, but was recently converted to the new MCOS (classdef) format, without an apparent major overhaul of the underlying logic (which I think is a pity, but refactoring naturally has lower priority than fixing bugs or adding new functionality).
Anyway, until HG2 (R2014b), the solution for bypassing the mode-manager’s callbacks hijacking was as follows (credit: Walter Roberson):

% This will only work in HG1 (R2014a or earlier)
hManager = uigetmodemanager(hFig);
set(hManager.WindowListenerHandles, 'Enable', 'off');
set(hFig, 'WindowKeyPressFcn', []);
set(hFig, 'KeyPressFcn', @myKeyPressCallback);

In HG2 (R2014b), the interface of WindowListenerHandles changed and the above code no longer works:

>> set(hManager.WindowListenerHandles, 'Enable', 'off');
Error using set
Cannot find 'set' method for event.proplistener class.

Luckily, in MathWorks’ refactoring to MCOS, the property name WindowListenerHandles was not changed, nor its status as a public read-only hidden property. So we can still access it directly. The only thing that changed is its meta-properties, and this is due not to any change in the mode-manager’s code, but rather to a much more general change in the implementation of the event.proplistener class:

>> hManager.WindowListenerHandles(1)
ans =
  proplistener with properties:
       Object: {[1x1 Figure]}
       Source: {7x1 cell}
    EventName: 'PreSet'
     Callback: @(obj,evd)(localModeWarn(obj,evd,hThis))
      Enabled: 0
    Recursive: 0

In the new proplistener, the Enabled meta-property is now a boolean (logical) value rather than a string, and was renamed Enabled in place of the original Enable. Also, the new proplistener doesn’t inherit hgsetget, so it does not have set and get methods, which is why we got an error above; instead, we should use direct dot-notation.
In summary, the code that should now be used, and is backward compatible with old HG1 releases as well as new HG2 releases, is as follows:

% This should work in both HG1 and HG2:
hManager = uigetmodemanager(hFig);
    set(hManager.WindowListenerHandles, 'Enable', 'off');  % HG1
    [hManager.WindowListenerHandles.Enabled] = deal(false);  % HG2
set(hFig, 'WindowKeyPressFcn', []);
set(hFig, 'KeyPressFcn', @myKeyPressCallback);

During an active mode, the mode-manager disables user-configured mouse and keyboard callbacks, in order to prevent interference with the mode’s functionality. For example, mouse clicking during zoom mode has a very specific meaning, and user-configured callbacks might interfere with it. Understanding this and taking the proper precautions (for example: chaining the default mode’s callback at the beginning or end of our own callback), we can override this default behavior, as shown above.
Note that the entire mechanism of mode-manager (and the related scribe objects [4]) is undocumented and might change without notice in future Matlab releases. We were fortunate in the current case that the change was small enough that a simple workaround could be found, but this may possibly not be the case next time around. It’s impossible to guess when such features will eventually break: it might happen in the very next release, or 20 years in the future. Since there are no real alternatives, we have little choice other than to use these features, and document the risk (the fact that they use undocumented aspects). In your documentation/comments, add a link back here so that if something ever breaks you’d be able to check if I posted any fix or workaround.
For the sake of completeness, here is a listing of the accessible properties (regular and hidden) of both the mode-manager as well as the zoom uimode, in R2015b:

>> warning('off', 'MATLAB:structOnObject');  % suppress warning about using structs (yes we know it's not good for us...)
>> struct(hManager)
ans =
                    CurrentMode: [1x1 matlab.uitools.internal.uimode]
                  DefaultUIMode: ''
                       Blocking: 0
                   FigureHandle: [1x1 Figure]
          WindowListenerHandles: [1x2 event.proplistener]
    WindowMotionListenerHandles: [1x2 event.proplistener]
                 DeleteListener: [1x1 event.listener]
            PreviousWindowState: []
                RegisteredModes: [1x1 matlab.uitools.internal.uimode]
>> struct(hManager.CurrentMode)
ans =
    WindowButtonMotionFcnInterrupt: 0
                UIControlInterrupt: 0
                   ShowContextMenu: 1
                         IsOneShot: 0
                    UseContextMenu: 'on'
                          Blocking: 0
               WindowJavaListeners: []
                    DeleteListener: [1x1 event.listener]
              FigureDeleteListener: [1x1 event.listener]
                    BusyActivating: 0
                              Name: 'Exploration.Zoom'
                      FigureHandle: [1x1 Figure]
             WindowListenerHandles: [1x2 event.proplistener]
                   RegisteredModes: []
                       CurrentMode: []
               ModeListenerHandles: []
               WindowButtonDownFcn: {@localWindowButtonDownFcn  [1x1 matlab.uitools.internal.uimode]}
                 WindowButtonUpFcn: []
             WindowButtonMotionFcn: {@localMotionFcn  [1x1 matlab.uitools.internal.uimode]}
                 WindowKeyPressFcn: []
               WindowKeyReleaseFcn: []
              WindowScrollWheelFcn: {@localButtonWheelFcn  [1x1 matlab.uitools.internal.uimode]}
                     KeyReleaseFcn: []
                       KeyPressFcn: {@localKeyPressFcn  [1x1 matlab.uitools.internal.uimode]}
                      ModeStartFcn: {@localStartZoom  [1x1 matlab.uitools.internal.uimode]}
                       ModeStopFcn: {@localStopZoom  [1x1 matlab.uitools.internal.uimode]}
                  ButtonDownFilter: []
                     UIContextMenu: []
                     ModeStateData: [1x1 struct]
                WindowFocusLostFcn: []
          UIControlSuspendListener: [1x1 event.listener]
      UIControlSuspendJavaListener: [1x1 handle.listener]
                   UserButtonUpFcn: []
               PreviousWindowState: []
                 ActionPreCallback: []
                ActionPostCallback: []
           WindowMotionFcnListener: [1x1 event.listener]
                       FigureState: [1x1 struct]
                        ParentMode: []
                     DefaultUIMode: ''
             CanvasContainerHandle: []
                            Enable: 'on'

Categories: Figure window, Listeners, Medium risk of breaking in future versions, Undocumented feature, Undocumented function

14 Comments (Open | Close)

14 Comments To "Enabling user callbacks during zoom/pan"

#1 Comment By Walter Roberson On October 28, 2015 @ 07:57

Thanks for the fix, Yair.

I notice that you use

[hManager.WindowListenerHandles.Enabled] = deal(false);

Is there a benefit to doing that compared to just

 hManager.WindowListenerHandles.Enabled = false;

I could see if it was a structure array where multiple destination might be set, but it is a property ?

#2 Comment By Yair Altman On October 28, 2015 @ 08:22

@Walter – hManager.WindowListenerHandles is indeed an array of objects (not a single object), so we must enclose it with [] and use deal() in order to set all its elements’ Enabled values to false. Of course, we could also use a simple loop, but using deal is simpler and easier.

#3 Comment By Maurizio On February 12, 2016 @ 23:14

Hi Yair, can you mix this approach with the way GUIDE sets callbacks?

This is part of the code I’m currently using, based on your post:

set(handles.figure1, 'KeyPressFcn', {@figure1_KeyPressFcn, handles});

One of the actions defined in figure1_KeyPressFcn increases a variable stored in handles (handles.index) when the right arrow key is pressed. However after running your bit of code the updated value of index is lost every time (i.e. I keep incrementing the same old value). I reckon that depends on hObject not being passed to figure1_KeyPressFcn in the previous snippet of code, which makes guidata(hObject, handles) kind of useless. Unfortunately the following code returns an error (too many input arguments):

set(handles.figure1, 'KeyPressFcn', {@figure1_KeyPressFcn, hObject, eventdata, handles});

Any idea?

#4 Comment By Yair Altman On February 13, 2016 @ 18:56

@Maurizio – you are wrong: the figure handle is the first input arg passed to your figure1_KeyPressFcn() callback function. Simply call handles=guidata(hObject) at the beginning of your callback and guidata(hObject,handles) after you modify handles and all should be ok.

In other words, if you keep passing handles as a static struct input arg, then of course it will always have the same values (the values that existed when you called the set(...) command).

#5 Comment By Maurizio On February 14, 2016 @ 17:19

Oh, I see what you mean. I’ll give that a try, thanks.
Meanwhile I fixed the problem with this line of code.

set(handles.figure1, 'KeyPressFcn', ...
           @(hObject,eventdata)myGui('figure1_KeyPressFcn', hObject,eventdata,guidata(hObject)));

I simply copied the anonymous function from the Property Inspector of figure1 in GUIDE.

#6 Comment By i3v On June 4, 2016 @ 18:25

Thanks for the code!
Still, it looks like these few lines is not the only thing to keep in mind.
I was unable to find a complete, fully working example, so I’ve decided to write my own. Hope it would be helpful for those who would try to to re-define these callbacks as well.

#7 Comment By David On August 17, 2016 @ 16:34

I’m trying to apply this approach in the zoomIn toolbar callback:

function zoomIn_OnCallback(hObject, eventdata, handles)
  [handles.mode_manager.WindowListenerHandles.Enabled] = deal(false);
  handles.theGui.WindowKeyPressFcn =  [];
  handles.theGui.KeyPressFcn = @theGui_KeyPressFcn;

where handles.mode_manager was set to uigetmodemanager in the GUIDE generated OpeningFcn. I can verify in the debugger that the call is executed and the callback is being assigned to theGui handle. The callback theGui_KeyPressFcn has the desired behavior when not in zoom mode, but in zoom mode it is not being called in spite of the above code.

Obviously I’ve missed something here, but I can’t see what it is.
(I’m not concerned about backwards compatibility, so I skipped the try/catch)

#8 Comment By David On August 17, 2016 @ 18:11

I explored this a bit more and the problem is specific to the key input ctrl-z. Any other keystroke is passed to my callback, but ctrl-z is trapped and interpreted by the zoom’s own callback. The question remains how do I override this behavior?

#9 Comment By Thommy On October 20, 2017 @ 10:15

Thank you mate, I’ve passed quite some time on that issue. You made my day !

#10 Comment By Donato On March 21, 2020 @ 20:12

Hi Yair,
it seems that in Matlab R2019b this method doesn’t work.
Have you some solutions?


#11 Comment By Hannes On August 19, 2020 @ 14:05

It is also not working for me on R2019a Update 5. Best Hannes.

#12 Comment By Sebastian Hölz On February 2, 2021 @ 14:16

I have not investigated this in detail, but I think one way to go in new versions of Matlab (>2019b) might be to use listeners to suitable figure properties, e.g.

fig = figure;
ax = axes;
ls = addlistener(fig,'CurrentCharacter','PostSet',@(h,e)disp(e));
zoom on

This solves one problem, as the KeyPress is directed to the callback function as desired. However, a new problem is that the figure loses the focus since the KeyPress is also directed to the command window.
I have not found a quick way to solve this problem, but maybe somebody else does …

#13 Comment By Sebastian Hölz On March 5, 2021 @ 01:19

OK, Been working on this again and found a solution which works in 2020a and 2020b. Below a working example which resets the KeyPressFcn for all ui-modes after activating them. It seems it is necessary to reset the 3rd KeyPressFcn, don’t ask me what the others are for …

HV = 'off';
fig = figure('HandleVisibility',HV);
ax  = axes('Parent',fig, 'HandleVisibility',HV);
plot(rand(1,12),'k.-', 'Parent',ax)
hManager = uigetmodemanager(fig);
function MonitorModeChange(varargin)
%hManager = uigetmodemanager(fig);
    if ~isempty(hManager.CurrentMode)
        switch hManager.CurrentMode.Name
            case 'Exploration.Brushing'
                disp('Brushing on')
                [hManager.WindowListenerHandles.Enabled] = deal(false);
                fig.KeyPressFcn{3} = @(h,e)disp('Brush KeyPressFcn replacement');
            case 'Exploration.Pan'
                disp('Pan on')
                [hManager.WindowListenerHandles.Enabled] = deal(false);
                fig.KeyPressFcn{3} = @(h,e)disp('Pan KeyPressFcn replacement');
            case 'Exploration.Zoom'
                disp('Zoom on')
                [hManager.WindowListenerHandles.Enabled] = deal(false);
                fig.KeyPressFcn{3} = @(h,e)disp('Zoom KeyPressFcn replacement');                
            case 'Exploration.Datacursor'
                disp('Zoom on')
                [hManager.WindowListenerHandles.Enabled] = deal(false);
                fig.KeyPressFcn{3} = @(h,e)disp('DataCursor KeyPressFcn replacement');

#14 Comment By Tommy On January 17, 2022 @ 04:41

Just adding my implementation inspired by Sebastian and i3v.

It handles the state reset that occurs when uimode changes. Also, has options to ignore/append for specific modes.

Only tested with 2019b and uses event.proplistener()

Article printed from Undocumented Matlab: https://undocumentedmatlab.com

URL to article: https://undocumentedmatlab.com/articles/enabling-user-callbacks-during-zoom-pan

URLs in this post:

[1] example1: https://www.mathworks.com/matlabcentral/newsreader/view_thread/235114

[2] example2: https://www.mathworks.com/matlabcentral/newsreader/view_thread/283529

[3] most recently yesterday: http://www.mathworks.com/matlabcentral/answers/251339-re-enable-keypress-capture-in-pan-or-zoom-mode

[4] scribe objects: http://undocumentedmatlab.com/blog/tag/scribe

[5] User-defined tab completions – take 2 : https://undocumentedmatlab.com/articles/user-defined-tab-completions-take-2

[6] Determining axes zoom state : https://undocumentedmatlab.com/articles/determining-axes-zoom-state

[7] Matlab callbacks for Java events : https://undocumentedmatlab.com/articles/matlab-callbacks-for-java-events

[8] Matlab callbacks for Java events in R2014a : https://undocumentedmatlab.com/articles/matlab-callbacks-for-java-events-in-r2014a

[9] uisplittool & uitogglesplittool callbacks : https://undocumentedmatlab.com/articles/uisplittool-uitogglesplittool-callbacks

[10] Removing user preferences from deployed apps : https://undocumentedmatlab.com/articles/removing-user-preferences-from-deployed-apps

[11] : http://www.mathworks.com/matlabcentral/fileexchange/57496-i3v-figkeys

[12] : https://github.com/tommyhosman/UseMyKeypress

Copyright © Yair Altman - Undocumented Matlab. All rights reserved.