Enabling user callbacks during zoom/pan

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, example2) and the Answers forum, most recently yesterday, 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) 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'
Walter Roberson liked this post
Categories: Figure window, Listeners, Medium risk of breaking in future versions, Undocumented feature, Undocumented function

Tags: , , , , ,

Bookmark and SharePrint Print

8 Responses to Enabling user callbacks during zoom/pan

  1. Walter Roberson says:

    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 ?

    • @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.

  2. Maurizio says:

    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?

    • @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).

    • Maurizio says:

      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.

  3. i3v says:

    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.

  4. David says:

    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)

    • David says:

      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?

Leave a Reply

Your email address will not be published. Required fields are marked *